[C#] 利用ASP.net和Windows Service實作Android手機的訊息推播(2012/6月底GCM版)

[C#] 利用ASP.net和Windows Service實作Android手機的訊息推播(2012/6月底GCM版)

2012.12.3追記:
噗XD
發文這麼久,怎麼忽然變成首頁精華XDD

前言

原本好不容易寫好C2DM版,在上個月底Google Android手機的訊息推播服務竟然改新版

image

網路上範例缺少的情況下,只好自己動手寫啦XD

以下站在Server端角度做個說明

開發步驟

先準備一個Google帳戶,然後到此網址點擊Google APIs Console page(如果是第一次執行的人,可能需要新增一個專案來管理這些APIs,照著系統提示做就行了)

http://developer.android.com/guide/google/gcm/gs.html

image

左方Services>找到Google Cloud Messaging for Android,開啟它

image

接著左方API Access,下方的Create new Server key,產生一組Server端要用的API key

image

 

 

 

 

 

訊息推播流程圖:

image

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

image_thumb5

※安裝好Windows Service後,最好再從系統管理工具>服務 確認服務要被啟動

image

 

 

 

執行結果:

(網路連線中才可收得到通知)

image_thumb7

結語

以上介紹了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