[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
點擊紅框處下載
解壓縮打開.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項目圖示右上角的新訊息數:
這邊提供一個演算法
假設我的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檔記得屬性設成以下↓建置專案時候才會把該檔案也一同輸出