Tuesday, June 10, 2014

APNS导致消息丢失和发送效率原因

首先说明一下,本文只是介绍一些容易被开发者忽视,而导致性能低下问题。并不是介绍如何向苹果设备成功发送一条消息,这里假设所有阅读者已经能够向苹果服务器发送消息,并且成功接收,只是发送效率比较低,并且丢失率很高。如果你不是此类情况,那么绕道吧。PS:伸手党可以直接看标红部分(结论)
    最近参与并且完成了公司1000W级的消息推送服务平台重建。此次重构级别解决了消息丢失,并且大幅度提升了推送效率。有些东西我想很多开发者也会碰到,并且难以被开发者所意识到。
    先先扫下盲哈。如果你发送消息是一次连接发送一条,那么请你先改成长连接发送--一次连接发送多条数据。粘下PHP代码吧:)
  1. $pass = ''// $pass是你在建立证书的时候输入的密码  
  2. $ctx = stream_context_create();  
  3. // apns.pem就是你的证书的路径了,最好写绝对路径  
  4. stream_context_set_option($ctx'ssl''local_cert''apns.pem');  
  5. stream_context_set_option($ctx'ssl''passphrase'$pass);  
  6. $fp = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195'$err$errstr, 60, STREAM_CLIENT_CONNECT, $ctx);  
  7. if(!$fp) {  
  8.     print "Failed to connect $err $errstr";  
  9.     exit();  
  10. else {  
  11.     print "Connection OK\n";  
  12. }  
  13. $body = array('aps' => array('badge' => 1));  
  14. for($i = 0; $i <= 10000; $i++) {  
  15.     $deviceToken = md5(time() . rand(0, 9999999)) . md5(time() . rand(0, 9999999)); // 模拟一个Device Token  
  16.     $body['aps']['alert'] = md5(time() . rand(0, 9999999)); // 随便模拟点数据  
  17.     $payload = json_encode($body);  
  18.     // 这里是简单的消息结构,如果想多发几个但是不要返回错误,可以用这个  
  19.     /* 
  20.     $msg = chr(0) . pack("n", 32) 
  21.         . pack('H*', str_replace(' ', '', $deviceToken)) 
  22.         . pack("n", strlen($payload)) . $payload; 
  23.     */  
  24.     // 这个是增强型消息格式,$i就是Identifier,864000就是Expiry了  
  25.     $msg = pack('CNNnH*', self::COMMAND_PUSH, $i, 864000, 32, $deviceToken)  
  26.         . pack('n'strlen($payload))  
  27.         . $payload;  
  28.     print "sending message :" . $payload . "\n";  
  29.     fwrite($fp$msg);  
  30.     // 这里是读取错误信息,不要没发一条就读取一次,这样苹果会认为攻击而终止连接  
  31.     //fread($fp, 6);  
  32. }  
    要往下面说我先解释一下这个东东--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啊!先不要做这样的结论,我们先看一下苹果官方文档所给出的东东:
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.
  • 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出现,你可以给我留言,我们一起研究到底哪里出问题了:)
    附录:苹果推送官方文档
更多0
主题推荐
apns开发者服务器processing二进制
猜你在找
golang的apns证书文件转换(P12 to Pem)
VIM颜色设置(evening, desert, morning....)
eclipse中出现No Default Proposals即编写代码时无法自动补全(智能提示)的问题
扯谈下XA事务
UILabel自适应高度和自动换行
IOS消息推送相关介绍
IOS7 AVAudioRecorder 不能录音的解决方法
关于模态窗口错误 Application tried to present modally an active controller
AddressBookUI.Framwork应用之ABPersonViewController, ABUnknownPersonViewController,ABNewPersonViewContro
学习心得:关于C#中Queue的线程安全问题
查看评论
5楼 songpingyi2009 2013-12-31 13:46发表 [回复]
捕获apns连接是否关闭是一直读取不到数据
我是用C#写的
byte[] BackFuer = new byte[6];
apnsStream.ReadTimeout = 2000;
int result = apnsStream.Read(BackFuer, 0, BackFuer.Length);

