最近在做苹果消息推送方面的工作,收集整理了网上提供的信息,包括客户端和服务器端代码(php以及c语言),分享给大家。

一.首先介绍一下苹果APNS的推送机制:

 苹果的推送服务APNs基本原理简单来说就是苹果利用自己专门的推送服务器(APNs)接收来自我们自己应用服务器的需要被推送的信息,然后推送到指定的iOS设备上,然后由设备通知到我们的应用程序,设备以通知或者声音的形式通知用户有新的消息。推送的前提是装有我们应用的设备需要向APNs服务器注册,注册成功后APNs服务器会返给我们一个device_token,拿到这个token后我们将这个token发给我们自己的应用服务器,当有需要被推送的消息时,我们的应用服务器会将消息按指定的格式打包,然后结合设备的device_token一并发给APNs服务器,由于我们的应用和APNs维持一个基于TCP的长连接,APNs将新消息推送到我们设备上,然后在屏幕上显示出新消息来。整个过程基本就这样,下面我们看一下设备注册APNs的流程图:


上图完成了如下步骤:

1.Device连接APNs服务器并携带设备序列号

2.连接成功,APNs经过打包和处理产生device_token并返回给注册的Device

3.Device携带获取的device_token向我们自己的应用服务器注册

4.完成需要培推送的Device在APNs服务器和我们自己的应用服务器注册


执行顺序如下所示:

  这里要提到的一点是,我们的设备和APNS服务器之间的通讯是基于SSL协议的TCP流通讯,二者之间维持一个长连接,当从APNS服务器注册成功后,一定要将device_token发送给我们的应用服务器,因为在推送过程中,首先是由我们的应用服务器(上图中Provider)将需要推送的消息结合device_token按指定格式(后面会提到)打包然后发送给APNS服务器,然后由APNS服务器推送给我们的设备。

好了,注册设备的过程完成了,接下来就是如何推送了:


推送的过程经过如下步骤:

1.首先,安装了具有推送功能的应用,我们的设备在有网络的情况下会连接苹果推送服务器,连接过程中,APNS会验证device_token,连接成功后维持一个长连接;

2.Provider(我们自己的服务器)收到需要被推送的消息并结合被推送设备的device_token一起打包发送给APNS服务器;

3.APNS服务器将推送信息推送给指定device_token的设备;

4.设备收到推送消息后通知我们的应用程序并显示和提示用户(声音、弹出框)

比较直观的流程参照下图:


上图显示了我们的应用服务器将消息推送到我们的App的完整路径,Provider就是我们自己程序的后台服务器,APNS是Apple Push Notification Service的缩写,也就是苹果的推送服务器。其实真正完成推送的是APNS服务器,我们自己的应用服务器只是将需要推送的消息告诉苹果服务器,至于如何维护消息队列或如何保证消息能被推送到指定的设备上,这些都由苹果APNS给我们做完了。

二.证书生成下载安装和处理工作

准备工作

首先要有一台苹果的设备,模拟器是不支持推送的,所以你需要一台iphone,ipod touch或者ipad。

 

我们的客户端与苹果服务器之间和我们自己的服务器与苹果服务器之间都需要证书来进行链接。下面我们来开始进入证书的制作过程

1 CSR文件

首先我们要生成一个Certificate Signing Request(也就是CSR)的请求文件。

在应用程序里的使用工具中找到钥匙串访问。
 


选择从证书颁发机构请求证书

填上你的邮箱和常用名,常用名要记一下,一会会用到。然后选择保存到磁盘,继续


保存位置在桌面,点击存储。

到这里点击完成后我们会在桌面上看到一个CertificateSigningRequest.certSigningRequest的请求文件,也就是我们说的CSR文件。在我们生成CSR文件的同时,会在钥匙串访问中生成一对秘钥,名称为刚才我们填写的常用名


2、下载开发证书和发布证书

(这里我为了大家能看清楚,已经把之前的证书事先吊销了)

https://developer.apple.com/devcenter/ios/index.action  登录后,在右侧的ios Developer Program里点击iOS Provisioning Portal。

进入下一级页面后在左侧选择Certificates



 

点击红色的部分生成一个开发证书



注意,如果你在后面测试的时候出现了问题,请检查一下这里,这里的CSR请求文件必须是我们刚才生成的那个。


 

 

选择完成后点击Submit提交



 

提交完成后返回页面。页面会是这样的,然后我们刷新一下页面



刷新后会出现一个下载按钮,我们点击下载。现在我们的开发证书已经配置并下载好了,发布证书的配置过程和开发证书是一致的,不再赘述。下载完后双击,会跳到钥匙串访问里。这就是我们之后要进行测的证书

 

3 配置AppID,配置并下载SSL证书

注意,这里的App ID必须不能是通配的,通配的不可以做推送)。点击Configure



 

进入后默认推送功能是关闭的,我们需要把推送功能打开



 

选中打开后,点击右边灰色的Configure按钮



 

这里的文件同样的,还是选择我们生成在桌面上的CSR请求文件


 

然后点击继续



 

出现了我们需要的SSL证书,我们点击下载后点击Done完成。



Status状态变成了绿色可用了。这里右边的下载和上一步的下载是一样的,如果在上一步中下载了证书,便无需再次下载。此时我们有了一个名字叫aps_development.cer的SSL证书,同样,我们把他放在桌面。双击后会跳到钥匙串访问,出现我们的SSL推送证书。同样,发布的SSL证书的步骤也是一样的。

 

4、下载Provisioning证书

在左侧选择Provisioning



 

注意App ID要与我们刚的程序对应)




之后变回出现下载按钮,我们点击下载。下载后双击,并将我们的设备上的描述文件更新一下(最好把之前的全部删除,然后再安装,防止出错)。


 

5 从钥匙串访问中导出秘钥

打开钥匙串访问,找到我们的专用秘钥(专用秘钥的名称就是我们在最开始生成CSR请求的时候填写的常用名)



右键选择导出



 

导出的文件名我们叫做Push



在这里需要输入一个密码来对文件进行加密。这里我们选择abcabc,当然你也可以自己选择是什么,但是这个密码必须要铭记,切记!



 

 

然后输入你电脑的密码,点击允许。这样我们就在桌面上生成了一个Push.p12文件。到此为止,我们在桌面上一共生成了三个文件。一个是CSR请求文件,一个是aps_development .cer的SSL证书文件,还有一个刚才生成的Push.p12秘钥文件。

 

现在我们的准备工作已经做完了。要开始对生成的文件进行处理了。原因上面已经解释过,因为我们的服务链接苹果服务器也是需要证书的,但是我们直接生成的证书windows系统(我们一般的服务器都是win系统的)是不识别的,所以我们需要生成一个后缀为pem的带证书带秘钥的文件。

 

6  处理证书

下面我们打开终端(位置:应用程序à实用工具à终端)。

cd到桌面,我们那三个文件所在的位置

 

1、把.cer的SSL证书转换为.pem文件,执行命令:

openssl x509 -in aps_development.cer -inform der -out PushChatCert.pem


在桌面上会生成一个PushChatCert.pem文件



 

2、把私钥Push.p12文件转化为.pem文件:

openssl pkcs12 -nocerts -out PushChatKey.pem -in Push.p12


这里需要我们输入密码,这个密码也就是我们导出p12文件时的密码,也就是我们上面设置的abcabc。然后,需要我们对生成的pem文件设置一个密语,这里我们推荐还是用上面这个abcabc,防止混乱(当然你也可以设置成别的更有意义的密语),这里的密语是要告诉我们服务器的。这样,桌面上又会生成一个PushChatKey.pem文件



 

3、对生成的这两个pem文件再生成一个pem文件,来把证书和私钥整合到一个文件里:

cat PushChatCert.pem PushChatKey.pem > ck.pem
生成ck.pem文件



 

