[C#] 利用ASP.net和Console專案實作iOS的訊息推播

[C#] 利用ASP.net和Console專案實作iOS的訊息推播

想說最近工作都要做,還是順便寫一寫好了

雖然對岸已經有人寫的超詳細:iPhone消息推送机制实现与探讨 - 麒麟 - 博客园

以下是寫給開發Server端 .Net人員看的

開發流程請見上面連結的第二張圖,很實用

 

1. 首先準備一個p12檔案及其密碼,沒有的話去向iOS開發人員要,他們會想辦法生出來

2. 寫一個接收iOS向Server端寫入token的WebService(或一個網頁)

以下以泛型處理常式.ashx做Demo



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 SaveiOSToken : IHttpHandler
{

    NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();//Nlog教學:http://blog.miniasp.com/post/2010/07/18/Useful-Library-NLog-Advanced-NET-Logging.aspx
    
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";

        //App傳送過來的的token
        string token = context.Request["token"];
        string Del = context.Request["Del"];//是否從DB刪除token
        SqlParameter[] param = new SqlParameter[] { new SqlParameter() { ParameterName = "@token", SqlDbType = SqlDbType.VarChar, Value = token } };
        string json = string.Empty;//輸出結果的json字串

        bool validate = false;
        

            if (!string.IsNullOrEmpty(token))//防呆
            {
                try
                {
                    if (!string.IsNullOrEmpty(Del) && "true".Equals(Del))
                    {//從DB把token刪除
                        SqlHelper.ExecteNonQuery(CommandType.Text, "Delete From tb_iOStoken Where token =@token", param);
                    }
                    else
                    {
                        int count = Convert.ToInt32(SqlHelper.ExecuteScalar(CommandType.Text, "Select count(*) From tb_iOStoken Where token=@token", param));
                        if (count==0)
                        {//無此token,//新增token到DB
                            SqlHelper.ExecteNonQuery(CommandType.Text, "Insert into tb_iOStoken (token) values (@token)", param);
                        }
                    }
                    validate = true;
                }
                catch (Exception ex)
                {
                    logger.Error(ex.ToString());//寫Log
                    validate = false;
                }
              
            }
           
            
             
            if (validate)
            {
                json = @"{""Success"":true}";
            }
            else
            {
                json = @"{""Success"":false}";    
            }
            
            context.Response.Write(json);//輸出json訊息
        
       
        
        
        
        
        
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }

}

3. 準備一個Console專案,這個Console專案寫好了是要用來搭配Windows作業系統的「排定工作」定期檢查有無新資料,有新資料的話,就發訊息給APNS Server,交由APNS Server發訊息

為啥要用Console專案而不是Windows Service專案?

因為訊息推播的代碼接下來要用網路上老外寫好的現成代碼,它是Console專案

請到這裡:Redth-APNS-Sharp · GitHub

點擊紅框處下載

image

解壓縮打開.sln檔,重點只有兩個專案:JdSoft.Apple.Apns.Notifications(類別庫專案)和JdSoft.Apple.Apns.Notifications.Test

因為JdSoft.Apple.Apns.Notifications會用到Newtonsoft.Json.dll,所以有缺Json.net的話,可以先到這下載加入參考

JdSoft.Apple.Apns.Notifications.Test專案就是我們要用來跑排程發訊息的專案,基本上程式碼照Copy記得加入JdSoft.Apple.Apns.Notifications這個專案的參考,然後改一下token和p12檔名和密碼就可以跑

不過有一行要注意一下

Badge它是指iOS項目圖示右上角的新訊息數:

IMAG0025

這邊提供一個演算法

假設我的Console程式排程每隔5秒鐘會去論詢資料來源RSS超連結

因為要算新訊息數,所以該RSS裡的每筆資料勢必要存進DB,供之後的輪詢做比對新舊資料,然後資料表開一個欄位叫isNew

RSS的每筆資料存進資料表前都做Select判斷

如果此資料不存在資料表就Insert並標記為新訊息(isNew='True'),已存在就把資料表裡的訊息標為舊訊息(isNew='False')

如果新訊息數大於0,程式就推播訊息

不過這個演算法有個缺點:由於RSS每筆資料都做Select比對已無存在DB,所以很耗DB效能,雖說RSS的資料不會超過100筆,但DB儲存下來的RSS資料也會愈增愈多慢慢地Query效能變差

雖然我有考慮過把DB舊資料刪除

但實際情況發現

程式每次輪詢資料來源RSS,一定會有萬年資料永遠固定在那

如果把資料表的舊資料刪除的話,下一次程式輪詢會把固定的資料當作新訊息推播出去>”<

對於新訊息數的算法,誰有更好解法的話,請務必留言分享一下Orz

※2012.7.18追記:以上演算法目前實務上有碰到一個Bug

如果Console程式一開始從RSS抓資料塞到DB(isNew=’True’),在下一次Console程式輪詢前,剛好RSS資料被刪除了

就會變成DB裡永遠會有一筆isNew=’True’的資料,然後程式會變成每5秒鐘一定推播的奇怪現象

 

目前想到另一種更好的解決方案:

除了建立token資料表、RSS訊息資料表外,再建立一個token與RSS訊息已推播資料表

就不再判斷RSS資料是否為新訊息,而是程式把RSS資料抓下來後判斷該token沒推過該RSS訊息的話,程式就推播,我想這會比較合理

 

比對新舊資料的類別:


using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Xml.Linq;
using System.Data.SqlClient;
using SystemDAO;
using System.Data;

namespace Console_ServerPush_Data
{
    //處理資料比對
    public class DataHandler
    {
        NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
     

        public int NewMsgNum()
        {
            int result = 0;//要回傳的變數
            string uri = ConfigurationManager.AppSettings["RssUrl"];//資料來源Url

            XDocument doc = null;
            try
            {
                doc = XDocument.Load(uri);//載入RSS
            }
            catch (Exception e1)
            {
                logger.Error(e1.ToString());
            }


            foreach (XElement ele in doc.Descendants("item"))
            {
                //RSS每筆都跟DB資料表比對,是否為新資料 
                string pubDate = Convert.ToDateTime(ele.Element("pubDate").Value).ToString("yyyy/MM/dd HH:mm:ss");
                string title = ele.Element("title").Value;
                string description = ele.Element("description").Value;
                
                SqlParameter[] paras = new SqlParameter[]
                { 
                    new SqlParameter(){ ParameterName="@pubDate", SqlDbType=SqlDbType.VarChar,Value = pubDate},
                    new SqlParameter(){ ParameterName="@title", SqlDbType=SqlDbType.NVarChar,Value = title},
                    new SqlParameter(){ ParameterName="@description", SqlDbType=SqlDbType.NVarChar,Value = description},                 
                };
                try
                {
                    //新資料就Insert,舊資料就標示isNew='False'
                    SqlHelper.ExecuteScalarProducts("sp_CheckRssNews", paras);
                }
                catch (Exception e2)
                {
                    logger.Error(e2.ToString());

                }


            }//End foreach


            try
            {   //計算新訊息數
                result = Convert.ToInt32(SqlHelper.ExecuteScalar(CommandType.Text, "Select count(*) from tb_News Where isNew='True'", null));
            }
            catch (Exception e3)
            {
                logger.Error(e3.ToString());

            }

            return result;


        }//End NewMsgNum()


    }//End class
}//End namespace

sp_CheckRssNews預存程序:


-- 檢查tb_News資料表有無此RSS資料
-- 有的話把現有資料標成舊資料,沒有的話做Insert
-- =============================================
-- Author:		Shadow
-- Create date: 2012.7.2
-- =============================================
CREATE PROCEDURE  sp_CheckRssNews  
	(
	  
	 @pubDate varchar(50),@title nvarchar(500),@description nvarchar(max) 
	)
AS
BEGIN
	 Declare @rownum int=0 /*用來判斷是否已重覆的變數*/

	 
	  /*比對資料是否已存在*/
	  Select @rownum=count(*)
	  From tb_News
	  Where  title=@title  And description=@description And Convert(varchar(10),pubDate,111)+' '+Convert(varchar(10),pubDate,108)=@pubDate
                  
	  if(@rownum=0)/*不存在,要新增*/
	  Insert into tb_News (pubDate,title,description,isNew) values (@pubDate,@title,@description,'True') 
	 else 
	 /*已存在,更新為舊資料*/
	 Update tb_News
	 Set isNew='False'
	 Where  title=@title  And description=@description  And Convert(varchar(10),pubDate,111)+' '+Convert(varchar(10),pubDate,108)=@pubDate
    
END
 


 
GO


Console專案:


using System.Collections.Generic;
using System.Linq;
using System.Text;
using JdSoft.Apple.Apns.Notifications;
using NLog;
using System.Data;
using SystemDAO;
using System.Configuration;
using System.Data.SqlClient; 

namespace JdSoft.Apple.Apns.Test
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {

            //先比對資料
            DataHandler dh = new DataHandler();
            int NewMsgNum = dh.NewMsgNum();//取得新資料數
            LogManager.GetCurrentClassLogger().Trace("新資料數:" + NewMsgNum);

 
            if (NewMsgNum > 0)
            {//要推播


                #region 推iOS
              
                //開發時期用true測試(使用Development p12檔),上架時用false(使用Production p12檔)
                bool sandbox = Convert.ToBoolean(ConfigurationManager.AppSettings["sendTest"]);

                //p12檔的相對路徑
                string p12File = ConfigurationManager.AppSettings["p12FilePath"];
                //p12檔的絕對路徑
                string p12Filename = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, p12File);
                //p12檔的密碼
                string p12FilePassword = ConfigurationManager.AppSettings["p12FilePassword"];

                

                //從DB取得iOS的token]
                DataTable dtToken = SqlHelper.GetTable(CommandType.Text, "Select token from tb_iOSToken Order by token ASC", null)[0];
                
                
                //Create a new notification to send
                Notification alertNotification = new Notification();  
                alertNotification.Payload.Alert.Body = "要推播的訊息";
                alertNotification.Payload.Sound = "default";//前端消息通知的音效:default、bingbong.aiff、chime
                alertNotification.Payload.Badge = NewMsgNum;//新訊息數

           
                foreach (DataRow row in dtToken.Rows)
                {//一筆一筆發送

                  NotificationService service = new NotificationService(sandbox, p12Filename, p12FilePassword, 1);
 
                  service.SendRetries = 5; //5 retries before generating notificationfailed event
                  service.ReconnectDelay = 5000; //5 seconds

                  service.Error += new NotificationService.OnError(service_Error);
                  service.NotificationTooLong += new NotificationService.OnNotificationTooLong(service_NotificationTooLong);

                  service.BadDeviceToken += new NotificationService.OnBadDeviceToken(service_BadDeviceToken);
                  service.NotificationFailed += new NotificationService.OnNotificationFailed(service_NotificationFailed);
                  service.NotificationSuccess += new NotificationService.OnNotificationSuccess(service_NotificationSuccess);
                  service.Connecting += new NotificationService.OnConnecting(service_Connecting);
                  service.Connected += new NotificationService.OnConnected(service_Connected);
                  service.Disconnected += new NotificationService.OnDisconnected(service_Disconnected);
                    string token = row["token"].ToString();
                   
                    alertNotification.DeviceToken = token;
                
                    if (service.QueueNotification(alertNotification))
                    {
                        LogManager.GetCurrentClassLogger().Trace("此token已排進佇列"+token);
                    }
                    else
                    {
                        LogManager.GetCurrentClassLogger().Trace("此token排進佇列失敗" + token);
                    }
                   
     	  service.Close();
                  service.Dispose();
                }//End foreach

         
        
                #endregion 
                
            
            }//End if 
             
        }//End Main()

        static void service_BadDeviceToken(object sender, BadDeviceTokenException ex)
        {
            Console.WriteLine("Bad Device Token: {0}", ex.Message);
            LogManager.GetCurrentClassLogger().Trace("Bad Device Token: "+ex.Message);
        }

        static void service_Disconnected(object sender)
        {
            Console.WriteLine("Disconnected...");
            LogManager.GetCurrentClassLogger().Trace("Disconnected...");
        }

        static void service_Connected(object sender)
        {
            Console.WriteLine("Connected...");
            LogManager.GetCurrentClassLogger().Trace("Connected...");
        }

        static void service_Connecting(object sender)
        {
            Console.WriteLine("Connecting...");
            LogManager.GetCurrentClassLogger().Trace("Connecting...");
        }

        static void service_NotificationTooLong(object sender, NotificationLengthException ex)
        {
            Console.WriteLine(string.Format("Notification Too Long: {0}", ex.Notification.ToString()));
            LogManager.GetCurrentClassLogger().Trace("Notification Too Long:"+ex.Notification.ToString());
        }

        static void service_NotificationSuccess(object sender, Notification notification)
        {
            Console.WriteLine(string.Format("Notification Success: {0}", notification.ToString()));
            LogManager.GetCurrentClassLogger().Trace("Notification Success:"+notification.ToString());
        }

        static void service_NotificationFailed(object sender, Notification notification)
        {
            Console.WriteLine(string.Format("Notification Failed: {0}", notification.ToString()));
            LogManager.GetCurrentClassLogger().Trace("Notification Failed:"+notification.ToString());
        }

        static void service_Error(object sender, Exception ex)
        {
            Console.WriteLine(string.Format("Error: {0}", ex.Message));
            LogManager.GetCurrentClassLogger().Trace("Error:"+ex.Message);
        }
    }
}

 

結語:

iOS的訊息推播不只可推iPhone,iPad應該也可以推播

目前手邊的裝置沒得裝App,執行結果就不擷圖了

 

 

 

其他參考文章:

How to Configure & Send Apple Push Notifications using PushSharp

Apple Push Notification Services Tutorial Part 1-2  Ray Wenderlich

How to build an Apple Push Notification provider server (tutorial)

How to renew your Apple Push Notification Push SSL Certificate

ios iphone在真机测试以及apns的设置 - Tomson Xu - 博客频道 - CSDN.NET

iphone 推送服务--Apple Push Notification Service - ios专栏 - 博客频道 - CSDN.NET

 

2012.7.6 追記

上述從Redth-APNS-Sharp · GitHub下載來的JdSoft.Apple.Apns.Feedback專案

這是專門取得無效token的,使用方法在

JdSoft.Apple.Apns.Feedback.Test專案的Program.cs方法


		{
			Console.WriteLine(string.Format("Feedback - Timestamp: {0} - DeviceId: {1}", feedback.Timestamp, feedback.DeviceToken));
		}

 

Console專案裡的p12檔記得屬性設成以下↓建置專案時候才會把該檔案也一同輸出

image