[C#] 利用ASP.net和Windows Service實作Android手機的訊息推播(2012/6月底GCM版)
2012.12.3追記:
噗XD
發文這麼久,怎麼忽然變成首頁精華XDD
前言
原本好不容易寫好C2DM版,在上個月底Google Android手機的訊息推播服務竟然改新版
網路上範例缺少的情況下,只好自己動手寫啦XD
以下站在Server端角度做個說明
開發步驟
先準備一個Google帳戶,然後到此網址點擊Google APIs Console page(如果是第一次執行的人,可能需要新增一個專案來管理這些APIs,照著系統提示做就行了)
http://developer.android.com/guide/google/gcm/gs.html
左方Services>找到Google Cloud Messaging for Android,開啟它
接著左方API Access,下方的Create new Server key,產生一組Server端要用的API key
訊息推播流程圖:
GCM架構和舊版C2DM很像
1、2步驟是前端App的事,交給前端開發人員煩惱就好
而Google GCM為Google那邊的Server,開發時期不會實際碰觸到,所以不用理會GCM的詳細實作及架設
Server端開發人員只要知道該送給GCM哪些參數就好(詳見程式碼實作↓)
步驟3. 前端App將Registration ID(也有人稱token,因為iOS平台那邊叫token,不過意思一樣都是做為手機的識別值)
送給AP Server的時機不一定,看App開發人員的設計
AP Server這邊就架一個ASP.net網站,掛上Web Service或泛型處理常式來接Registration ID
並到DB檢查,如果沒有此Registration ID的話,就儲存進DB
此Web Service也再做一個「從DB移除Registration ID」的函數供前端App呼叫(不然該RegistationID是留在DB中有效的ID,儘管前端使用者取消訊息推播服務,Windows Service程式仍舊會推播出去)
步驟4. 因為Server端要自動訊息推播
除了可用Windows Service專案外,也可考慮使用Console專案搭配Windows作業系統的排程功能達到定期檢查有無新資料的目的
步驟5. GCM把訊息送到手機上,這部份是Google他家的事,可以不用理會
限制方面,我目前只看到:
1. AP Server送給GCM的訊息有限制總長度,官方文件沒有明講多少(GCM Architectural Overview Android Developers),所以Windows Service只要送出簡單的訊息就好,並不是把DB裡全部新的資料送出去。
※Server端 .Net開發人員簡單講,只要寫兩支程式一個WebService,一個Windows Service就行了
代碼實作
步驟3:WebService部份我使用泛型處理常式接收App送過來的RegistrationID並儲存至DB
※以下寫Log的物件我是用NLog,教學這邊有:介紹好用函式庫:NLog - Advanced .NET Logging by 保哥
<%@ WebHandler Language="C#" Class="SaveAndroidRegisID" %>
using System;
using System.Web;
using System.Data;
using System.Data.SqlClient;
using SystemDAO;//以下用的SqlHelper來自:http://www.cnblogs.com/sufei/archive/2010/01/14/1648026.html
public class SaveAndroidRegisID : IHttpHandler {
NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
//App傳送的RegistrationID
string RegistrationID = context.Request["RegistrationID"];
string Del = context.Request["Del"];//是否刪除RegistrationID
SqlParameter[] param = new SqlParameter[] { new SqlParameter() { ParameterName = "@RegistrationID", SqlDbType = SqlDbType.VarChar, Value = RegistrationID } };
string json = string.Empty;//輸出結果的json字串
bool validate = false;
if (!string.IsNullOrEmpty(RegistrationID))//防呆
{
try
{
if (!string.IsNullOrEmpty(Del) && "true".Equals(Del))
{//從DB把RegistrationID刪除
SqlHelper.ExecteNonQuery(CommandType.Text, "Delete From tb_MyRegisID Where RegistrationID =@RegistrationID", param);
}
else
{
int count = Convert.ToInt32(SqlHelper.ExecuteScalar(CommandType.Text, "Select count(*) From tb_MyRegisID Where RegistrationID =@RegistrationID",param));
if (count==0)
{//DB無此RegistrationID,//新增RegistrationID到DB
SqlHelper.ExecteNonQuery(CommandType.Text, "Insert into tb_MyRegisID (RegistrationID) values (@RegistrationID)", param);
}
}
validate = true;//執行成功
}
catch (Exception ex)
{
logger.Error(ex.ToString());//寫Log
validate = false;//執行失敗
}
}
else
{
validate = false;
}
if (validate)
{
json = @"{""Success"":true}";
}
else
{
json = @"{""Success"":false}";
}
context.Response.Write(json);//輸出訊息
}
public bool IsReusable {
get {
return false;
}
}
}
步驟4:新增一個Windows Service專案,目的是為了定時檢查有無新的資料,並推播訊息至GCM,再由GCM發送訊息
※以下的JsonConvert為Json.net的dll,可以到這下載加入參考:http://json.codeplex.com/
Windows Service代碼實現:
using System;
using System.Collections.Generic;
using System.ComponentModel
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Timers;
using SystemDAO;//SqlHelper
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Net;
using System.Configuration;
using System.Xml.Linq;
namespace ws_ServerPushNotification
{
public partial class Service_ServerPusth : ServiceBase
{
Timer timer = new Timer();
public Service_ServerPusth()
{
InitializeComponent();
#region 設定timer
timer.Enabled = true;
timer.Interval = 5000;//輪詢間隔5秒
timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
#endregion
dtTemp.Columns.Add("title", typeof(string));
dtTemp.Columns.Add("datetime", typeof(string));
}
protected override void OnStart(string[] args)
{
timer.Start();//開始輪詢
}
//此事件會反覆執行
private void timer_Elapsed(object sender, ElapsedEventArgs e)
{
timer.Stop();//以下要長時間作業,所以timer先停止(沒停止的話,會變成非同步程式設計)
if (this.isNotify())//如果有要通知的話
{
//從DB取得Android的RegistrationID的DataTable
DataTable dtRegistrationID = SqlHelper.GetTable(CommandType.Text, "Select RegistrationID from tb_MyRegisID Order by RegistrationID ASC", null)[0];
string API_Key = ConfigurationManager.AppSettings["你的API_Key"];
string message = "要推播的訊息";
string result = HttpPostToGCM(dtRegistrationID, API_Key, message);
}
timer.Start();//長時間作業結束,啟動timer
}
DataTable dtTemp = new DataTable();//要暫存資料的全域變數
string RssUrl = ConfigurationManager.AppSettings["RssUrl"];//資料來源Rss的超連結
private bool isNotify()
{
bool is_notify = false;//預設不通知
try
{
//Linq to Rss
XDocument xDoc = XDocument.Load(RssUrl);
IEnumerable<XElement> items = xDoc.Descendants("item");//抓出所有的目標資料
if (items.Any())//至少有一筆
{
XElement ele = items.FirstOrDefault();//取得第一筆item
string title = ele.Element("title").Value;
string datetime = ele.Element("datetime").Value;
#region 第一次如果全域變數沒有資料的話,就先存進全域變數DataTable
if (dtTemp.Rows.Count == 0)
{
dtTemp.Rows.Add(title, datetime);//加第一筆至DataTable
}
#endregion
#region 和全域變數比對(第一次執行不會進入此if)
if (title != dtTemp.Rows[0]["title"].ToString() ||
datetime != dtTemp.Rows[0]["datetime"].ToString())
{//任一資料不一樣
dtTemp.Clear();//清除舊數據
dtTemp.Rows.Add(title, datetime);//加第一筆至DataTable,下一次就和此數據比對
is_notify = true;//要通知
}
#endregion
}
}
catch (Exception ex)
{
EventLog.WriteEntry("isNotify()發生例外:" + ex.ToString());
}
return is_notify;
}//End isNotify();
/// <summary>
/// 對GCM Server發出Http post
/// </summary>
/// <param name="傳一個DataTable"></param>
/// <param name="API_Key"></param>
/// <param name="message"></param>
/// <returns></returns>
public string HttpPostToGCM(DataTable regIDTable,string API_Key,string message)
{
StringBuilder returnStr = new StringBuilder();//要回傳的字串
if (regIDTable!=null && regIDTable.Rows.Count > 0)//防呆
{
foreach (DataRow row in regIDTable.Rows)
{//一筆一筆發送
//準備對GCM Server發出Http post
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("https://android.googleapis.com/gcm/send");
request.Method = "POST";
request.ContentType = "application/json;charset=utf-8;";
request.Headers.Add(string.Format("Authorization: key={0}", API_Key));
//以下寫法無法知道哪個RegistrationID無效
//var postData =
// new
// {
// data = new
// {
// message = message //message這個tag要讓前端開發人員知道,前端App才知道要顯示什麼訊息
// },
// registration_ids = from r in regIDTable.Select()
// select r.Field<string>("RegistrationID")
// };
string RegistrationID= row["RegistrationID"].ToString();
var postData =
new
{
data = new
{
message = message //message這個tag要讓前端開發人員知道
},
registration_ids = new string[] { RegistrationID }
};
string p = JsonConvert.SerializeObject(postData);//將Linq to json轉為字串
byte[] byteArray = Encoding.UTF8.GetBytes(p);//要發送的字串轉為byte[]
request.ContentLength = byteArray.Length;
Stream dataStream = request.GetRequestStream();
dataStream.Write(byteArray, 0, byteArray.Length);
dataStream.Close();
//發出Request
WebResponse response = request.GetResponse();
Stream responseStream = response.GetResponseStream();
StreamReader reader = new StreamReader(responseStream);
string responseStr = reader.ReadToEnd();
reader.Close();
responseStream.Close();
response.Close();
JObject obj = (JObject)JsonConvert.DeserializeObject(responseStr);
if (Convert.ToInt32(obj["failure"].ToString()) > 0)
{//有失敗情況就寫Log
EventLog.WriteEntry("發送訊息給"+RegistrationID+"失敗:" + responseStr);
obj = (JObject)obj["results"][0];
if (obj["error"].ToString() == "InvalidRegistration" || obj["error"].ToString()=="NotRegistered")
{ //無效的RegistrationID
//從DB移除
SqlParameter[] param = new SqlParameter[] { new SqlParameter() { ParameterName = "@RegistrationID", SqlDbType = SqlDbType.VarChar, Value = RegistrationID } };
SqlHelper.ExecteNonQuery(CommandType.Text, "Delete from tb_MyRegisID Where RegistrationID=@RegistrationID",param);
}
}
returnStr.Append(responseStr+"\n");
}//End foreach
}//End if
return returnStr.ToString();
}
protected override void OnStop()
{
}
}
}
↑撰寫完成後,Windows Service專案的安裝專案建立可以參考:如何建立 Windows 服務應用程式的安裝專案在 Visual C# 中、[技術] 安裝Windows服務
※小提醒:為了讓Windows Service可以在32位元和64位元電腦上跑,記得Windows Service專案右鍵>屬性>建置>平台目標最好選Any CPU
※安裝好Windows Service後,最好再從系統管理工具>服務 確認服務要被啟動
執行結果:
(網路連線中才可收得到通知)
結語
以上介紹了Server端如何推播訊息給Android前端,至於使用者收到通知後
要如何呈現資料,就要再另外設計
我自己實務上為了避免推播的訊息太大,所以使用者點選了通知後,前端App會發一次Request到Server端這邊再抓資料呈現
2012.8.16 追記
CodeProjcet已經有人釋出Android GCM for .net原始碼
http://www.codeproject.com/Tips/434338/Android-GCM-Push-Notification
和本文不同的是在送給GCM Server的字串是用QueryString串接
依官方文件說明,要在同一次的Request送給多台裝置訊息的話,要用Json字串(也就是本篇的Sample Code)
不過也是可以用QueryString送出,跑迴圈多發幾次Request就可以達到發送多台裝置目的
GCM訊息推播其實還有很多專有名詞未說明到,其它可以參考以下官網說明
其他可參考的文章:
Google Cloud Messaging for Android Android Developers by Google官方文件
Redth-PushSharp · GitHub 此老外已封裝好一堆功能,教學在這:How to Configure & Send GCM Google Cloud Messaging Push Notifications using PushSharp
android - GCM Push Notification with Asp.Net - Stack Overflow