一.基礎知識
在前一部分,我們使用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權限的方法實現:
//獲取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服務器發送數據的方法爲://向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設備的數據)。
圖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方法爲:
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();
}
在開始處我們定義了幾個相關的靜態變量,其中在實際使用的時候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的總體功能及實現,具體的細節還需要我們在實際使用中進一步學習。
文章對應的完整代碼例子下載地址: