和本地通知不同脯颜,推送通知(遠(yuǎn)程推送)是由應(yīng)用服務(wù)提供商
發(fā)起的,通過蘋果的APNs(Apple Push Notification Server)發(fā)送到應(yīng)用客戶端
Provider:就是為指定iOS設(shè)備應(yīng)用程序提供Push的服務(wù)器箱吕,(如果iOS設(shè)備的應(yīng)用程序是客戶端的話,那么Provider可以理解為服務(wù)端[消息的發(fā)起者])柿冲;
APNS:Apple Push Notification Service[蘋果消息推送服務(wù)器]茬高;
iPhone:用來接收APNS下發(fā)下來的消息;
Client App:iOS設(shè)備上的應(yīng)用程序假抄,用來接收iphone傳遞APNS下發(fā)的消息到制定的一個(gè)客戶端 app[消息的最終響應(yīng)者]怎栽;
推送通知可以分為三個(gè)階段
階段一:Provider[服務(wù)端]把要發(fā)送的消息,目的iOS設(shè)備標(biāo)識(shí)打包宿饱,發(fā)送給APNS熏瞄;
階段二:APNS在自身的已注冊(cè)Push服務(wù)的iOS設(shè)備列表中,查找有相應(yīng)標(biāo)識(shí)的iOS設(shè)備谬以,并將消息發(fā)送到iOS設(shè)備强饮;
階段三:iOS設(shè)備把發(fā)送的消息傳遞給對(duì)應(yīng)的應(yīng)用程序,并且按照設(shè)定彈出Push通知为黎。
具體過程邮丰,如下圖
1行您、[Client App]注冊(cè)消息推送;
只有注冊(cè)過的應(yīng)用才有可能接收到消息剪廉,程序中通常通過UIApplication的registerUserNotificationSettings:方法注冊(cè)娃循,iOS8中通知注冊(cè)的方法發(fā)生了改變,如果是iOS7及之前版本的iOS請(qǐng)參考其他代碼斗蒋。
注冊(cè)之前有兩個(gè)前提條件必須準(zhǔn)備好:開發(fā)配置文件(provisioning profile捌斧,也就是.mobileprovision后綴的文件)的App ID不能使用通配ID必須使用指定APP ID并且生成配置文件中選擇Push Notifications服務(wù),一般的開發(fā)配置文件無法完成注冊(cè)泉沾;應(yīng)用程序的Bundle Identifier必須和生成配置文件使用的APP ID完全一致捞蚂。
2、[Client App]跟[APNS Service]要deviceToken, Client App接收到APNs 分配的deviceToken
在UIApplication的
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
代理方法中獲取令牌爆哑,此方法發(fā)生在注冊(cè)之后洞难。如果無法正確獲得device token可以在UIApplication的
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
代理方法中查看詳細(xì)錯(cuò)誤信息舆吮,此方法發(fā)生在獲取device token失敗之后揭朝。必須真機(jī)調(diào)試,模擬器無法獲取device token色冀。
3潭袱、[Client App]將deviceToken發(fā)送給[Provider]Push服務(wù)端程序,告訴服務(wù)器端當(dāng)前設(shè)備允許接收消息。
device token的生成算法只有Apple掌握锋恬,為了確保算法發(fā)生變化后仍然能夠正常接收服務(wù)器端發(fā)送的通知屯换,每次應(yīng)用程序啟動(dòng)都重新獲得device token(device token的獲取不會(huì)造成性能問題,蘋果官方已經(jīng)做過優(yōu)化)与学。
通惩冢可以創(chuàng)建一個(gè)網(wǎng)絡(luò)連接發(fā)送給應(yīng)用程序提供商的服務(wù)器端, 在這個(gè)過程中最好將上一次獲得的device token存儲(chǔ)起來索守,避免重復(fù)發(fā)送晕窑,一旦發(fā)現(xiàn)device token發(fā)生了變化最好將原有的device token一塊發(fā)送給服務(wù)器端,服務(wù)器端刪除原有令牌存儲(chǔ)新令牌避免服務(wù)器端發(fā)送無效消息卵佛。
4杨赤、當(dāng)Push服務(wù)端程序滿足發(fā)送消息條件了,[Provider]向[APNS Service]發(fā)送消息截汪;
- 發(fā)送時(shí)指定device token和消息內(nèi)容疾牲,并且完全按照蘋果官方的消息格式組織消息內(nèi)容,通常情況下可以借助其他第三方消息推送框架來完成衙解。
5阳柔、[APNS Service]根據(jù)消息中的device token查找已注冊(cè)的設(shè)備推送消息,將消息發(fā)送給[Client App].
- 正常情況下可以根據(jù)device token將消息成功推送到客戶端設(shè)備中,但是也不排除用戶卸載程序的情況蚓峦,此時(shí)推送消息失敗舌剂,APNs會(huì)將這個(gè)錯(cuò)誤消息通知服務(wù)器端以避免資源浪費(fèi)(服務(wù)器端此時(shí)可以根據(jù)錯(cuò)誤刪除已經(jīng)存儲(chǔ)的device token医咨,下次不再發(fā)送)
推送通知的流程代碼:
//
// AppDelegate.m
// pushnotification
//
#import "AppDelegate.h"
#import "LTYViewController.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
#pragma mark - 應(yīng)用程序代理方法
#pragma mark 應(yīng)用程序啟動(dòng)之后
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
_window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
_window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1];
//設(shè)置全局導(dǎo)航條風(fēng)格和顏色
[[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]];
[[UINavigationBar appearance] setBarStyle:UIBarStyleBlack];
LTYViewController *mainController=[[LTYViewController alloc]init];
_window.rootViewController=mainController;
[_window makeKeyAndVisible];
//注冊(cè)推送通知(注意iOS8注冊(cè)方法發(fā)生了變化)
[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]];
[application registerForRemoteNotifications];
return YES;
}
#pragma mark 注冊(cè)推送通知之后
//在此接收設(shè)備令牌
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
[self addDeviceToken:deviceToken];
NSLog(@"device token:%@",deviceToken);
}
#pragma mark 獲取device token失敗后
-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{
NSLog(@"didFailToRegisterForRemoteNotificationsWithError:%@",error.localizedDescription);
[self addDeviceToken:nil];
}
#pragma mark 接收到推送通知之后
-(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
NSLog(@"receiveRemoteNotification,userInfo is %@",userInfo);
}
#pragma mark - 私有方法
/**
* 添加設(shè)備令牌到服務(wù)器端
*
* @param deviceToken 設(shè)備令牌
*/
-(void)addDeviceToken:(NSData *)deviceToken{
NSString *key=@"DeviceToken";
NSData *oldToken= [[NSUserDefaults standardUserDefaults] objectForKey:key];
//如果偏好設(shè)置中的已存儲(chǔ)設(shè)備令牌和新獲取的令牌不同則存儲(chǔ)新令牌并且發(fā)送給服務(wù)器端
if (![oldToken isEqualToData:deviceToken]) {
[[NSUserDefaults standardUserDefaults] setObject:deviceToken forKey:key];
[self sendDeviceTokenWidthOldDeviceToken:oldToken newDeviceToken:deviceToken];
}
}
-(void)sendDeviceTokenWidthOldDeviceToken:(NSData *)oldToken newDeviceToken:(NSData *)newToken{
//注意一定確保真機(jī)可以正常訪問下面的地址
NSString *urlStr=@"http://192.168.1.101/RegisterDeviceToken.aspx";
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
NSMutableURLRequest *requestM=[NSMutableURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:10.0];
[requestM setHTTPMethod:@"POST"];
NSString *bodyStr=[NSString stringWithFormat:@"oldToken=%@&newToken=%@",oldToken,newToken];
NSData *body=[bodyStr dataUsingEncoding:NSUTF8StringEncoding];
[requestM setHTTPBody:body];
NSURLSession *session=[NSURLSession sharedSession];
NSURLSessionDataTask *dataTask= [session dataTaskWithRequest:requestM completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"Send failure,error is :%@",error.localizedDescription);
}else{
NSLog(@"Send Success!");
}
}];
[dataTask resume];
}
@end
- iOS客戶端代碼的代碼比較簡(jiǎn)單,注冊(cè)推送通知架诞,獲取device token存儲(chǔ)到偏好設(shè)置中拟淮,并且如果新獲取的device token不同于偏好設(shè)置中存儲(chǔ)的數(shù)據(jù)則發(fā)送給服務(wù)器端,更新服務(wù)器端device token列表谴忧。
- 由于device token需要發(fā)送給服務(wù)器端很泊,這里使用一個(gè)Web應(yīng)用作為服務(wù)器端接收device token,這里使用了ASP.NET程序來處理令牌接收注冊(cè)工作沾谓,當(dāng)然你使用其他技術(shù)同樣沒有問題委造。下面是對(duì)應(yīng)的后臺(tái)代碼:
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using CMJ.Framework.Data;
namespace WebServer
{
public partial class RegisterDeviceToken : System.Web.UI.Page
{
private string _appID = @"com.cmjstudio.pushnotification";
private SqlHelper _helper = new SqlHelper();
protected void Page_Load(object sender, EventArgs e)
{
try
{
string oldToken = Request["oldToken"] + "";
string newToken = Request["newToken"] + "";
string sql = "";
//如果傳遞舊的設(shè)備令牌則刪除舊令牌添加新令牌
if (oldToken != "")
{
sql = string.Format("DELETE FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}';", _appID, oldToken);
}
sql += string.Format(@"IF NOT EXISTS (SELECT ID FROM dbo.Device WHERE AppID='{0}' AND DeviceToken='{1}')
INSERT INTO dbo.Device ( AppID, DeviceToken ) VALUES ( N'{0}', N'{1}');", _appID, newToken);
_helper.ExecuteNonQuery(sql);
Response.Write("注冊(cè)成功!");
}
catch(Exception ex)
{
Response.Write("注冊(cè)失敗均驶,錯(cuò)誤詳情:"+ex.ToString());
}
}
}
}
這個(gè)過程主要就是保存device token到數(shù)據(jù)庫中昏兆,當(dāng)然如果同時(shí)傳遞舊的設(shè)備令牌還需要先刪除就的設(shè)備令牌,這里簡(jiǎn)單的在數(shù)據(jù)庫中創(chuàng)建了一張Device表來保存設(shè)備令牌妇穴,其中記錄了應(yīng)用程序Id和設(shè)備令牌爬虱。
- 第三步就是服務(wù)器端發(fā)送消息,如果要給APNs發(fā)送消息就必須按照Apple的標(biāo)準(zhǔn)消息格式組織消息內(nèi)容腾它。但是好在目前已經(jīng)有很多開源的第三方類庫供我們使用跑筝,具體消息如何包裝完全不用自己組織,這里使用一個(gè)開源的類庫Push Sharp來給APNs發(fā)送消息 ,除了可以給Apple設(shè)備推送消息瞒滴,Push Sharp還支持Android曲梗、Windows Phone等多種設(shè)備,更多詳細(xì)內(nèi)容大家可以參照官方說明妓忍。前面說過如果要開發(fā)消息推送應(yīng)用不能使用一般的開發(fā)配置文件虏两,這里還需要注意:如果服務(wù)器端要給APNs發(fā)送消息其秘鑰也必須是通過APNs Development iOS類型的證書來導(dǎo)出的,一般的iOS Development 類型的證書導(dǎo)出的秘鑰無法用作服務(wù)器端發(fā)送秘鑰世剖。下面通過在一個(gè)簡(jiǎn)單的WinForm程序中調(diào)用Push Sharp給APNs發(fā)送消息定罢,這里讀取之前Device表中的所有設(shè)備令牌循環(huán)發(fā)送消息:
using System;
using System.IO;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using PushSharp;
using PushSharp.Apple;
using CMJ.Framework.Data;
using CMJ.Framework.Logging;
using CMJ.Framework.Windows.Forms;
namespace PushNotificationServer
{
public partial class frmMain : PersonalizeForm
{
private string _appID = @"com.cmjstudio.pushnotification";
private SqlHelper _helper = new SqlHelper();
public frmMain()
{
InitializeComponent();
}
private void btnClose_Click(object sender, EventArgs e)
{
this.Close();
}
private void btnSend_Click(object sender, EventArgs e)
{
List<string> deviceTokens = GetDeviceToken();
SendMessage(deviceTokens, tbMessage.Text);
}
#region 發(fā)送消息
/// <summary>
/// 取得所有設(shè)備令牌
/// </summary>
/// <returns>設(shè)備令牌</returns>
private List<string> GetDeviceToken()
{
List<string> deviceTokens = new List<string>();
string sql = string.Format("SELECT DeviceToken FROM dbo.Device WHERE AppID='{0}'",_appID);
DataTable dt = _helper.GetDataTable(sql);
if(dt.Rows.Count>0)
{
foreach(DataRow dr in dt.Rows)
{
deviceTokens.Add((dr["DeviceToken"]+"").TrimStart('<').TrimEnd('>').Replace(" ",""));
}
}
return deviceTokens;
}
/// <summary>
/// 發(fā)送消息
/// </summary>
/// <param name="deviceToken">設(shè)備令牌</param>
/// <param name="message">消息內(nèi)容</param>
private void SendMessage(List<string> deviceToken, string message)
{
//創(chuàng)建推送對(duì)象
var pusher = new PushBroker();
pusher.OnNotificationSent += pusher_OnNotificationSent;//發(fā)送成功事件
pusher.OnNotificationFailed += pusher_OnNotificationFailed;//發(fā)送失敗事件
pusher.OnChannelCreated += pusher_OnChannelCreated;
pusher.OnChannelDestroyed += pusher_OnChannelDestroyed;
pusher.OnChannelException += pusher_OnChannelException;
pusher.OnDeviceSubscriptionChanged += pusher_OnDeviceSubscriptionChanged;
pusher.OnDeviceSubscriptionExpired += pusher_OnDeviceSubscriptionExpired;
pusher.OnNotificationRequeue += pusher_OnNotificationRequeue;
pusher.OnServiceException += pusher_OnServiceException;
//注冊(cè)推送服務(wù)
byte[] certificateData = File.ReadAllBytes(@"E:\KenshinCui_Push.p12");
pusher.RegisterAppleService(new ApplePushChannelSettings(certificateData, "123"));
foreach (string token in deviceToken)
{
//給指定設(shè)備發(fā)送消息
pusher.QueueNotification(new AppleNotification()
.ForDeviceToken(token)
.WithAlert(message)
.WithBadge(1)
.WithSound("default"));
}
}
void pusher_OnServiceException(object sender, Exception error)
{
Console.WriteLine("消息發(fā)送失敗,錯(cuò)誤詳情:" + error.ToString());
PersonalizeMessageBox.Show(this, "消息發(fā)送失敗搁廓,錯(cuò)誤詳情:" + error.ToString(), "系統(tǒng)提示");
}
void pusher_OnNotificationRequeue(object sender, PushSharp.Core.NotificationRequeueEventArgs e)
{
Console.WriteLine("pusher_OnNotificationRequeue");
}
void pusher_OnDeviceSubscriptionExpired(object sender, string expiredSubscriptionId, DateTime expirationDateUtc, PushSharp.Core.INotification notification)
{
Console.WriteLine("pusher_OnDeviceSubscriptionChanged");
}
void pusher_OnDeviceSubscriptionChanged(object sender, string oldSubscriptionId, string newSubscriptionId, PushSharp.Core.INotification notification)
{
Console.WriteLine("pusher_OnDeviceSubscriptionChanged");
}
void pusher_OnChannelException(object sender, PushSharp.Core.IPushChannel pushChannel, Exception error)
{
Console.WriteLine("消息發(fā)送失敗引颈,錯(cuò)誤詳情:" + error.ToString());
PersonalizeMessageBox.Show(this, "消息發(fā)送失敗,錯(cuò)誤詳情:" + error.ToString(), "系統(tǒng)提示");
}
void pusher_OnChannelDestroyed(object sender)
{
Console.WriteLine("pusher_OnChannelDestroyed");
}
void pusher_OnChannelCreated(object sender, PushSharp.Core.IPushChannel pushChannel)
{
Console.WriteLine("pusher_OnChannelCreated");
}
void pusher_OnNotificationFailed(object sender, PushSharp.Core.INotification notification, Exception error)
{
Console.WriteLine("消息發(fā)送失敗境蜕,錯(cuò)誤詳情:" + error.ToString());
PersonalizeMessageBox.Show(this, "消息發(fā)送失敗蝙场,錯(cuò)誤詳情:"+error.ToString(), "系統(tǒng)提示");
}
void pusher_OnNotificationSent(object sender, PushSharp.Core.INotification notification)
{
Console.WriteLine("消息發(fā)送成功!");
PersonalizeMessageBox.Show(this, "消息發(fā)送成功粱年!", "系統(tǒng)提示");
}
#endregion
}
}
服務(wù)器端消息發(fā)送應(yīng)用運(yùn)行效果:
iOS客戶端接收的消息的效果:
到目前為止通過服務(wù)器端應(yīng)用可以順利發(fā)送消息給APNs并且iOS應(yīng)用已經(jīng)成功接收推送消息售滤。