这样,我们的文件就制作完了。下面进入测试阶段

为了测试证书是否工作,执行下面的命令:

telnet gateway.sandbox.push.apple.com 2195
它将尝试发送一个规则的,不加密的连接到APNS服务。如果你看到上面的反馈,那说明你的MAC能够到达APNS。按下Ctrl+C关闭连接。如果得到一个错误信息,那么你需要确保你的防火墙允许2195端口。一般这里都不会出现什么问题。



下面我们要使用我们生成的SSL证书和私钥来设置一个安全的链接去链接苹果服务器:

openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert PushChatCert.pem -key PushChatKey.pem

执行完这一句命令后需要我们输入密语

Enter pass phrase for PushChatKey.pem:

我们输入abcabc按回车

你会看到一个完整的输出,让你明白OpenSSL在后台做什么。如果链接是成功的,你可以随便输入一个字符,按下回车,服务器就会断开链接,如果建立连接时有问题,OpenSSL会给你返回一个错误信息。


 当你在最后的时候你看到这样说明你已经成功了:

CONNECTED(00000003)
 depth=1 /C=US/O=Entrust, Inc./OU=www.entrust.net/rpa isincorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust CertificationAuthority - L1C
 verify error:num=20:unable to get local issuercertificate
 verify return:0
 ---
 Certificate chain
  0s:/C=US/ST=California/L=Cupertino/O=Apple Inc./OU=iTMSEngineering/CN=gateway.sandbox.push.apple.com
   i:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated byreference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
  1s:/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa is incorporated byreference/OU=(c) 2009 Entrust, Inc./CN=Entrust Certification Authority - L1C
    i:/O=Entrust.net/OU=www.entrust.net/CPS_2048incorp. by ref. (limits liab.)/OU=(c) 1999 Entrust.net Limited/CN=Entrust.netCertification Authority (2048)
 ---
 Server certificate
 -----BEGIN CERTIFICATE-----
 MIIFGzCCBAOgAwIBAgIETBz90jANBgkqhkiG9w0BAQUFADCBsTELMAkGA1UEBhMC
 ……省略……
 fMGbLqkGn8YogdPqe5T1
 -----END CERTIFICATE-----
 subject=/C=US/ST=California/L=Cupertino/O=AppleInc./OU=iTMS Engineering/CN=gateway.sandbox.push.apple.com
 issuer=/C=US/O=Entrust, Inc./OU=www.entrust.net/rpa isincorporated by reference/OU=(c) 2009 Entrust, Inc./CN=Entrust CertificationAuthority - L1C
 ---
 No client certificate CA names sent
 ---
 SSL handshake has read 2731 bytes and written 2165 bytes
 ---
 New, TLSv1/SSLv3, Cipher is AES256-SHA
 Server public key is 2048 bit
 Secure Renegotiation IS supported
 Compression: NONE
 Expansion: NONE
 SSL-Session:
     Protocol  : TLSv1
     Cipher    : AES256-SHA
     Session-ID:
     Session-ID-ctx:
     Master-Key:C7A47EED5E1F5……省略……369D4
     Key-Arg   : None
     Start Time:1361862882
     Timeout   : 300 (sec)
     Verify return code: 0 (ok)
 ---

在这里提醒一下,也许你会看到像我这样的提示:verify error:num=20:unable to get local issuercertificate

verify return:0

其实是没问题的。



 

 

七 项目测试,客户端代码

注意BundleIdentifier必须和我们推送应用的App id一致)

在AppDelegate里didFinishLaunchingWithOptions函数里写

- (BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
 ……
   //推送的形式:标记,声音,提示
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes: UIRemoteNotificationTypeBadge |UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert];
 
  //程序启动有两种方式,一种是通过点击主屏幕app图标启动,一种是通过点击推送消息触发,如果是通过点击推送消息触发,那么launchOptions中的UIApplicationLaunchOptionsRemoteNotificationKey的键值将不为空
