| page.title=GCM Cloud Connection Server (XMPP) |
| @jd:body |
| |
| <div id="qv-wrapper"> |
| <div id="qv"> |
| |
| |
| <h2>In this document</h2> |
| |
| <ol class="toc"> |
| <li><a href="#connecting">Establishing a Connection</a> |
| <ol class="toc"> |
| <li><a href="#auth">Authentication</a></li> |
| </ol> |
| </li> |
| <li><a href="#format">Message Format</a> |
| <ol class="toc"> |
| <li><a href="#request">Request format</a></li> |
| <li><a href="#response">Response format</a></li> |
| </ol> |
| </li> |
| <li><a href="#upstream">Upstream Messages</a> |
| <ol> |
| <li><a href="#receipts">Receive delivery receipts</a></li> |
| </ol> |
| </li> |
| <li><a href="#flow">Flow Control</a> </li> |
| <li><a href="#implement">Implementing an XMPP-based App Server</a> |
| <ol class="toc"> |
| <li><a href="#smack">Java sample using the Smack library</a></li> |
| <li><a href="#python">Python sample</a></li> |
| </ol> |
| </li> |
| </ol> |
| |
| <h2>See Also</h2> |
| |
| <ol class="toc"> |
| <li><a href="server-ref.html">Server Reference</a></li> |
| <li><a href="{@docRoot}google/gcm/http.html">HTTP</a></li> |
| <li><a href="{@docRoot}google/gcm/gs.html">Getting Started</a></li> |
| <li><a href="{@docRoot}google/gcm/server.html">Implementing GCM Server</a></li> |
| <li><a href="{@docRoot}google/gcm/client.html">Implementing GCM Client</a></li> |
| </ol> |
| |
| </div> |
| </div> |
| |
| <p>The Google Cloud Messaging (GCM) Cloud Connection Server (CCS) is an XMPP endpoint that provides a |
| persistent, asynchronous, bidirectional connection to Google servers. The |
| connection can be used to send and receive messages between your server and |
| your users' GCM-connected devices.</p> |
| |
| <p class="note"><strong>Note:</strong> The content in this document |
| applies to <a href="http://developer.chrome.com/apps/cloudMessaging"> |
| GCM with Chrome apps</a> as well as Android. |
| |
| <p>You can continue to use the HTTP request mechanism to send messages to GCM |
| servers, side-by-side with CCS which uses XMPP. Some of the benefits of CCS include:</p> |
| |
| <ul> |
| <li>The asynchronous nature of XMPP allows you to send more messages with fewer |
| resources.</li> |
| <li>Communication is bidirectional—not only can your server send messages |
| to the device, but the device can send messages back to your server.</li> |
| <li>The device can send messages back using the same connection used for receiving, |
| thereby improving battery life.</li> |
| </ul> |
| |
| <p>The upstream messaging (device-to-cloud) feature of CCS is part of the Google |
| Play services platform. Upstream messaging is available through the |
| <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html"> |
| {@code GoogleCloudMessaging}</a> |
| APIs. For examples, see |
| <a href="#implement">Implementing an XMPP-based App Server</a>.</p> |
| |
| <p class="note"><strong>Note:</strong> See the |
| <a href="server-ref.html">Server Reference</a> for a list of all the message |
| parameters and which connection server(s) supports them.</p> |
| |
| <h2 id="connecting">Establishing a Connection</h2> |
| |
| <p>CCS just uses XMPP as an authenticated transport layer, so you can use most |
| XMPP libraries to manage the connection. For an example, see <a href="#smack"> |
| Java sample using the Smack library</a>.</p> |
| |
| <p>The CCS XMPP endpoint runs at {@code gcm.googleapis.com:5235}. When testing |
| functionality (with non-production users), you should instead connect to |
| {@code gcm-preprod.googleapis.com:5236} (note the different port). Regular |
| testing on preprod (a smaller environment where the latest CCS builds run) is |
| beneficial both for isolating real users from test code, as well as for early |
| detection of unexpected behavior changes. Note that a connection receives upstream |
| messages destined for its GCM sender ID, regardless of which environment (gcm or |
| gcm-preprod) it is connected to. Therefore, test code connecting to |
| {@code gcm-preprod.googleapis.com:5236} should use a different GCM sender ID to |
| avoid upstream messages from production traffic being sent over test connections.</p> |
| |
| <p>The connection has two important requirements:</p> |
| |
| <ul> |
| <li>You must initiate a Transport Layer Security (TLS) connection. Note that |
| CCS doesn't currently support the <a href="http://xmpp.org/rfcs/rfc3920.html" |
| class="external-link" target="_android">STARTTLS extension</a>.</li> |
| <li>CCS requires a SASL PLAIN authentication mechanism using |
| {@code <your_GCM_Sender_Id>@gcm.googleapis.com} (GCM sender ID) |
| and the API key as the password, where the sender ID and API key are the same |
| as described in <a href="gs.html">Getting Started</a>.</li> |
| </ul> |
| |
| <p>If at any point the connection fails, you should immediately reconnect. |
| There is no need to back off after a disconnect that happens after |
| authentication.</p> |
| |
| <h3 id="auth">Authentication</h3> |
| |
| <p>The following snippets illustrate how to perform authentication in CCS.</p> |
| <h4>Client</h4> |
| <pre><stream:stream to="gcm.googleapis.com" |
| version="1.0" xmlns="jabber:client" |
| xmlns:stream="http://etherx.jabber.org/streams"/> |
| </pre> |
| <h4>Server</h4> |
| <pre><str:features xmlns:str="http://etherx.jabber.org/streams"> |
| <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> |
| <mechanism>X-OAUTH2</mechanism> |
| <mechanism>X-GOOGLE-TOKEN</mechanism> |
| <mechanism>PLAIN</mechanism> |
| </mechanisms> |
| </str:features> |
| </pre> |
| |
| <h4>Client</h4> |
| <pre><auth mechanism="PLAIN" |
| xmlns="urn:ietf:params:xml:ns:xmpp-sasl">MTI2MjAwMzQ3OTMzQHByb2plY3RzLmdjbS5hb |
| mFTeUIzcmNaTmtmbnFLZEZiOW1oekNCaVlwT1JEQTJKV1d0dw==</auth> |
| </pre> |
| |
| <h4>Server</h4> |
| <pre><success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/></pre> |
| |
| <h2 id="format">Message Format</h2> |
| <p>Once the XMPP connection is established, CCS and your server use normal XMPP |
| <code><message></code> stanzas to send JSON-encoded messages back and |
| forth. The body of the <code><message></code> must be:</p> |
| <pre> |
| <gcm xmlns:google:mobile:data> |
| <em>JSON payload</em> |
| </gcm> |
| </pre> |
| |
| <p>The JSON payload for regular GCM messages is similar to |
| <a href="http.html#request">what the GCM http endpoint uses</a>, with these |
| exceptions:</p> |
| <ul> |
| <li>There is no support for multiple recipients.</li> |
| <li>{@code to} is used instead of {@code registration_ids}.</li> |
| <li>CCS adds the field {@code message_id}, which is required. This ID uniquely |
| identifies the message in an XMPP connection. The ACK or NACK from CCS uses the |
| {@code message_id} to identify a message sent from 3rd-party app servers to CCS. |
| Therefore, it's important that this {@code message_id} not only be unique (per |
| sender ID), but always present.</li> |
| </ul> |
| |
| <p>In addition to regular GCM messages, control messages are sent, indicated by |
| the {@code message_type} field in the JSON object. The value can be either |
| 'ack' or 'nack', or 'control' (see formats below). Any GCM message with an |
| unknown {@code message_type} can be ignored by your server.</p> |
| |
| <p>For each device message your app server receives from CCS, it needs to send |
| an ACK message. |
| It never needs to send a NACK message. If you don't send an ACK for a message, |
| CCS will just resend it. |
| </p> |
| <p>CCS also sends an ACK or NACK for each server-to-device message. If you do not |
| receive either, it means that the TCP connection was closed in the middle of the |
| operation and your server needs to resend the messages. See |
| <a href="#flow">Flow Control</a> for details. |
| </p> |
| |
| <p class="note"><strong>Note:</strong> See the |
| <a href="server-ref.html">Server Reference</a> for a list of all the message |
| parameters and which connection server(s) supports them.</p> |
| |
| <h3 id="request">Request format</h3> |
| |
| <p>Here is an XMPP stanza containing the JSON message from a 3rd-party app server to CCS: |
| |
| </p> |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "to":"REGISTRATION_ID", // "to" replaces "registration_ids" |
| "message_id":"m-1366082849205" // new required field |
| "data": |
| { |
| "hello":"world", |
| } |
| "time_to_live":"600", |
| "delay_while_idle": true/false, |
| "delivery_receipt_requested": true/false |
| } |
| </gcm> |
| </message> |
| </pre> |
| |
| <h3 id="response">Response format</h3> |
| |
| <p>A CCS response can have 3 possible forms. The first one is a regular 'ack' |
| message. But when the response contains an error, there are 2 |
| different forms the message can take, described below.</p> |
| |
| <h4 id="ack">ACK message</h4> |
| |
| <p>Here is an XMPP stanza containing the ACK/NACK message from CCS to 3rd-party app server: |
| </p> |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "from":"REGID", |
| "message_id":"m-1366082849205" |
| "message_type":"ack" |
| } |
| </gcm> |
| </message> |
| </pre> |
| |
| <h4 id="nack">NACK message</h4> |
| |
| <p>A NACK error is a regular XMPP message in which the {@code message_type} status |
| message is "nack". A NACK message contains:</p> |
| <ul> |
| <li>Nack error code.</li> |
| <li>Nack error description.</li> |
| </ul> |
| |
| <p>Below are some examples.</p> |
| |
| <p>Bad registration:</p> |
| |
| <pre><message> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "message_type":"nack", |
| "message_id":"msgId1", |
| "from":"SomeInvalidRegistrationId", |
| "error":"BAD_REGISTRATION", |
| "error_description":"Invalid token on 'to' field: SomeInvalidRegistrationId" |
| } |
| </gcm> |
| </message></pre> |
| |
| <p>Invalid JSON:</p> |
| |
| <pre><message> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "message_type":"nack", |
| "message_id":"msgId1", |
| "from":"APA91bHFOtaQGSwupt5l1og", |
| "error":"INVALID_JSON", |
| "error_description":"InvalidJson: JSON_TYPE_ERROR : Field \"time_to_live\" must be a JSON java.lang.Number: abc" |
| } |
| </gcm> |
| </message> |
| </pre> |
| |
| <p>Device Message Rate Exceeded:</p> |
| |
| <pre><message id="..."> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "message_type":"nack", |
| "message_id":"msgId1", |
| "from":"REGID", |
| "error":"DEVICE_MESSAGE_RATE_EXCEEDED", |
| "error_description":"Downstream message rate exceeded for this registration id" |
| } |
| </gcm> |
| </message> |
| </pre> |
| |
| <p>See the <a href="server-ref.html#table11">Server Reference</a> for a complete list of the |
| NACK error codes. Unless otherwise |
| indicated, a NACKed message should not be retried. Unexpected NACK error codes |
| should be treated the same as {@code INTERNAL_SERVER_ERROR}.</p> |
| |
| <h4 id="stanza">Stanza error</h4> |
| |
| <p>You can also get a stanza error in certain cases. |
| A stanza error contains:</p> |
| <ul> |
| <li>Stanza error code.</li> |
| <li>Stanza error description (free text).</li> |
| </ul> |
| <p>For example:</p> |
| |
| <pre><message id="3" type="error" to="123456789@gcm.googleapis.com/ABC"> |
| <gcm xmlns="google:mobile:data"> |
| {"random": "text"} |
| </gcm> |
| <error code="400" type="modify"> |
| <bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/> |
| <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"> |
| InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n |
| </text> |
| </error> |
| </message> |
| </pre> |
| |
| <h4 id="control">Control messages</h4> |
| |
| <p>Periodically, CCS needs to close down a connection to perform load balancing. Before it |
| closes the connection, CCS sends a {@code CONNECTION_DRAINING} message to indicate that the connection is being drained |
| and will be closed soon. "Draining" refers to shutting off the flow of messages coming into a |
| connection, but allowing whatever is already in the pipeline to continue. When you receive |
| a {@code CONNECTION_DRAINING} message, you should immediately begin sending messages to another CCS |
| connection, opening a new connection if necessary. You should, however, keep the original |
| connection open and continue receiving messages that may come over the connection (and |
| ACKing them)—CCS will handle initiating a connection close when it is ready.</p> |
| |
| <p>The {@code CONNECTION_DRAINING} message looks like this:</p> |
| <pre><message> |
| <data:gcm xmlns:data="google:mobile:data"> |
| { |
| "message_type":"control" |
| "control_type":"CONNECTION_DRAINING" |
| } |
| </data:gcm> |
| </message></pre> |
| |
| <p>{@code CONNECTION_DRAINING} is currently the only {@code control_type} supported.</p> |
| |
| <h2 id="upstream">Upstream Messages</h2> |
| |
| <p>Using CCS and the |
| <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html"> |
| {@code GoogleCloudMessaging}</a> |
| API, you can send messages from a user's device to the cloud.</p> |
| |
| <p>Here is how you send an upstream message using the |
| <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html"> |
| {@code GoogleCloudMessaging}</a> |
| API. For a complete example, see <a href="client.html">Implementing GCM Client</a>:</p> |
| |
| <pre>GoogleCloudMessaging gcm = GoogleCloudMessaging.get(context); |
| String GCM_SENDER_ID = "Your-Sender-ID"; |
| AtomicInteger msgId = new AtomicInteger(); |
| String id = Integer.toString(msgId.incrementAndGet()); |
| Bundle data = new Bundle(); |
| // Bundle data consists of a key-value pair |
| data.putString("hello", "world"); |
| // "time to live" parameter |
| // This is optional. It specifies a value in seconds up to 24 hours. |
| int ttl = [0 seconds, 24 hours] |
| |
| gcm.send(GCM_SENDER_ID + "@gcm.googleapis.com", id, ttl, data); |
| </pre> |
| |
| <p>This call generates the necessary XMPP stanza for sending the upstream message. |
| The message goes from the app on the device to CCS to the 3rd-party app server. |
| The stanza has the following format:</p> |
| |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "category":"com.example.yourapp", // to know which app sent it |
| "data": |
| { |
| "hello":"world", |
| }, |
| "message_id":"m-123", |
| "from":"REGID" |
| } |
| </gcm> |
| </message></pre> |
| |
| <p>Here is the format of the ACK expected by CCS from 3rd-party app servers in |
| response to the above message:</p> |
| |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "to":"REGID", |
| "message_id":"m-123" |
| "message_type":"ack" |
| } |
| </gcm> |
| </message></pre> |
| |
| <h3 id="receipts">Receive delivery receipts</h3> |
| |
| <p>You can use upstream messaging to get delivery receipts (sent from CCS to |
| your 3rd party app server) when |
| a device confirms that it received a message sent by CCS.</p> |
| |
| <p>To enable this feature, the message your 3rd-party app server sends to CCS must include |
| a field called <code>"delivery_receipt_requested"</code>. When this field is set to |
| <code>true</code>, CCS sends a delivery receipt when a device confirms that it received a particular message.</p> |
| |
| <p>Here is an XMPP stanza containing a JSON |
| message with <code>"delivery_receipt_requested"</code> set to <code>true</code>:</p> |
| |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "to":"REGISTRATION_ID", |
| "message_id":"m-1366082849205" |
| "data": |
| { |
| "hello":"world", |
| } |
| "time_to_live":"600", |
| "delay_while_idle": true, |
| <strong>"delivery_receipt_requested": true</strong> |
| } |
| </gcm> |
| </message> |
| </pre> |
| |
| |
| |
| <p>Here is an example of the delivery receipt that CCS sends to tell your 3rd-party |
| app server that a device received a message that CCS sent it:</p> |
| |
| </p> |
| <pre><message id=""> |
| <gcm xmlns="google:mobile:data"> |
| { |
| "category":"com.example.yourapp", // to know which app sent it |
| "data": |
| { |
| “message_status":"MESSAGE_SENT_TO_DEVICE", |
| “original_message_id”:”m-1366082849205” |
| “device_registration_id”: “REGISTRATION_ID” |
| }, |
| "message_id":"dr2:m-1366082849205", |
| "message_type":"receipt", |
| "from":"gcm.googleapis.com" |
| } |
| </gcm> |
| </message></pre> |
| |
| <p>Note the following:</p> |
| |
| <ul> |
| <li>The {@code "message_type"} is set to {@code "receipt"}. |
| <li>The {@code "message_status"} is set to {@code "MESSAGE_SENT_TO_DEVICE"}, |
| indicating that the device received the message. Notice that in this case, |
| {@code "message_status"} is not a field but rather part of the data payload.</li> |
| <li>The receipt message ID consists of the original message ID, but with a |
| <code>dr2:</code> prefix. Your 3rd-party app server must send an ACK back with this ID, |
| which in this example is {@code dr2:m-1366082849205}.</li> |
| <li>The original message ID, the device registration ID, and the status are inside the |
| {@code "data"} field.</li> |
| </ul> |
| |
| <h2 id="flow">Flow Control</h2> |
| |
| <p>Every message sent to CCS receives either an ACK or a NACK response. Messages |
| that haven't received one of these responses are considered pending. If the pending |
| message count reaches 100, the 3rd-party app server should stop sending new messages |
| and wait for CCS to acknowledge some of the existing pending messages as illustrated in |
| figure 1:</p> |
| |
| <img src="{@docRoot}images/gcm/CCS-ack.png"> |
| |
| <p class="img-caption"> |
| <strong>Figure 1.</strong> Message/ack flow. |
| </p> |
| |
| <p>Conversely, to avoid overloading the 3rd-party app server, CCS will stop sending |
| if there are too many unacknowledged messages. Therefore, the 3rd-party app server |
| should "ACK" upstream messages, received from the client application via CCS, as soon as possible |
| to maintain a constant flow of incoming messages. The aforementioned pending message limit doesn't |
| apply to these ACKs. Even if the pending message count reaches 100, the 3rd-party app server |
| should continue sending ACKs for messages received from CCS to avoid blocking delivery of new |
| upstream messages.</p> |
| |
| <p>ACKs are only valid within the context of one connection. If the connection is |
| closed before a message can be ACKed, the 3rd-party app server should wait for CCS |
| to resend the upstream message before ACKing it again. Similarly, all pending messages for which an |
| ACK/NACK was not received from CCS before the connection was closed should be sent again. |
| </p> |
| |
| <h2 id="implement">Implementing an XMPP-based App Server</h2> |
| |
| <p>This section gives examples of implementing an app server that works with CCS. |
| Note that a full GCM implementation requires a client-side implementation, in |
| addition to the server. For more information, see <a href="client.html"> |
| Implementing GCM Client</a>.</a> |
| |
| <h3 id="smack">Java sample using the Smack library</h3> |
| |
| <p>Here is a sample app server written in Java, using the |
| <a href="http://www.igniterealtime.org/projects/smack/">Smack</a> library.</p> |
| |
| <pre>import org.jivesoftware.smack.ConnectionConfiguration; |
| import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; |
| import org.jivesoftware.smack.ConnectionListener; |
| import org.jivesoftware.smack.PacketInterceptor; |
| import org.jivesoftware.smack.PacketListener; |
| import org.jivesoftware.smack.SmackException; |
| import org.jivesoftware.smack.SmackException.NotConnectedException; |
| import org.jivesoftware.smack.XMPPConnection; |
| import org.jivesoftware.smack.XMPPException; |
| import org.jivesoftware.smack.filter.PacketTypeFilter; |
| import org.jivesoftware.smack.packet.DefaultPacketExtension; |
| import org.jivesoftware.smack.packet.Message; |
| import org.jivesoftware.smack.packet.Packet; |
| import org.jivesoftware.smack.packet.PacketExtension; |
| import org.jivesoftware.smack.provider.PacketExtensionProvider; |
| import org.jivesoftware.smack.provider.ProviderManager; |
| import org.jivesoftware.smack.tcp.XMPPTCPConnection; |
| import org.jivesoftware.smack.util.StringUtils; |
| import org.json.simple.JSONValue; |
| import org.json.simple.parser.ParseException; |
| import org.xmlpull.v1.XmlPullParser; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| |
| import javax.net.ssl.SSLSocketFactory; |
| |
| /** |
| * Sample Smack implementation of a client for GCM Cloud Connection Server. This |
| * code can be run as a standalone CCS client. |
| * |
| * <p>For illustration purposes only. |
| */ |
| public class SmackCcsClient { |
| |
| private static final Logger logger = Logger.getLogger("SmackCcsClient"); |
| |
| private static final String GCM_SERVER = "gcm.googleapis.com"; |
| private static final int GCM_PORT = 5235; |
| |
| private static final String GCM_ELEMENT_NAME = "gcm"; |
| private static final String GCM_NAMESPACE = "google:mobile:data"; |
| |
| static { |
| |
| ProviderManager.addExtensionProvider(GCM_ELEMENT_NAME, GCM_NAMESPACE, |
| new PacketExtensionProvider() { |
| @Override |
| public PacketExtension parseExtension(XmlPullParser parser) throws |
| Exception { |
| String json = parser.nextText(); |
| return new GcmPacketExtension(json); |
| } |
| }); |
| } |
| |
| private XMPPConnection connection; |
| |
| /** |
| * Indicates whether the connection is in draining state, which means that it |
| * will not accept any new downstream messages. |
| */ |
| protected volatile boolean connectionDraining = false; |
| |
| /** |
| * Sends a downstream message to GCM. |
| * |
| * @return true if the message has been successfully sent. |
| */ |
| public boolean sendDownstreamMessage(String jsonRequest) throws |
| NotConnectedException { |
| if (!connectionDraining) { |
| send(jsonRequest); |
| return true; |
| } |
| logger.info("Dropping downstream message since the connection is draining"); |
| return false; |
| } |
| |
| /** |
| * Returns a random message id to uniquely identify a message. |
| * |
| * <p>Note: This is generated by a pseudo random number generator for |
| * illustration purpose, and is not guaranteed to be unique. |
| */ |
| public String nextMessageId() { |
| return "m-" + UUID.randomUUID().toString(); |
| } |
| |
| /** |
| * Sends a packet with contents provided. |
| */ |
| protected void send(String jsonRequest) throws NotConnectedException { |
| Packet request = new GcmPacketExtension(jsonRequest).toPacket(); |
| connection.sendPacket(request); |
| } |
| |
| /** |
| * Handles an upstream data message from a device application. |
| * |
| * <p>This sample echo server sends an echo message back to the device. |
| * Subclasses should override this method to properly process upstream messages. |
| */ |
| protected void handleUpstreamMessage(Map<String, Object> jsonObject) { |
| // PackageName of the application that sent this message. |
| String category = (String) jsonObject.get("category"); |
| String from = (String) jsonObject.get("from"); |
| @SuppressWarnings("unchecked") |
| Map<String, String> payload = (Map<String, String>) jsonObject.get("data"); |
| payload.put("ECHO", "Application: " + category); |
| |
| // Send an ECHO response back |
| String echo = createJsonMessage(from, nextMessageId(), payload, |
| "echo:CollapseKey", null, false); |
| |
| try { |
| sendDownstreamMessage(echo); |
| } catch (NotConnectedException e) { |
| logger.log(Level.WARNING, "Not connected anymore, echo message is |
| not sent", e); |
| } |
| } |
| |
| /** |
| * Handles an ACK. |
| * |
| * <p>Logs a {@code INFO} message, but subclasses could override it to |
| * properly handle ACKs. |
| */ |
| protected void handleAckReceipt(Map<String, Object> jsonObject) { |
| String messageId = (String) jsonObject.get("message_id"); |
| String from = (String) jsonObject.get("from"); |
| logger.log(Level.INFO, "handleAckReceipt() from: " + from + ", |
| messageId: " + messageId); |
| } |
| |
| /** |
| * Handles a NACK. |
| * |
| * <p>Logs a {@code INFO} message, but subclasses could override it to |
| * properly handle NACKs. |
| */ |
| protected void handleNackReceipt(Map<String, Object> jsonObject) { |
| String messageId = (String) jsonObject.get("message_id"); |
| String from = (String) jsonObject.get("from"); |
| logger.log(Level.INFO, "handleNackReceipt() from: " + from + ", |
| messageId: " + messageId); |
| } |
| |
| protected void handleControlMessage(Map<String, Object> jsonObject) { |
| logger.log(Level.INFO, "handleControlMessage(): " + jsonObject); |
| String controlType = (String) jsonObject.get("control_type"); |
| if ("CONNECTION_DRAINING".equals(controlType)) { |
| connectionDraining = true; |
| } else { |
| logger.log(Level.INFO, "Unrecognized control type: %s. This could |
| happen if new features are " + "added to the CCS protocol.", |
| controlType); |
| } |
| } |
| |
| /** |
| * Creates a JSON encoded GCM message. |
| * |
| * @param to RegistrationId of the target device (Required). |
| * @param messageId Unique messageId for which CCS will send an |
| * "ack/nack" (Required). |
| * @param payload Message content intended for the application. (Optional). |
| * @param collapseKey GCM collapse_key parameter (Optional). |
| * @param timeToLive GCM time_to_live parameter (Optional). |
| * @param delayWhileIdle GCM delay_while_idle parameter (Optional). |
| * @return JSON encoded GCM message. |
| */ |
| public static String createJsonMessage(String to, String messageId, |
| Map<String, String> payload, String collapseKey, Long timeToLive, |
| Boolean delayWhileIdle) { |
| Map<String, Object> message = new HashMap<String, Object>(); |
| message.put("to", to); |
| if (collapseKey != null) { |
| message.put("collapse_key", collapseKey); |
| } |
| if (timeToLive != null) { |
| message.put("time_to_live", timeToLive); |
| } |
| if (delayWhileIdle != null && delayWhileIdle) { |
| message.put("delay_while_idle", true); |
| } |
| message.put("message_id", messageId); |
| message.put("data", payload); |
| return JSONValue.toJSONString(message); |
| } |
| |
| /** |
| * Creates a JSON encoded ACK message for an upstream message received |
| * from an application. |
| * |
| * @param to RegistrationId of the device who sent the upstream message. |
| * @param messageId messageId of the upstream message to be acknowledged to CCS. |
| * @return JSON encoded ack. |
| */ |
| protected static String createJsonAck(String to, String messageId) { |
| Map<String, Object> message = new HashMap<String, Object>(); |
| message.put("message_type", "ack"); |
| message.put("to", to); |
| message.put("message_id", messageId); |
| return JSONValue.toJSONString(message); |
| } |
| |
| /** |
| * Connects to GCM Cloud Connection Server using the supplied credentials. |
| * |
| * @param senderId Your GCM project number |
| * @param apiKey API Key of your project |
| */ |
| public void connect(long senderId, String apiKey) |
| throws XMPPException, IOException, SmackException { |
| ConnectionConfiguration config = |
| new ConnectionConfiguration(GCM_SERVER, GCM_PORT); |
| config.setSecurityMode(SecurityMode.enabled); |
| config.setReconnectionAllowed(true); |
| config.setRosterLoadedAtLogin(false); |
| config.setSendPresence(false); |
| config.setSocketFactory(SSLSocketFactory.getDefault()); |
| |
| connection = new XMPPTCPConnection(config); |
| connection.connect(); |
| |
| connection.addConnectionListener(new LoggingConnectionListener()); |
| |
| // Handle incoming packets |
| connection.addPacketListener(new PacketListener() { |
| |
| @Override |
| public void processPacket(Packet packet) { |
| logger.log(Level.INFO, "Received: " + packet.toXML()); |
| Message incomingMessage = (Message) packet; |
| GcmPacketExtension gcmPacket = |
| (GcmPacketExtension) incomingMessage. |
| getExtension(GCM_NAMESPACE); |
| String json = gcmPacket.getJson(); |
| try { |
| @SuppressWarnings("unchecked") |
| Map<String, Object> jsonObject = |
| (Map<String, Object>) JSONValue. |
| parseWithException(json); |
| |
| // present for "ack"/"nack", null otherwise |
| Object messageType = jsonObject.get("message_type"); |
| |
| if (messageType == null) { |
| // Normal upstream data message |
| handleUpstreamMessage(jsonObject); |
| |
| // Send ACK to CCS |
| String messageId = (String) jsonObject.get("message_id"); |
| String from = (String) jsonObject.get("from"); |
| String ack = createJsonAck(from, messageId); |
| send(ack); |
| } else if ("ack".equals(messageType.toString())) { |
| // Process Ack |
| handleAckReceipt(jsonObject); |
| } else if ("nack".equals(messageType.toString())) { |
| // Process Nack |
| handleNackReceipt(jsonObject); |
| } else if ("control".equals(messageType.toString())) { |
| // Process control message |
| handleControlMessage(jsonObject); |
| } else { |
| logger.log(Level.WARNING, |
| "Unrecognized message type (%s)", |
| messageType.toString()); |
| } |
| } catch (ParseException e) { |
| logger.log(Level.SEVERE, "Error parsing JSON " + json, e); |
| } catch (Exception e) { |
| logger.log(Level.SEVERE, "Failed to process packet", e); |
| } |
| } |
| }, new PacketTypeFilter(Message.class)); |
| |
| // Log all outgoing packets |
| connection.addPacketInterceptor(new PacketInterceptor() { |
| @Override |
| public void interceptPacket(Packet packet) { |
| logger.log(Level.INFO, "Sent: {0}", packet.toXML()); |
| } |
| }, new PacketTypeFilter(Message.class)); |
| |
| connection.login(senderId + "@gcm.googleapis.com", apiKey); |
| } |
| |
| public static void main(String[] args) throws Exception { |
| final long senderId = 1234567890L; // your GCM sender id |
| final String password = "Your API key"; |
| |
| SmackCcsClient ccsClient = new SmackCcsClient(); |
| |
| ccsClient.connect(senderId, password); |
| |
| // Send a sample hello downstream message to a device. |
| String toRegId = "RegistrationIdOfTheTargetDevice"; |
| String messageId = ccsClient.nextMessageId(); |
| Map<String, String> payload = new HashMap<String, String>(); |
| payload.put("Hello", "World"); |
| payload.put("CCS", "Dummy Message"); |
| payload.put("EmbeddedMessageId", messageId); |
| String collapseKey = "sample"; |
| Long timeToLive = 10000L; |
| String message = createJsonMessage(toRegId, messageId, payload, |
| collapseKey, timeToLive, true); |
| |
| ccsClient.sendDownstreamMessage(message); |
| } |
| |
| /** |
| * XMPP Packet Extension for GCM Cloud Connection Server. |
| */ |
| private static final class GcmPacketExtension extends DefaultPacketExtension { |
| |
| private final String json; |
| |
| public GcmPacketExtension(String json) { |
| super(GCM_ELEMENT_NAME, GCM_NAMESPACE); |
| this.json = json; |
| } |
| |
| public String getJson() { |
| return json; |
| } |
| |
| @Override |
| public String toXML() { |
| return String.format("<%s xmlns=\"%s\">%s</%s>", |
| GCM_ELEMENT_NAME, GCM_NAMESPACE, |
| StringUtils.escapeForXML(json), GCM_ELEMENT_NAME); |
| } |
| |
| public Packet toPacket() { |
| Message message = new Message(); |
| message.addExtension(this); |
| return message; |
| } |
| } |
| |
| private static final class LoggingConnectionListener |
| implements ConnectionListener { |
| |
| @Override |
| public void connected(XMPPConnection xmppConnection) { |
| logger.info("Connected."); |
| } |
| |
| @Override |
| public void authenticated(XMPPConnection xmppConnection) { |
| logger.info("Authenticated."); |
| } |
| |
| @Override |
| public void reconnectionSuccessful() { |
| logger.info("Reconnecting.."); |
| } |
| |
| @Override |
| public void reconnectionFailed(Exception e) { |
| logger.log(Level.INFO, "Reconnection failed.. ", e); |
| } |
| |
| @Override |
| public void reconnectingIn(int seconds) { |
| logger.log(Level.INFO, "Reconnecting in %d secs", seconds); |
| } |
| |
| @Override |
| public void connectionClosedOnError(Exception e) { |
| logger.info("Connection closed on error."); |
| } |
| |
| @Override |
| public void connectionClosed() { |
| logger.info("Connection closed."); |
| } |
| } |
| }</pre> |
| |
| <h3 id="python">Python sample</h3> |
| |
| <p>Here is an example of a CCS app server written in Python. This sample echo |
| server sends an initial message, and for every upstream message received, it sends |
| a dummy response back to the application that sent the upstream message. This |
| example illustrates how to connect, send, and receive GCM messages using XMPP. It |
| shouldn't be used as-is on a production deployment.</p> |
| |
| <pre> |
| #!/usr/bin/python |
| import sys, json, xmpp, random, string |
| |
| SERVER = 'gcm.googleapis.com' |
| PORT = 5235 |
| USERNAME = "Your GCM Sender Id" |
| PASSWORD = "API Key" |
| REGISTRATION_ID = "Registration Id of the target device" |
| |
| unacked_messages_quota = 100 |
| send_queue = [] |
| |
| # Return a random alphanumerical id |
| def random_id(): |
| rid = '' |
| for x in range(8): rid += random.choice(string.ascii_letters + string.digits) |
| return rid |
| |
| def message_callback(session, message): |
| global unacked_messages_quota |
| gcm = message.getTags('gcm') |
| if gcm: |
| gcm_json = gcm[0].getData() |
| msg = json.loads(gcm_json) |
| if not msg.has_key('message_type'): |
| # Acknowledge the incoming message immediately. |
| send({'to': msg['from'], |
| 'message_type': 'ack', |
| 'message_id': msg['message_id']}) |
| # Queue a response back to the server. |
| if msg.has_key('from'): |
| # Send a dummy echo response back to the app that sent the upstream message. |
| send_queue.append({'to': msg['from'], |
| 'message_id': random_id(), |
| 'data': {'pong': 1}}) |
| elif msg['message_type'] == 'ack' or msg['message_type'] == 'nack': |
| unacked_messages_quota += 1 |
| |
| def send(json_dict): |
| template = ("<message><gcm xmlns='google:mobile:data'>{1}</gcm></message>") |
| client.send(xmpp.protocol.Message( |
| node=template.format(client.Bind.bound[0], json.dumps(json_dict)))) |
| |
| def flush_queued_messages(): |
| global unacked_messages_quota |
| while len(send_queue) and unacked_messages_quota > 0: |
| send(send_queue.pop(0)) |
| unacked_messages_quota -= 1 |
| |
| client = xmpp.Client('gcm.googleapis.com', debug=['socket']) |
| client.connect(server=(SERVER,PORT), secure=1, use_srv=False) |
| auth = client.auth(USERNAME, PASSWORD) |
| if not auth: |
| print 'Authentication failed!' |
| sys.exit(1) |
| |
| client.RegisterHandler('message', message_callback) |
| |
| send_queue.append({'to': REGISTRATION_ID, |
| 'message_id': 'reg_id', |
| 'data': {'message_destination': 'RegId', |
| 'message_id': random_id()}}) |
| |
| while True: |
| client.Process(1) |
| flush_queued_messages()</pre> |