e89c6ebece
Fixes for: b/15348323 b/15349955 b/15330953 Change-Id: Ie256a76ea83cf16256cded858531dd78b8da0558
968 lines
36 KiB
Plaintext
968 lines
36 KiB
Plaintext
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 return 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="{@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>
|
|
<li><a href="https://services.google.com/fb/forms/gcm/" class="external-link"
|
|
target="_android">CCS and User Notifications Signup Form</a></li>
|
|
</ol>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<p>The 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>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
|
|
<a href="server.html#params">Implementing GCM Server</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-staging.googleapis.com:5236} (note the different port). Testing on
|
|
staging (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.</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
|
|
<a href="server.html#params">Implementing GCM Server</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>Quota exceeded:</p>
|
|
|
|
<pre><message>
|
|
<gcm xmlns="google:mobile:data">
|
|
{
|
|
"message_type":"nack",
|
|
"message_id":"msgId1",
|
|
"from":"APA91bHFOtaQGSwupt5l1og",
|
|
"error":"QUOTA_EXCEEDED",
|
|
"error_description":"Short-term downstream quota exceeded for this registration id"
|
|
}
|
|
</gcm>
|
|
</message>
|
|
</pre>
|
|
|
|
|
|
<p>The following table lists 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>
|
|
|
|
<p class="table-caption" id="table1">
|
|
<strong>Table 1.</strong> NACK error codes.</p>
|
|
|
|
<table border="1">
|
|
<tr>
|
|
<th>Error Code</th>
|
|
<th>Description</th>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code BAD_ACK}</td>
|
|
<td>The ACK message is improperly formed.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code BAD_REGISTRATION}</td>
|
|
<td>The device has a registration ID, but it's invalid or expired.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code CONNECTION_DRAINING}</td>
|
|
<td>The message couldn't be processed because the connection is draining. The
|
|
message should be immediately retried over another connection.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code DEVICE_UNREGISTERED}</td>
|
|
<td>The device is not registered.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code INTERNAL_SERVER_ERROR}</td>
|
|
<td>The server encountered an error while trying to process the request.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code INVALID_JSON}</td>
|
|
<td>The JSON message payload is not valid.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code QUOTA_EXCEEDED}</td>
|
|
<td>The rate of messages to a particular registration ID (in other words, to a
|
|
sender/device pair) is too high. If you want to retry the message, try using a slower
|
|
rate.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>{@code SERVICE_UNAVAILABLE}</td>
|
|
<td>CCS is not currently able to process the message. The
|
|
message should be retried over the same connection using exponential backoff
|
|
with an initial delay of 1 second.</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<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 return receipts</h3>
|
|
|
|
<p>You can use upstream messaging to get receipt notifications, confirming
|
|
that a given message was sent to a device. Your 3rd-party app server receives the receipt
|
|
notification from CCS once the message has been sent to the device.</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 return receipt. 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 a receipt notification message that CCS sends back to your 3rd-party
|
|
app server:</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 message was delivered. 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>dr:</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 and 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.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.util.StringUtils;
|
|
import org.json.simple.JSONValue;
|
|
import org.json.simple.parser.ParseException;
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
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.
|
|
*
|
|
* <p>For illustration purposes only.
|
|
*/
|
|
public class SmackCcsClient {
|
|
|
|
Logger logger = Logger.getLogger("SmackCcsClient");
|
|
|
|
public static final String GCM_SERVER = "gcm.googleapis.com";
|
|
public static final int GCM_PORT = 5235;
|
|
|
|
public static final String GCM_ELEMENT_NAME = "gcm";
|
|
public static final String GCM_NAMESPACE = "google:mobile:data";
|
|
|
|
static Random random = new Random();
|
|
XMPPConnection connection;
|
|
ConnectionConfiguration config;
|
|
|
|
/**
|
|
* XMPP Packet Extension for GCM Cloud Connection Server.
|
|
*/
|
|
class GcmPacketExtension extends DefaultPacketExtension {
|
|
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, json, GCM_ELEMENT_NAME);
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
public Packet toPacket() {
|
|
return new Message() {
|
|
// Must override toXML() because it includes a <body>
|
|
@Override
|
|
public String toXML() {
|
|
|
|
StringBuilder buf = new StringBuilder();
|
|
buf.append("<message");
|
|
if (getXmlns() != null) {
|
|
buf.append(" xmlns=\"").append(getXmlns()).append("\"");
|
|
}
|
|
if (getLanguage() != null) {
|
|
buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
|
|
}
|
|
if (getPacketID() != null) {
|
|
buf.append(" id=\"").append(getPacketID()).append("\"");
|
|
}
|
|
if (getTo() != null) {
|
|
buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
|
|
}
|
|
if (getFrom() != null) {
|
|
buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
|
|
}
|
|
buf.append(">");
|
|
buf.append(GcmPacketExtension.this.toXML());
|
|
buf.append("</message>");
|
|
return buf.toString();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
public SmackCcsClient() {
|
|
// Add GcmPacketExtension
|
|
ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME,
|
|
GCM_NAMESPACE, new PacketExtensionProvider() {
|
|
|
|
@Override
|
|
public PacketExtension parseExtension(XmlPullParser parser)
|
|
throws Exception {
|
|
String json = parser.nextText();
|
|
GcmPacketExtension packet = new GcmPacketExtension(json);
|
|
return packet;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 getRandomMessageId() {
|
|
return "m-" + Long.toString(random.nextLong());
|
|
}
|
|
|
|
/**
|
|
* Sends a downstream GCM message.
|
|
*/
|
|
public void send(String jsonRequest) {
|
|
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 process an upstream message.
|
|
*/
|
|
public void handleIncomingDataMessage(Map<String, Object> jsonObject) {
|
|
String from = jsonObject.get("from").toString();
|
|
|
|
// PackageName of the application that sent this message.
|
|
String category = jsonObject.get("category").toString();
|
|
|
|
// Use the packageName as the collapseKey in the echo packet
|
|
String collapseKey = "echo:CollapseKey";
|
|
@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, getRandomMessageId(), payload, collapseKey, null, false);
|
|
send(echo);
|
|
}
|
|
|
|
/**
|
|
* Handles an ACK.
|
|
*
|
|
* <p>By default, it only logs a {@code INFO} message, but subclasses could override it to
|
|
* properly handle ACKS.
|
|
*/
|
|
public void handleAckReceipt(Map<String, Object> jsonObject) {
|
|
String messageId = jsonObject.get("message_id").toString();
|
|
String from = jsonObject.get("from").toString();
|
|
logger.log(Level.INFO, "handleAckReceipt() from: " + from + ", messageId: " + messageId);
|
|
}
|
|
|
|
/**
|
|
* Handles a NACK.
|
|
*
|
|
* <p>By default, it only logs a {@code INFO} message, but subclasses could override it to
|
|
* properly handle NACKS.
|
|
*/
|
|
public void handleNackReceipt(Map<String, Object> jsonObject) {
|
|
String messageId = jsonObject.get("message_id").toString();
|
|
String from = jsonObject.get("from").toString();
|
|
logger.log(Level.INFO, "handleNackReceipt() from: " + from + ", messageId: " + messageId);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public 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 username GCM_SENDER_ID@gcm.googleapis.com
|
|
* @param password API Key
|
|
* @throws XMPPException
|
|
*/
|
|
public void connect(String username, String password) throws XMPPException {
|
|
config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
|
|
config.setSecurityMode(SecurityMode.enabled);
|
|
config.setReconnectionAllowed(true);
|
|
config.setRosterLoadedAtLogin(false);
|
|
config.setSendPresence(false);
|
|
config.setSocketFactory(SSLSocketFactory.getDefault());
|
|
|
|
// NOTE: Set to true to launch a window with information about packets sent and received
|
|
config.setDebuggerEnabled(true);
|
|
|
|
// -Dsmack.debugEnabled=true
|
|
XMPPConnection.DEBUG_ENABLED = true;
|
|
|
|
connection = new XMPPConnection(config);
|
|
connection.connect();
|
|
|
|
connection.addConnectionListener(new ConnectionListener() {
|
|
|
|
@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.log(Level.INFO, "Connection closed on error.");
|
|
}
|
|
|
|
@Override
|
|
public void connectionClosed() {
|
|
logger.info("Connection closed.");
|
|
}
|
|
});
|
|
|
|
// 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
|
|
handleIncomingDataMessage(jsonObject);
|
|
|
|
// Send ACK to CCS
|
|
String messageId = jsonObject.get("message_id").toString();
|
|
String from = jsonObject.get("from").toString();
|
|
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 {
|
|
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, "Couldn't send echo.", 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(username, password);
|
|
}
|
|
|
|
public static void main(String [] args) {
|
|
final String userName = "Your GCM Sender Id" + "@gcm.googleapis.com";
|
|
final String password = "API Key";
|
|
|
|
SmackCcsClient ccsClient = new SmackCcsClient();
|
|
|
|
try {
|
|
ccsClient.connect(userName, password);
|
|
} catch (XMPPException e) {
|
|
e.printStackTrace();
|
|
}
|
|
|
|
// Send a sample hello downstream message to a device.
|
|
String toRegId = "RegistrationIdOfTheTargetDevice";
|
|
String messageId = ccsClient.getRandomMessageId();
|
|
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;
|
|
Boolean delayWhileIdle = true;
|
|
ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey,
|
|
timeToLive, delayWhileIdle));
|
|
}
|
|
}</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>
|