if
        NSDictionary *pushNotificationKey = [launchOptionsobjectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if
self  handleRemoteNotification:application userInfo:pushNotificationKey];
         }
     }
    return YES;
 }
 
-(void)handleRemoteNotification:(UIApplication *)application userInfo:(NSDictionary
{
    [[UIApplicationsharedApplication]setApplicationIconBadgeNumber:1];
NSString *alertMsg = [[userInfoobjectForKey:@"aps"]objectForKey:@"alert"];
    UIAlertView *alert = [[UIAlertViewalloc]initWithTitle:@"**推送通知**" message:alertMsg delegate:selfcancelButtonTitle:@"知道了"otherButtonTitles:nil,nil];
    alert.tag =alert_start_tag;
show];
}
 - (void)application:(UIApplication *)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData *)pToken {
    NSLog(@"regisger success:%@",pToken);
    //注册成功,将deviceToken保存到应用服务器数据库中
 }
 
 //当程序正在运行或在后台时,收到推送消息会回调该函数
 - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo{
     // 处理推送消息
     NSLog(@"userinfo:%@",userInfo);
    
     NSLog(@"收到推送消息:%@",[[userInfo objectForKey:@"aps"] objectForKey:@"alert"]);
 }
 - (void)application:(UIApplication *)applicationdidFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"Registfail%@",error);
 }

我们运行程序的时候会有提示,说我们的程序要发送推送通知。



八  服务器代码

c语言服务器代码(可在linux下编译运行,需要添加openssl库,我的文件名为:PushMsg.c,编译命令:gcc -o PushMsg PushMsg.c -l ssl):


# include <sys/types.h>  
# include <sys/socket.h>  
# include <netinet/in.h>  
<netdb.h>
# include <arpa/inet.h>  
<string.h>
<unistd.h>
<fcntl.h>
<errno.h>
<stdlib.h>
<stdint.h>
<assert.h>
#include <openssl/ssl.h>  
#include <openssl/bio.h>  
#include <openssl/err.h>
 
 
256
32
 
static uint32_t msgid=0;
 
void DeviceToken2Binary(constchar* sz,constint len,unsignedchar*const binary,constint
{  
int
constchar*     pin;  
char        buf[3] = {0};  
   
    assert(size >= TOKEN_SIZE);  
   
for (i =0;i < TOKEN_SIZE;i++)  
    {  
2;  
0] = pin[0];  
1] = pin[1];  
   
0;  
"%X", &val);  
        binary[i] = val;  
    }  
   
return;  
}  
   
void DeviceBinary2Token(constunsignedchar* data,constint len,char*const token,constint
{  
int
   
2);  
   
for (i =0;i < len;i++)  
    {  
2,"%02x", data[i]);  
    }  
   
return;  
}  
   
void Closesocket(int
{  
    close(socket);  
}  
   
初始化ssl库
void
{  
    SSL_library_init();  
    ERR_load_BIO_strings();  
    SSL_load_error_strings();  
    OpenSSL_add_all_algorithms();  
}  
   
SSL_CTX* init_ssl_context(  
constchar* clientcert,/*客户端的证书 */
constchar* clientkey,/*客户端的Key */
        constchar* keypwd,/*客户端Key的密码,如果有的话 */  
constchar* cacert)/* 服务器CA证书如果有的话 */
{  
    // set up the ssl context  
    SSL_CTX *ctx = SSL_CTX_new(SSLv23_client_method());  
if
returnNULL;  
    }  
   
    // certificate  
if (SSL_CTX_use_certificate_file(ctx, clientcert, SSL_FILETYPE_PEM) <=0) {  
returnNULL;  
    }  
 
    //set KeyWD
"1234");
   
    // key  
if (SSL_CTX_use_PrivateKey_file(ctx, clientkey, SSL_FILETYPE_PEM) <=0) {  
returnNULL;  
    }  
   
    // make sure the key and certificate file match  
if (SSL_CTX_check_private_key(ctx) ==0) {  
returnNULL;  
    }  
   
    // load ca if exist  
if
if (!SSL_CTX_load_verify_locations(ctx, cacert,NULL)) {  
returnNULL;  
        }  
    }  
   
return
}  
   
建立TCP连接到服务器
int tcp_connect(constchar* host,int
{  
struct
struct
int sock = -1;  
   
    //  解析域名
if
return -1;  
    }  
   
0,sizeof(addr));  
struct in_addr*)hp->h_addr_list[0];  
    addr.sin_family = AF_INET;  
    addr.sin_port = htons(port);  
   
if ((sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) <0){  
return -1;  
    }  
   
if (connect(sock, (struct sockaddr*)&addr,sizeof(addr)) !=0) {  
return -1;  
    }  
   
return
}  
   
实现SSL握手,建立SSL连接
SSL* ssl_connect(SSL_CTX* ctx,int
{  
    SSL *ssl = SSL_new(ctx);  
    BIO *bio = BIO_new_socket(socket, BIO_NOCLOSE);  
    SSL_set_bio(ssl, bio, bio);  
   
if (SSL_connect(ssl) <=0) {  
returnNULL;  
    }  
   
return
}  
   
// 验证服务器证书  
// 首先要验证服务器的证书有效,其次要验证服务器证书的CommonName(CN)与我们  
// 实际要连接的服务器域名一致  
int verify_connection(SSL* ssl,constchar* peername)  
{  
int
if
"WARNING! ssl verify failed: %d", result);  
return -1;  
    }  
   
    X509 *peer;  
char peer_CN[256] = {0};  
   
    peer = SSL_get_peer_certificate(ssl);  
255);  
if (strcmp(peer_CN, peername) !=0) {  
        fprintf(stderr, "WARNING! Server Name Doesn't match, got: %s, required: %s", peer_CN,  
                peername);  
    }  
return0;  
}  
   
void json_escape(char*  str)  
{  
int
char    buf[1024];  
   
sizeof(char) +100;  
sizeof(buf));  
   
    strncpy(buf, str, n);  
'\0';  
char
while (*found !='\0')  
    {  
if('\\' == *found ||'"' == *found ||'\n' == *found ||'/'
'\\';  
   
if('\n'
'n';  
   
        *str++ = *found++;         
    }  
   
'\0';  
   
return;  
}  
   
 // Payload example  
   
 // {"aps":{"alert" : "You got your emails.","badge" : 9,"sound" : "default"}}  
int build_payload(char* buffer,int* plen,char* msg,int badage,constchar
{  
int
char buf[2048];  
    char str[2048] ="{\"aps\":{\"alert\":\"";  
   
    n = strlen(str);  
   
if
    {  
        strcpy(buf, msg);  
        json_escape(buf);  
"%s", buf);  
    }  
   
"%s%d","\",\"badge\":", badage);  
   
if
    {  
"%s",",\"sound\":\"");  
        strcpy(buf, sound);  
        json_escape(buf);  
"%s%s", buf,"\"");  
    }  
   
"}}");  
   
    n = strlen(str);  
   
if
    {  
        *plen = n;  
return -1;  
    }  
   
   
if
    {  
        strcpy(buffer, str);  
else
    {  
        strncpy(buffer, str, *plen);  
    }  
   
    *plen = n;  
   
return
}  
int build_output_packet_2(char* buf,int buflen,/* 缓冲区及长度 */
/*消息编号 */
/*过期时间 */
constchar* tokenbinary,/*二进制Token */
char* msg,/* message */
int badage,/* badage */
constchar * sound)/* sound */
{  
1 +4 + 4 + 2 + TOKEN_SIZE + 2
   
char
    // command  
1;  
   
    // messageid  
    pdata++;  
    *(uint32_t*)pdata = messageid;  
   
    // expiry time  
4;  
    *(uint32_t*)pdata = htonl(expiry);  
   
    // token length  
4;  
    *(uint16_t*)pdata = htons(TOKEN_SIZE);  
   
    // token binary  
2;  
    memcpy(pdata, tokenbinary, TOKEN_SIZE);  
   
    pdata += TOKEN_SIZE;  
   
int
if (build_payload(pdata +2, &payloadlen, msg, badage, sound) < 0)   
    {  
'\0';  
        payloadlen = MAX_PAYLOAD_SIZE;  
if (build_payload(pdata +2, &payloadlen, msg, badage, sound) <= 0)   
        {  
return -1;  
        }  
    }  
   
    *(uint16_t*)pdata = htons(payloadlen);  
   
return1 + 4 + 4 +  2 + TOKEN_SIZE +2
}  
   
