1、实现provider向APNS推送消息
在通过provider向APNS发送消息之前,我们首先需要了解需要发送的格式,APNS的信息结构包如下图:
上图显示的这个消息体就是我们的服务器(Provider)发送给APNS服务器的消息结构,APNS验证这个结构正确并提取其中的信息后,再将消息推送到指定的设备。这个结构体包括七个部分:
第一个部分是命令标示符;
第二部分是一个表示这个通知的一个表示位,如果apns不能解释该通知,就返回一个错误包;
第三部分是一个以秒标识的时间值,它标识通知是否有效,标识的通知时间与UNIX之差与当前时间与UNIX时间之差,如果大于零,APNS最少通知一次,如果小于同于零APNS不在存储通知(网络字节顺序)。
第四个部分是我们的device_token的长度,
第五部分是我们的device_token字符串,
第六部分是推送消息体(Payload)的长度,
最后一部分也就是真正的消息内容了,里面包含了推送消息的基本信息,比如消息内容,应用Icon右上角显示多少数字以及推送消息到达时所播放的声音等。
接下来我们拆解看一下Payload(消息体)的结构:
这其实就是个JSON结构体,alert标签的内容就是会显示在用户手机上的推送信息,badge显示的数量(注意是整型)是会在应用Icon右上角显示的数量,提示有多少条未读消息等,sound就是当推送信息送达是手机播放的声音,传defalut就标明使用系统默认声音,如果传比如“beep.wav”就会播放在我们应用工程目录下名称为beep.wav的音频文件,比如当手机锁屏时QQ在后台收到新消息时的滴滴声。
provider通过APNS提供的接口,通过Socket进行异步通信,
在Production环境下面通过gateway.push.apple.com,端口:2195
在Development环境下通过gateway.sandbox.push.apple.com 端口:2195
Provider可以建立多个与APNs的连接。每个都得用TLS(or SSL)来建立安全通道,需要用到SSL证书(就是上面提到的provider连接APNs要用到的SSL证书)。
发送消息的二进制接口和消息的数据包格式
消息须是网络字节顺序(即大尾顺序),消息里面的payload部分不可以超过256字节,且不得以'\0'结尾。
有这么一种情况,当我们将应用从设备卸载后,推送的消息改如何处理呢。我们知道,当我们将应用从设备卸载后,我们是收不到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读取的数据结构如下:
结构中包含三个部分,第一部分是一个时间戳,记录的是设备失效后的时间信息,第二个部分是device_token的长度,第三部分就是失效的device_token,我们所要获取的就是第三部分,跟我们的数据库进行对比后,删除对应的device_token,下次不再向这些设备发送推送信息。
The Feedback Service
feedback service包含了这样的列表:某iOS应用程序对应的"设备"("设备"用二进制格式的设备token来标识)。--这些设备是由于各种原因而不能接收APNs发来的消息。
Provider应该定期查询这个列表,并且作出对应处理,如:停止向这些的设备发送消息。
provider访问feedback server通过一个与发送消息类似的二进制接口。
Production环境通过feedback.push.apple.com:2196来建立连接,Development通过feedback.sandbox.push.apple.com:2196。
feedback service和发送消息是不同的服务接口(但都属于APNs),他的连接方式和发送消息是一样的。也要通过证书建立SSL连接,连接后你不需要发送任何命令,直接开始读取流一直读完为止,然后provider要解析读到的数据。
数据是由多个这样的格式组成的:
| 四字节时间 | 2字节的token 长度 | 32字节的设备token |
关 于"四字节时间":Provider需要判断对应设备的这个iOS应用程序有没有在该时间之后重新像provider发送注册推送消息所获得的设备 token。如果没有,就认为该设备失效了,需停止向该设备发送消息。如果有,那就是这个设备失效过,但是现在又有效了,只是feedback service还没来得及刷新列表。
简单通知报文
最前面1个字节是命令类型,2-3字节是DeviceToken的长度,后面是DeviceToken的具体值,后面是消息的长度和消息的具体内容。发完消息后,在关闭Socket前还可以查看错误的响应,以便确认消息是否成功地发给APNS,错误响应的报文如下:
命令为8的表示是错误响应的报文,Status是错误码,Identifier是用于定位具体的哪条推送消息,这个是增强型通知里的传入的值,首先看下苹果都有哪些返回错误码:
增强型通知报文(传入Identifier):
Figure 5-1 Notification format
The first byte in the notification format is a command value of 1. The remaining fields are as follows:
Identifier—An arbitrary value that identifies this notification. This same identifier is returned in a error-response packet if APNs cannot interpret a notification.
Expiry—A fixed UNIX epoch date expressed in seconds (UTC) that identifies when the notification is no longer valid and can be discarded. The expiry value uses network byte order (big endian). If the expiry value is positive, APNs tries to deliver the notification at least once. Specify zero (or a value less than zero) to request that APNs not store the notification at all.
Token length—The length of the device token in network order (that is, big endian)
Device token—The device token in binary form.
Payload length—The length of the payload in network order (that is, big endian). The payload must not exceed 256 bytes and must not be null-terminated.
批量发送
应使用长连接,一次连接发送多条数据
以下部份内容摘自: http://blog.csdn.net/tlq1988/article/details/9612237
$pass = ''; // $pass是你在建立证书的时候输入的密码 $ctx = stream_context_create(); // apns.pem就是你的证书的路径了,最好写绝对路径 stream_context_set_option($ctx, 'ssl', 'local_cert', 'apns.pem'); stream_context_set_option($ctx, 'ssl', 'passphrase', $pass); $fp = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195', $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx); if(!$fp) { print "Failed to connect $err $errstr"; exit(); } else { print "Connection OK\n"; } $body = array('aps' => array('badge' => 1)); for($i = 0; $i <= 10000; $i++) { $deviceToken = md5(time() . rand(0, 9999999)) . md5(time() . rand(0, 9999999)); // 模拟一个Device Token $body['aps']['alert'] = md5(time() . rand(0, 9999999)); // 随便模拟点数据 $payload = json_encode($body); // 这里是简单的消息结构,如果想多发几个但是不要返回错误,可以用这个 /* $msg = chr(0) . pack("n", 32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n", strlen($payload)) . $payload; */ // 这个是增强型消息格式,$i就是Identifier,864000就是Expiry了 $msg = pack('CNNnH*', self::COMMAND_PUSH, $i, 864000, 32, $deviceToken) . pack('n', strlen($payload)) . $payload; print "sending message :" . $payload . "\n"; fwrite($fp, $msg); // 这里是读取错误信息,不要没发一条就读取一次,这样苹果会认为攻击而终止连接 //fread($fp, 6); } |
Broken Pipe,如果你有过大量的数据推送,并且看下你的错误日志那么Writen Broken Pipe你一定不陌生。这个错误产生的原因通常是当管道读端没有在读,而管道的写端继续有线程在写,就会造成管道中断。可以简单的理解为你在向一个已经关闭的连接写数据就会抛出这个错误。
由于Broken
Pipe的关系,我们不得不重新和苹果服务器建立连接,这个连接耗时在国内.....(你们懂的3sec+),这个应该是我们推送速度最大的瓶颈了。有很
多开发者也许会认为这个是由于国内的网络环境导致,因为他们习惯的“traceroute
gateway.push.apple.com”一下,然后发现30+的路由跳转然后就会说这个断开是无法避免的。如果你这么想那么你就错了。
我们用大量(10W左右)能保证基本正确的Device Token来做测试,平均一次连接的能写入3W左右的数据,好的情况下能一次写完这10W数据!!!这个测试也就证明平凡出现Broken Pipe不是由于网络原因。既然不是由于网络原因,那么我做个大胆的假设:这个连接是由APNs主动断开的。
那么假设这个猜想是正确的,那苹果什么时候会断开连接了?解释这个问题,我们又做了一个测试:往这10W的Device
Token里面均匀插入1000个错误的Device
Token。神奇的事情发生了,发送期间平均断开连接900次+。这个实验正好验证了我之前的猜测:产生Broken Pipe是因为APNs服务器主动断开了连接,并且是由于错误的Device Token引起的(或者其他的错误)。苹果的错误类型和代码编号:
Status code |
Description |
---|---|
0 |
No errors encountered |
1 |
Processing error |
2 |
Missing device token |
3 |
Missing topic |
4 |
Missing payload |
5 |
Invalid token size |
6 |
Invalid topic size |
7 |
Invalid payload size |
8 |
Invalid token |
10 |
Shutdown |
255 |
None (unknown) |
我们进一步跟进测试,我们发现一个奇怪的现象,断开连接的时的前一个Token并不是我们所特意设置的错误Token。同时我们也发现消息送达率也变得非常的低(偶尔有设备能收到)。这个很好解释,之前我就有文章提到过(官方也有相应说明)当一次连接先发送一个错误的Token,之后的有效Token的消息是无法送达的(http://blog.csdn.net/hjq_tlq/article/details/8131115),这就导致了错误的Token后面的正确的Token全部没有收到,从而送达率也就明显下降了。
经过上面的测试,当APNs接收到错误的Token的时候会主动断开连接,但是断开连接之前会有1sec左右的延迟。那么你可以有下面这个例子理解:
你要发送1000条数据并且第20个Token是错误的
当此次连接发到第20个Token的时候苹果认为此次连接终止(但是连接并没有断开,只是APNs将抛弃之后的内容),并且不处理此次连接之后的消息
1sec左右的时间之后苹果主动断开SSL连接,如果你继续忘此连接写数据,你将可以捕捉到Broken Pipe错误
此时由于1sec左右的延迟,你已经发送到了第123个消息
此时从20以后直至123的消息将全部没有送达
太可怕了.....你竟然不知道是从哪一个错了!!!苹果是SB啊!先不要做这样的结论,我们先看一下苹果官方文档所给出的东东:
-
Identifier—An arbitrary value that identifies this notification. This same identifier is returned in a error-response packet
if APNs cannot interpret a notification. -
Expiry—A
fixed UNIX epoch date expressed in seconds (UTC) that identifies when
the notification is no longer valid and can be discarded. The expiry
value
uses network byte order (big endian). If the expiry value is positive,
APNs tries to deliver the notification at least once. Specify zero (or a
value less than zero) to request that APNs not store the notification
at all. -
Token length—The length of the device token in network order (that is, big endian)
-
Device token—The device token in binary form.
-
Payload length—The length of the payload in network order (that is, big endian). The payload must not exceed 256 bytes and must not be null-terminated.
-
Payload—The notification payload.
PS:这里苹果到是做了件好事,这个消息结构在早些的文档中显示的是5-2 Enhanced Notification
Format,而之前的5-1是Notification
Format。区别在于之前的5-1中的消息结构为简单消息结构,没有Identifier和Expiry字段并且Command为0。现在直接把简单的
消息体结构给去掉了,这样可以强制开发者加上Identifier,从而得到返回值。
为了方便我直接把官方文档粘过来了哈:)我们需要注意的是Identifier这个东东。没错,这个就是苹果用来提供的给第三方的4唯一标示,如果鸟语不
是很好的话他后面的那个注释大致就是说:一个消息的唯一标识。如果苹果服务器不能解释这个消息,那么将在错误中返回这个唯一标示。
可恶的苹果并没有说明这个会有延迟,以及怎么确保我们能收到这个错误。我们现在采用的是每发送100条消息,就检查一下(read)是否有失败的。如果你抓到这个错误,那么果断断开连接,并且重新发送这条错误以后的Token,这样就能保证消息基本能送达。
哦,顺便说一下如何得到错误反馈,如果你发送的时候加上了Identifier,那么此时你一定有一个和APNs的连接吧(废话,没连接怎么write),那么你只要read就好了,如果有就能读到一个二进制数据:)
有一个也需要提一下,就是APNs的FeedBack功能也一定要用上,这个能帮助你更好的剔除错误的Token。
当你的Token基本为正确的时候,如果还有大量的Broken Pipe出现,你可以给我留言,我们一起研究到底哪里出问题了:)
附: http://www.oschina.net/translate/http2-protocol-for-apns?cmp
其它:
2015年12月17日起,发布 “基于 HTTP/2 的全新 APNs 协议”,iOS 系统以及 OS X 系统,统一将最大
playload 大小提升到4KB。参考文档:Apple Push Notification Service Update 12-17 2015
新旧 APNs 协议工作示意图对比
HTTP / 2
Version Old / socket connect
参考:
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW2
APNS架构图: