855 lines
30 KiB
Plaintext
855 lines
30 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="#usage">How to Use CCS</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> </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 class="note"><strong>Note:</strong> To try out this feature, sign up using
|
||
<a href="https://services.google.com/fb/forms/gcm/">this form</a>.</p>
|
||
|
||
<p>The GCM Cloud Connection Server (CCS) is a connection server based on XMPP.
|
||
CCS allows 3rd-party app servers (which you're
|
||
responsible for implementing) to communicate
|
||
with Android devices by establishing a persistent TCP connection with Google
|
||
servers using the XMPP protocol. This communication is asynchronous and bidirectional.</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 the server send messages
|
||
to the device, but the device can send messages back to the server.</li>
|
||
<li>You 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="usage">How to Use CCS</h2>
|
||
|
||
<p>GCM Cloud Connection Server (CCS) is an XMPP endpoint, running on
|
||
{@code http://gcm.googleapis.com} port 5235.</p>
|
||
|
||
<p>CCS requires a Transport Layer Security (TLS) connection. That means the XMPP
|
||
client must initiate a TLS connection.
|
||
For example in Java, you would call {@code setSocketFactory(SSLSocketFactory)}.</p>
|
||
|
||
<p>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>.</p>
|
||
|
||
<p> You can use most XMPP libraries to interact with CCS.</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>CCS uses normal XMPP <code><message></code> stanzas. The body of the message must be:
|
||
</p>
|
||
<pre>
|
||
<gcm xmlns:google:mobile:data>
|
||
<em>JSON payload</em>
|
||
</gcm>
|
||
</pre>
|
||
|
||
<p>The JSON payload for server-to-device is similar to what the GCM http endpoint
|
||
uses, 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, but
|
||
always present.</li>
|
||
|
||
<li>For ACK/NACK messages that are special control messages, you also need to
|
||
include a {@code message_type} field in the JSON message. The value can be either
|
||
'ack' or 'nack'. For example:
|
||
|
||
<pre>message_type = ('ack');</pre>
|
||
</li>
|
||
</ul>
|
||
<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
|
||
}
|
||
</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>
|
||
<data:gcm xmlns:data="google:mobile:data">
|
||
{
|
||
"error":"BAD_REGISTRATION", // error code
|
||
"message_id":"msgId1",
|
||
"from":"PA91bHFOtaQGSwupt5l1og",
|
||
"message_type":"nack"
|
||
}
|
||
</data:gcm>
|
||
</message></pre>
|
||
|
||
<p>Invalid "time to live":</p>
|
||
|
||
<pre><message>
|
||
<data:gcm xmlns:data="google:mobile:data">
|
||
{
|
||
"error":"InvalidJson : INVALID_TTL : Invalid value (-1) for \"time_to_live\": must be between 0 and \"2419200\"\n",
|
||
"message_id":"msgId1",
|
||
"from":"APA91bHFOtaQGSwupt5l1og",
|
||
"message_type":"nack"
|
||
}
|
||
</data:gcm>
|
||
</message></pre>
|
||
|
||
<p>JSON type error:</p>
|
||
|
||
<pre><message>
|
||
<data:gcm xmlns:data="google:mobile:data">
|
||
{
|
||
"error":"InvalidJson : JSON_TYPE_ERROR : Field \"delay_while_idle\" must be a JSON java.lang.Boolean: not-boolean-user-supplied-value\n",
|
||
"message_id":"msgId1",
|
||
"from":"APA91bHFOtaQGSwupt5l1og",
|
||
"message_type":"nack"
|
||
}
|
||
</data:gcm>
|
||
</message></pre>
|
||
|
||
|
||
<p>The following table lists some of the more common NACK error codes.</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_REGISTRATION}</td>
|
||
<td>The device has a registration ID, but it's invalid.</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 SERVICE_UNAVAILABLE}</td>
|
||
<td>The CCS connection server is temporarily unavailable, try again later
|
||
(using exponential backoff, etc.).</td>
|
||
</tr>
|
||
<tr>
|
||
<td>{@code BAD_ACK}</td>
|
||
<td>The ACK message is improperly formed.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>{@code AUTHENTICATION_FAILED}</td>
|
||
<td>This is a 401 error indicating that there was an error authenticating the sender account.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>{@code INVALID_TTL}</td>
|
||
<td>There was an error in the supplied "time to live" value.</td>
|
||
</tr>
|
||
<tr>
|
||
<td>{@code JSON_TYPE_ERROR}</td>
|
||
<td>There was an error in the supplied JSON data type.</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>
|
||
|
||
|
||
<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 4 weeks.
|
||
int ttl = [0 seconds, 4 weeks]
|
||
|
||
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>
|
||
|
||
<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 1000, 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 1000, 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 = 1000
|
||
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>
|