int send_message_2(SSL *ssl,constchar* token, uint32_t id, uint32_t expire,char* msg,int badage,constchar* sound)  
{  
int
char buf[1 +4 +4 + 2 + TOKEN_SIZE +2
unsignedchar
int buflen =sizeof(buf);  
   
    n = strlen(token);  
    printf("token length : %d, TOKEN_SIZE = %d\n token = %s\n", n, TOKEN_SIZE, token);  
    DeviceToken2Binary(token, n, binary, TOKEN_SIZE);  
   
for (i =0; i < TOKEN_SIZE; i++)  
"%d ", binary[i]);  
"\n");  
   
   
constchar*)binary, msg, badage, sound);  
   
if (buflen <=0) {  
return -1;  
    }  
   
    n = SSL_write(ssl, buf, buflen);  
   
return
} 
 
void sendMessageToALLToken(SSL *ssl,constchar *token,uint32_t id,uint32_t expire,char* msg,int badage,constchar*sound)
{
       
int
    printf("after send_message_2, n = %d\n", n);  
if (n <=0)   
    {  
"send failed: %s\n", ERR_reason_error_string(ERR_get_error()));  
else
    {  
        printf("send sucessfully.\n");  
    }
}
 
void PushMsg(char
{
if(!message)
return;
    init_openssl();
 
    // init Context  
"ck.pem","ck.pem",NULL,NULL);  
if
        fprintf(stderr, "init ssl context failed: %s\n",  
                ERR_reason_error_string(ERR_get_error()));  
return -1;  
    }  
 
    // connect to test Server 
    constchar* host ="gateway.sandbox.push.apple.com";  
constint port =2195;  
int
if (socket <0) {  
        fprintf(stderr, "failed to connect to host %s\n",  
                strerror(errno));  
return -1;  
    }
 
    // SSL connection 
    SSL *ssl = ssl_connect(ctx, socket);  
if
"ssl connect failed: %s\n",  
                ERR_reason_error_string(ERR_get_error()));  
        Closesocket(socket);  
return -1;  
    }
 
NULL) +24 *3600; // expire 1 day  
"main, expire = %d\n", expire); 
 
    //  发送一条消息
    constchar* token ="1b14a89b65361301e7ded0f8de35ec1f322c37698b9fe712252a2cba42207d90";
1,"default");
 
    //  关闭连接
    SSL_shutdown(ssl);  
    Closesocket(socket);  
   
"exit\n");
}
 
 
int main(int argc,char** argv)
{
    char * message ="hello,this is a test message!";
    PushMsg(message,msgid++);
return0;
}
 
  
 php 服务器代码(需要安装php解释器,我的文件名为:pushme.php,直接执行:php pushme.php): 

 <?php
     
    //这里是我们上面得到的deviceToken,直接复制过来(记得去掉空格)
     
    $deviceToken = '1b14a89b65361301e7ded0f8de35ec1f322c37698b9fe712252a2cba42207d90';
     
     
     
    // Put your private key's passphrase here:
     
'1234';
     
     
     
    // Put your alert message here:
     
    $message = 'NVR:192.168.6.128,   通道: 1 产生报警,   日期:2014/7/24!';
     
     
     
    
     
     
     
    $ctx = stream_context_create();
     
'ssl','local_cert','ck.pem');
     