如果不加 apnsStream.ReadTimeout = 2000;这个是卡死状态
Re: hjq_tlq 2014-01-16 10:36发表 [回复]
回复songpingyi2009:读不到这个东东和麻烦的,得看苹果心情了:)
哈哈!
其实也不是啦,你注意看上面苹果返回的值,只有错误信息,成功的话就不会返回任何值的,还有一个就是,你发送的东东确定了错误的???
不过这个值很难抓到,比如说你抓这个值的时候苹果还没有返回,但是等你下次抓这个值的时候连接已经断开了.....
Re: songpingyi2009 2014-01-23 09:26发表 [回复]
不知楼主的处理方式和频率是什么样的,我们这边单独开一个线程去每2-3秒读一次,日志记录有时也能读到,但感觉效果不理想。
Re: hjq_tlq 2014-01-27 16:15发表 [回复]
回复songpingyi2009:能读到一些就不错了,主要好事靠feedback接口吧
4楼 laizhaolin 2013-11-12 13:40发表 [回复]
用一个程序测试发送错误的token到苹果服务器,用另一个程序测试读取feedback列表,读取到的返回值总是0,请问这是怎么回事?(连接的都是产品版的服务器,这个可以确实没有问题。)
Re: laizhaolin 2013-11-12 15:20发表 [回复]
回复laizhaolin:已解决。
1)当token是错误的,或者格式正确,但是由测试者自己编造的token,feedback列表中不会有数据。
2)当存在token,其曾经是某设备的,但因为该设备删除对应软件后,苹果服务器没有收到设备回馈信息后,这个token会被列入feedback列表中。
Re: hjq_tlq 2013-11-14 18:29发表 [回复]
回复laizhaolin:“当token是错误的,或者格式正确,但是由测试者自己编造的token,feedback列表中不会有数据”这个之前真没有测试过,谢谢咯:)
最近又要给做一个新的消息中心.......我是跟苹果干上了:)
3楼 laizhaolin 2013-11-05 17:28发表 [回复]
急@!!!请问一下,
1)C++写的代码,存在错误token时,每发送25条消息后关闭连接,然后重连,几次后 SSL_shutdown(m_pssl); /*其中 SSL *m_pssl;*/就出现broken pipe了,怎么会这样?
2)APNs的FeedBack的C++代码网上都没有看到,您有参考代码吗?
3)我用的消息格式是简单的消息体结构,没有加Identifier和Expiry字段
Re: hjq_tlq 2013-11-08 18:21发表 [回复]
回复laizhaolin:不好意思过这么多天才回复你的消息。
1、出现broken pipe这样的错误有几个原因,最常见的就是错误的Tokend导致的了,还有就是我上面列出来的那几种错误了(苹果的错误类型和代码编号),这么平凡的出现broken pipe就建议你检查一下你的消息体是否符合苹果的要求了,因为错误的消息体(例如:没有传入token或者token的格式错误、没有按照苹果要求封装消息)也会导致苹果断开连接。
2、FeedBack的C++代码我真没有,不过FeedBack的原理和Push差不多,也是跟苹果建立一个ssl连接,然后去读取东西就好了
3、最好改成增强型的消息结构,因为简单消息结构已经在苹果官网不见了很久了,不知道还会不会继续支持。
希望对你有帮助:)
2楼 nianshaoqingkuang_80 2013-10-21 16:36发表 [回复]
捕获apns连接是否关闭是fread($fp,6)吗?我是用php写的,使用fread的话网页好久没有反应,请问你是怎么获知apns是否停止了读操作
Re: hjq_tlq 2013-11-04 16:35发表 [回复]
回复nianshaoqingkuang_80:就是用的fread($fp,6)啊,这个我试了,不会导致页面卡死啊。
我最慢的一次fread($fp,6)也就1.3秒啊。
还有你有什么业务需要在页面展示这个啊?
1楼 yangqisheng 2013-09-26 17:19发表 [回复] [引用] [举报]
楼主,有个几个问题和你讨论下。你说“每发送100条消息,就检查一下(read)是否有失败的消息。”
1. 你发送完100条消息(其中有错误的token),如果马上read的话,多半是读不到error-response,因为这个错误返回是有时间的,具体多少,有人测过200-300ms。如果我们严格处理出错情况的话,一个链接两次发送数据的时间间隔必须大于300ms。这样效率就会大大降低。不知楼主怎么解决这个效率问题。
2. 如果APNS已经关闭一个ssl,而程序却仍然往里写了数据,接着read到了0,再SSL_shutdown(ssl)时,会Broken pipe。我在程序里signal(SIGPIPE, SIG_IGN)了,程序依然挂。

希望能与楼主进一步讨论。谢谢!我的邮箱yangqisheng2004@gmail.com
Re: hjq_tlq 2013-09-27 14:30发表 [回复]
回复yangqisheng:哦,刚想起来了。我之前写的由于赶时间,没有用的多线程。关于你的第二个问题,你可以再起一个线程,用来read啊:)
如果读到错误值,那么就关闭连接并且给发送线程一个信号(就用Identifier就好了)。
PS:这里你read的时候要注意咯。因为你在read的时候,如果过于频繁,那么也会被苹果给断开了。他会认为你是攻击,不过不要担心,只是警告而已。我们测试过,连续3天的高频率read,并没有导致封IP或者其他问题。时间久了就不知道咯:)
Re: hjq_tlq 2013-09-27 14:22发表 [回复] [引用] [举报]
回复yangqisheng:这个没不好弄啊。多少条读read一次完全是一个经验值。看你们的业务了,如果是推送消息(类似广告那样的)我个人感觉没必要。如果是做类似IM的话那在应用内不建议用这个东东,因为实在无解。
关于两条消息间隔必须大于300ms,这个完全没必要的,你只要确保你能捕获到苹果的的错误消息,那么你就可以重试啊。
你是用什么写的?跑出来broken pipe会导致程序挂掉?你没有捕获这个异常么(或者忽略错误)?至少我用php和python以及java都是有办法捕获这个异常,然后重连的。