一.基础知识
在前一部分,我们使用curl命令来代替了服务器端的实现,虽然在测试时使用curl命令是一个很是简单方便的模拟方式,但实际使用中我们需要把C2DM相关的服务器部分功能结合到已有的框架中,因此需要使用具体的代码来实现。
第三方服务器端部分的功能主要是通过C2DM服务器向客户端发送要推送的数据。
为了发送数据,第三方服务器需要向这个地址https://android.apis.google.com/c2dm/send发送一个POST请求,其中POST的内容包含:
registration_id:是客户端发送过来的registration_id值。必须包含。
collapse_key:一个任意的字符串,用来表示一组相似的消息。当Android设备由离线到上线时,之前使用相同collapse_key推送的消息,只有最后一条才会推送给Android设备。设置这个值用来避免Android设备上线时收到太多已经过时的消息。必须包含。
data.<key>:要推送的数据内容,以键值对的方式组织。当客户端程序接收时,就通过键值<key>来获取对应的内容。一条推送消息中包含的键值对数目没有限制,虽然整体的数据大小有限制。可选。
delay_while_idle:如果包含这项,则表明当Android设备idle时,C2DM服务不会立即把消息推送给设备而是等到设备重新变回active。可选。
Authorization: GoogleLogin auth=[AUTH_TOKEN]:HTTP头部要包含的信息,是为SenderID申请的C2DM(服务代码为ac2dm)服务权限,这个需要提前获取。必须包含。
因此第三方服务器就是构造这样的POST请求,然后向C2DM服务器发送。
在这部分中,我们就使用java代码的方式实现之前使用curl模拟的第三方服务器功能。
二.实例开发
创建一个Java工程,工程名为C2DMMessageServer,新建包名com.ichliebephone.server.c2dmmessage,并新建一个类C2DMMessageServer。
第三方服务器端的和C2DM相关的功能可以分为两个,第一个是获取注册使用C2DM功能的用户账号的ClientLogin权限Auth值;第二个是按格式给C2DM服务器发送要Push的数据。
我们先来看下获取Auth权限的方法实现:
[java] view plain copy print ?
-
- public static String getAuthToken(String url, String params) throws IOException{
- String auth = null;
-
- byte[] postData = params.getBytes();
-
- URL requestUrl = new URL(url);
- HttpURLConnection connection = (HttpURLConnection)requestUrl.openConnection();
- connection.setDoOutput(true);
- connection.setUseCaches(false);
- connection.setRequestMethod("POST");
- connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
- connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
-
- OutputStream out = connection.getOutputStream();
- out.write(postData);
- out.flush();
- out.close();
-
- BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String responseLine;
- StringBuilder responseDataBuidler = new StringBuilder();
- while((responseLine=reader.readLine())!=null){
- responseDataBuidler.append(responseLine);
- }
- int responseCode = connection.getResponseCode();
- System.out.println("auth responseCode = "+responseCode);
- if(responseCode == 200){
-
- int authStartIndex = responseDataBuidler.indexOf("Auth=");
- auth = responseDataBuidler.substring(authStartIndex+5);
-
- } else{
-
- System.out.println(responseDataBuidler);
- }
- return auth;
- }
//获取Auth权限值 public static String getAuthToken(String url, String params) throws IOException{ String auth = null; //要POST的数据 byte[] postData = params.getBytes(); //构造POST请求 URL requestUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection)requestUrl.openConnection(); connection.setDoOutput(true); connection.setUseCaches(false); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); connection.setRequestProperty("Content-Length", Integer.toString(postData.length)); //写入POST数据 OutputStream out = connection.getOutputStream(); out.write(postData); out.flush(); out.close(); //获取并处理请求返回的数据 BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String responseLine; StringBuilder responseDataBuidler = new StringBuilder(); while((responseLine=reader.readLine())!=null){ responseDataBuidler.append(responseLine); } int responseCode = connection.getResponseCode(); System.out.println("auth responseCode = "+responseCode); if(responseCode == 200){ //如果请求成功,提取Auth值 int authStartIndex = responseDataBuidler.indexOf("Auth="); auth = responseDataBuidler.substring(authStartIndex+5); //System.out.println(auth); } else{ //如果失败,打印出失败的结果 System.out.println(responseDataBuidler); } return auth; }
关于获取ClientLogin的说明参考谷歌的官方文档,方法的第一个参数url为请求的地址:https://www.google.com/accounts/ClientLogin,第二个参数params为POST时带的内容,包括accountType(申请权限的账户类型),Email(账户邮箱地址),Passwd(账户邮箱密码),service(申请权限的服务类型,这里申请C2DM服务,值为ac2dm),source(表示我们应用程序的一个简短字符串)。这里我传入的值为"accountType=HOSTED_OR_GOOGLE&[email protected]&Passwd=androidc2dmdemo&service=ac2dm&source=bupt-c2dmdemo-1.0";
在getAuthToken方法中首先新建HttpURLConnection连接,构造POST请求,设置好参数,写入POST数据,然后获取并处理请求返回的内容。根据ClientLogin的官方文档,成功返回的response形式为:
HTTP/1.0 200 OK
Server: GFE/1.3
Content-Type: text/plain
SID=DQAAAGgA...7Zg8CTN
LSID=DQAAAGsA...lk8BBbG
Auth=DQAAAGgA...dk3fA5N
没有成功请求返回的response形式为:
HTTP/1.0 403 Access Forbidden
Server: GFE/1.3
Content-Type: text/plain
Url=http://www.google.com/login/captcha
Error=CaptchaRequired
CaptchaToken=DQAAAGgA...dkI1LK9
CaptchaUrl=Captcha?ctoken=HiteT4b0Bk5Xg18_AcVoP6-yFkHPibe7O9EqxeiI7lUSN
因此这里我们根据返回的responseCode判断,如果是200则表明请求成功,从返回的数据中提取我们需要的Auth值。如果不是200则表明请求出错,这里只是简单的打印出出错的信息。实际使用中需要更加错误的信息做相应的处理,比如是服务器端没响应的话那就等待一段时间重新尝试,等等。
获取了Auth权限值,接了下来我们就可以给C2DM服务器发送我们想要其Push的数据了,实现向C2DM服务器发送数据的方法为:
[java] view plain copy print ?
-
- public static boolean sendPushMessage(String url, String registration_id, String collapse_key, String auth, Map<String, String> data) throws IOException{
- boolean flag = false;
-
- StringBuilder postDataBuidler = new StringBuilder();
- postDataBuidler.append("registration_id").append("=").append(registration_id);
- postDataBuidler.append("&").append("collapse_key").append("=").append(collapse_key);
- for(Object keyObject:data.keySet()){
- String key = (String)keyObject;
- if(key.startsWith("data.")){
- String value = data.get(key);
- postDataBuidler.append("&").append(key).append("=").append(value);
- }
-
- }
- byte[] postData = postDataBuidler.toString().getBytes();
-
- URL requestUrl = new URL(url);
- HttpURLConnection connection = (HttpURLConnection)requestUrl.openConnection();
- connection.setDoOutput(true);
- connection.setUseCaches(false);
- connection.setRequestMethod("POST");
- connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
- connection.setRequestProperty("Content-Length", Integer.toString(postData.length));
- connection.setRequestProperty("Authorization", "GoogleLogin auth="+auth);
-
- OutputStream out = connection.getOutputStream();
- out.write(postData);
- out.flush();
- out.close();
-
- int responseCode = connection.getResponseCode();
- System.out.println("c2dm responseCode = "+responseCode);
- if(responseCode == 200){
- BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String responseLine;
- StringBuilder responseDataBuidler = new StringBuilder();
- while((responseLine=reader.readLine())!=null){
- responseDataBuidler.append(responseLine);
- }
- if(responseDataBuidler.toString().startsWith("id=")){
- flag = true;
- }
- System.out.println(responseDataBuidler);
- } else if(responseCode == 503){
- System.out.println("The server is temporarily unavailable, please try later");
- } else if(responseCode == 401){
- System.out.println(" the ClientLogin AUTH_TOKEN used to validate the sender is invalid");
- }
- return flag;
- }
//向C2DM服务器发送需要Push的数据 public static boolean sendPushMessage(String url, String registration_id, String collapse_key, String auth, Map<String, String> data) throws IOException{ boolean flag = false; //构造POST的数据 StringBuilder postDataBuidler = new StringBuilder(); postDataBuidler.append("registration_id").append("=").append(registration_id); postDataBuidler.append("&").append("collapse_key").append("=").append(collapse_key); for(Object keyObject:data.keySet()){ String key = (String)keyObject; if(key.startsWith("data.")){ String value = data.get(key); postDataBuidler.append("&").append(key).append("=").append(value); } } byte[] postData = postDataBuidler.toString().getBytes(); //构造POST请求 URL requestUrl = new URL(url); HttpURLConnection connection = (HttpURLConnection)requestUrl.openConnection(); connection.setDoOutput(true); connection.setUseCaches(false); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"); connection.setRequestProperty("Content-Length", Integer.toString(postData.length)); connection.setRequestProperty("Authorization", "GoogleLogin auth="+auth); //写入POST数据 OutputStream out = connection.getOutputStream(); out.write(postData); out.flush(); out.close(); //获取并处理请求返回的数据 int responseCode = connection.getResponseCode(); System.out.println("c2dm responseCode = "+responseCode); if(responseCode == 200){ BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); String responseLine; StringBuilder responseDataBuidler = new StringBuilder(); while((responseLine=reader.readLine())!=null){ responseDataBuidler.append(responseLine); } if(responseDataBuidler.toString().startsWith("id=")){ flag = true; } System.out.println(responseDataBuidler); } else if(responseCode == 503){ System.out.println("The server is temporarily unavailable, please try later"); } else if(responseCode == 401){ System.out.println(" the ClientLogin AUTH_TOKEN used to validate the sender is invalid"); } return flag; }
关于第三方服务器向C2DM发送数据的说明可以查看C2DM的官方文档,其中发送的URL为https://android.apis.google.com/c2dm/send,因为使用https的方式会提示出错,因此使用http的方式,URL为http://android.apis.google.com/c2dm/send, 使用POST方式提交要包含的内容有registration_id(Android应用程序注册获得的id),collapse_key
(用任意一个字符传表示一组类似的消息,如果Android设备离线了,那等其上线后这组消息中只有最后一条消息才会被Push给Android设备,这样重新上线后的Android设备不会收到太多之前过时的消息),ClientLogin
auth(上一步获取的使用C2DM功能的账户权限),data(这是一个键值对形式表示的数据,形式比如为data.msg=ichliebejiajia,其中键值需要和Android设备上的程序统一化,以便其可以获取解析,这是真正需要Push给Android设备的数据)。
在sendPushMessage方法中,首先构造POST的数据,然后创建HttpURLConnection连接,以POST的方式发送请求,最后接收并处理请求返回的数据。根据官方文档的说明,当返回的responseCode为200,并且返回的数据以id=开头时,才算数据发送成功。
图1 C2DM服务器返回的结果
当responseCode为200,但返回的数据不是以id=开头时,则表明向C2DM发送数据错误,返回的数据包含错误代码,Error=[错误代码]。错误代码可能为:
QuotaExceeded:超过发送的配额了,等待一会后重新尝试。在C2DM服务注册页面,我们可以看到有两项是填写和配额相关的内容:一项是估计每天发送的总条数(Estimated total number of messages per day?);另一项是每秒钟发送的峰值条数(Estimated peak queries per second (QPS))。
DeviceQuotaExceeded:给某一个特定的设备发送了太多的信息,等待一会后重新尝试。
InvalidRegistration:缺少或者是错误的registration_id。如果是这个错误,那服务器应该停止给这个registration_id对应的设备发送信息。
NotRegistered:registration_id值不再有效,比如客户端程序的使用者关闭了推送通知接收功能或者卸载了客户端程序时。此时服务器也应该停止给这个registration_id对应的设备发送信息。
MessageTooBig:发送的信息太大了,因为信息的长度限制为不大于1024字节。需要减少信息的长度。
MissingCollapseKey:POST的内容中缺少collapse_key值。
当responseCode是503时,表示C2DM服务器没有响应,此时可以等待一段时间后重新尝试。
当responseCode为401时,表示ClientLogin的Auth值无效。
除了发送成功外,其他情况我们打印出对应的错误说明。在实际使用时,我们需要更加具体的错误情况来做具体的处理,比如当提示超过配额(如果为峰值配额)时,等待一段时间重新尝试;当服务器没有响应时,等待一个回退时间再重新尝试,等等。
实现了获取ClientLogin的Auth值和向C2DM服务器发送需要Push的数据两个功能后,接下来我们就可以使用代码的方式来完成下之前用curl命令模拟的第三方服务器功能。C2DMMessageServer的main方法为:
[java] view plain copy print ?
- public static String ClientLoginURL = "https://www.google.com/accounts/ClientLogin";
- public static String AuthTokenParams = "accountType=HOSTED_OR_GOOGLE&[email protected]&Passwd=androidc2dmdemo&service=ac2dm&source=bupt-c2dmdemo-1.0";
- public static String C2DMServerURL = "http://android.apis.google.com/c2dm/send";
- public static String Registration_ID = "APA91bGUBoSvt3G5Ny9t0IGLmIKAKYX6G6VHwSQHh3tP2fqcaQ0N4GPdKh5B3RDUHFCFF06YwT8ifOP_cOy5BAWyCLHL8d8NpuIW9AqXt9h2JSBVF2MitZA";
-
-
-
- public static void main(String[] args) {
-
- System.out.println("The C2DMMessageServer is started...");
- try {
- String authToken = getAuthToken(ClientLoginURL, AuthTokenParams);
- if(authToken==null){
- System.out.println("Can not get the Auth");
- return;
- }
- BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
- while(true){
- System.out.println("Please input the message to push:(Exit with q)");
- String message = reader.readLine();
- if(message.equalsIgnoreCase("q")){
- break;
- }
- Map<String, String> data = new HashMap<String, String>();
- data.put("data.msg", message);
- if(sendPushMessage(C2DMServerURL, Registration_ID, "1", authToken, data)){
- System.out.println("Send Successfully");
- }else{
- System.out.println("Send Failed");
- }
-
- }
- } catch (IOException e) {
-
- e.printStackTrace();
- }
-
- <div style="text-align: left;">
- </div><div style="text-align: left;"> }</div>
public static String ClientLoginURL = "https://www.google.com/accounts/ClientLogin"; public static String AuthTokenParams = "accountType=HOSTED_OR_GOOGLE&
[email protected]&Passwd=androidc2dmdemo&service=ac2dm&source=bupt-c2dmdemo-1.0"; public static String C2DMServerURL = "http://android.apis.google.com/c2dm/send"; public static String Registration_ID = "APA91bGUBoSvt3G5Ny9t0IGLmIKAKYX6G6VHwSQHh3tP2fqcaQ0N4GPdKh5B3RDUHFCFF06YwT8ifOP_cOy5BAWyCLHL8d8NpuIW9AqXt9h2JSBVF2MitZA"; /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("The C2DMMessageServer is started..."); try { String authToken = getAuthToken(ClientLoginURL, AuthTokenParams); if(authToken==null){ System.out.println("Can not get the Auth"); return; } BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); while(true){ System.out.println("Please input the message to push:(Exit with q)"); String message = reader.readLine(); if(message.equalsIgnoreCase("q")){ break; } Map<String, String> data = new HashMap<String, String>(); data.put("data.msg", message);//这里和客户端代码统一,使用的key为msg,并且只push一组数据 if(sendPushMessage(C2DMServerURL, Registration_ID, "1", authToken, data)){ System.out.println("Send Successfully"); }else{ System.out.println("Send Failed"); } } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } <div style="text-align: left;"> </div><div style="text-align: left;"> }</div>
在开始处我们定义了几个相关的静态变量,其中在实际使用的时候AuthTokenParams中的Email和Passwd需要换成你自己申请C2DM时的邮箱和密码,Registration_ID也要换成你的Android设备向C2DM注册时返回的值。
在main方法里,首先获得Auth值,然后提示从终端中输入要发送给C2DM服务器Push的信息,最后调用sendPushMessage方法进行实现。
打开之前我们使用过的Android模拟器,然后运行C2DMMessageServer程序,运行后首先会去获取ClientLogin的Auth值,获取成功的话会显示200的返回码,并且提示可以输入想要Push的信息了:
图2 输入要Push的信息
然后我们可以试着输入我们想要Push的数据,比如依次输入两个数据:
图3 输入了两个要Push的信息
我们可以在DDMS中看到分别收到了这两个Push的数据:
图4 DDMS中显示接收到的Push信息
并且Android模拟器中的通知栏中也会依次显示对应的通知:
图5 模拟器通知栏总显示接收到的Push信息
这样我们就单独实现了之前用curl两次命令实现的Server端的两个功能,现在只要运行Server端程序,输入想要输入的信息,然后Android设备上就可以收到相应的Push信息了。
三.总结
在上面这部分介绍中,我们实现了第三方服务器端向C2DM服务器发送要推送的数据的相关功能。主要是实现了获取ClientLogin的Auth值和构造向C2DM发送的HTTP POST请求,不过在实际使用中,还要增加接收客户端程序发送过来的registration_id值,并且对registration_id值和ClientLogin的Auth值进行保存。同时可能还要从其他地方获取并处理需要进行推送的数据等。不过熟悉了和C2DM相关的最主要的部分之后,如果你本来就从事服务器端开发的话,应该是比较好实现C2DM推送功能并且整合到你的系统中。
通过这三部分的学习,我们对Android的C2DM特性有了一定的了解,在需要使用推送功能时可以考虑C2DM的方式。不过目前C2DM还处于实验室状态,在实际的大规模Push使用中可能还有一些限制,比如单次Push消息的大小限制在1024字节;对一个sender能给Push的总条数有限制;并且一个sender对某一个特定的Android设备的Push条数也有限制等。
以上我们学习了C2DM的总体功能及实现,具体的细节还需要我们在实际使用中进一步学习。
文章对应的完整代码例子下载地址:
http://download.csdn.net/source/3471530