'ssl','passphrase', $passphrase);
     
     
     
    // Open a connection to the APNS server
     
    //这个为正是的发布地址
     
    //$fp = stream_socket_client(“ssl://gateway.push.apple.com:2195“, $err, $errstr, 60, //STREAM_CLIENT_CONNECT, $ctx);
     
    //这个是沙盒测试地址,发布到appstore后记得修改哦
     
    $fp = stream_socket_client(
                                
                               'ssl://gateway.sandbox.push.apple.com:2195', $err,
                                
60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx);
     
     
     
if
     
    exit("Failed to connect: $err $errstr". PHP_EOL);
     
     
     
    echo'Connected to APNS'. PHP_EOL;
     
     
     
    // Create the payload body
     
'aps'] =array(
                          
'alert'
                          
'sound' =>'default',
                          
'badage' =>1
                          
                         );
     
     
     
    // Encode the payload as JSON
     
    $payload = json_encode($body);
     
     
     
    // Build the binary notification
     
0). pack('n',32). pack('H*', $deviceToken). pack('n', strlen($payload)).
     
     
     
    // Send it to the server
     
    $result = fwrite($fp, $msg, strlen($msg));
     
     
     
if
     
    echo'Message not delivered'. PHP_EOL;
     
else
     
    echo'Message successfully delivered'. PHP_EOL;
     
     
     
    // Close the connection to the server
     
    fclose($fp);
     
?>




最后在上面提到了将device_token和推送消息打包的过程,那么,接下来就看看这个信息包结构是怎样的:

ios推送实现 ios消息推送机制_推送

上图显示的这个消息体就是我们的服务器(Provider)发送给APNS服务器的消息结构,APNS验证这个结构正确并提取其中的信息后,再将消息推送到指定的设备。这个结构体包括五个部分,第一个部分是命令标示符,第二个部分是我们的device_token的长度,第三部分是我们的device_token字符串,第四部分是推送消息体(Payload)的长度,最后一部分也就是真正的消息内容了,里面包含了推送消息的基本信息,比如消息内容,应用Icon右上角显示多少数字以及推送消息到达时所播放的声音等。接下来我们拆解看一下Payload(消息体)的结构:

ios推送实现 ios消息推送机制_服务器_02

这其实就是个JSON结构体,alert标签的内容就是会显示在用户手机上的推送信息,badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等,sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音,如果传比如“beep.wav”就会播放在我们应用工程目录下名称为beep.wav的音频文件,比如当手机锁屏时QQ在后台收到新消息时的滴滴声。


有这么一种情况,当我们将应用从设备卸载后,推送的消息改如何处理呢。我们知道,当我们将应用从设备卸载后,我们是收不到Provider给我们推送的消息的,但是,如何让APNS和Provider都知道不去向这台卸载了应用的设备推送消息呢?针对这个问题,苹果也已经帮我们解决了,那就是Feedback service。他是APNS的一部分,APNS会持续的更新Feedback service的列表,当我们的Provider将信息发给APNS推送到我们的设备时,如果这时设备无法将消息推送到指定的应用,就会向APNS服务器报告一个反馈信息,而这个信息就记录在feedback service中。按照这种方式,Provider应该定时的去检测Feedback service的列表,然后删除在自己数据库中记录的存在于反馈列表中的device_token,从而不再向这些设备发送推送信息。连接Feedback service的过程同样使用Socket的方式,连接上后,直接接收由APNS传输给我们的反馈列表,传输完成后断开连接,然后我们根据这个最新的反馈列表在更新我们自己的数据库,删除那些不再需要推送信息的设备的device_token。从Feedback service读取的数据结构如下:

ios推送实现 ios消息推送机制_iOS_03

结构中包含三个部分,第一部分是一个时间戳,记录的是设备失效后的时间信息,第二个部分是device_token的长度,第三部分就是失效的device_token,我们所要获取的就是第三部分,跟我们的数据库进行对比后,删除对应的device_token,下次不再向这些设备发送推送信息。