994 lines
37 KiB
Plaintext
994 lines
37 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>
|
|
</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.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>
|