基于Codeigniter框架实现的APNS批量推送—叮咚,查水表
最近兼职公司已经众筹成功的无线门铃的消息推送出现了问题,导致有些用户接收不到推送的消息,真是吓死宝宝了,毕竟自己一手包办的后台服务,影响公司信誉是多么的尴尬,容我简单介绍一下我们的需求:公司开发的是一款无线门铃系统,如果有人在门外按了门铃开关,门铃开关会发射一个信号,屋里的接收网关接收到信号会发出响声,同时也会推送一条消息到用户手机,即使这个手机是远程的,也就是主人不在家也知道有人按了家里的门铃。这里后台需要解决的问题是搭建APNS推送的Provider,因为要想把消息推送到苹果手机,按照苹果公司设计的机制,必须通过自己的服务器推送到苹果的PUSH服务器,再由它推送到手机,每个手机对应一个deviceToken,我这里介绍的重点并不是这个平台怎么搭建,这个国内网上的教程已经相当丰富了。比如你可以参考:一步一步教你做ios推送

网上的教程大多是走的通的,但是他们操作的对象是一个手机,我的意思是它们是一次给一个手机终端推送消息,在我们公司设计的产品中,同一个账户可以在多个手机上登录(理论上是无数个,因为我在后台并没有限制),每个手机对应的deviceToken是不同的,另外公司的产品还设计了分享功能,也就是主用户可以把设备分享给其他用户,而其他用户也有可能在不同设备上同时登录,如果有人按了门铃要向所有已经登录的用户包括分享的用户推送消息,也就是要批量推送到很多个手机终端。当然我这里举的例子并不会有这么复杂,所有的问题抽象出来其实就是一个问题:给你一个存储deviceToken的数组,APNS如何批量推送给多个用户?
首先我们设计一个数据库,用来存储用户的推送令牌(deviceToken),为简单起见,这个表就两个字段。
| client_id | deviceToken |
| 1 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX |
这里我使用的是CodeIgniter3的框架,我们新建一个Model,来管理用户deviceToken数据。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
<?php// ios推送令牌管理class Apns_model extends CI_Model{ public function __construct() { $this->load->database(); } /** * 新建推送令牌 * create a apns如果已经存在就更新这个deviceToken * $data is an array organized by controller */ public function create($data) { if($this->db->replace('tb_apns', $data)) { return TRUE; } else { return FALSE; } } //删除某个用户的推送令牌 public function delete($user_id) { if(isset($user_id)){ $result=$this->db->delete('tb_apns', array('client_id' => $user_id)); return TRUE; }else{ return FALSE; } } //根据推送令牌删除推送令牌 public function deletebytoken($token) { if (isset($token)) { $result=$this->db->delete('tb_apns', array('deviceToken'=>$token)); return TRUE; }else{ return FALSE; } } //查询某个用户的iso推送令牌 public function get($client_id) { $sql = "SELECT deviceToken FROM `tb_apns` WHERE `client_id`='$client_id'"; $result = $this->db->query($sql); if ($result->num_rows()>0) { return $result->result_array(); } else { return FALSE; } }} |
在我后台的第一个版本中,按照网上的教程,大多是一次给一个终端推送消息的,我稍微改了一下,将所有取得的deviceToken存在$deviceTokens数组中,参数$message是需要推送的消息,使用for循环依次从数组中取出一个deviceToken来推送,然后计数,如果所有的推送成功则返回true。这个方法看似是没有任何破绽的,而且也测试成功了,所以我就直接上线了,(主要是我也没想到公司会突然出这样一个产品,把推送功能的地位抬得很高,我一直以为是可有可无的)。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
function _send_apns($deviceTokens,$message) { // Put your private key's passphrase here:密语 $passphrase = 'xxxxx'; //////////////////////////////////////////////////////////////////////////////// $ctx = stream_context_create(); stream_context_set_option($ctx, 'ssl', 'local_cert', 'xxxx.pem'); stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase); // Open a connection to the APNS server $fp = stream_socket_client( $errstr, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx); if (!$fp) exit("Failed to connect: $err $errstr" . PHP_EOL); echo 'Connected to APNS' . PHP_EOL; // Create the payload body $body['aps'] = array( 'alert' => $message, 'sound' => 'default' ); // Encode the payload as JSON $payload = json_encode($body); $num=count($deviceTokens); $countOK=0;//统计发送成功的条数 for($i=0;$i<$num;$i++) { $deviceToken=$deviceTokens[$i]; $deviceToken=preg_replace("/\s/","",$deviceToken);//删除deviceToken里的空格 // Build the binary notification $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload; // Send it to the server $result = fwrite($fp, $msg, strlen($msg)); if ($result) { $countOK++; } } // Close the connection to the server fclose($fp); if($countOK==$num) return TRUE; else return FALSE; } |
就是上面的代码导致了后来推送出现了一系列问题。
第一个大问题是:这里默认了所有的推送令牌都是有效的,而实际上,如果用户直接删除了app或者app升级都有可能造成后台数据库里的deviceToken没有发生更新,从而使推送令牌失效。但是有人按了门铃,后台还是会把它当成有效的deviceToken纳入到$deviceTokens中,如何清除失效过期的deviceToken是个必须考虑的问题。
查阅相关资料发现APNS服务有一个The Feedback Service的服务,国内的博客基本上忽略了这个环节,很少有资料提及,还是谷歌找个官方网站比较靠谱。下面简要介绍一下这个服务:
在进行APNS远程推送时,如果由于用户卸载了app而导致推送失败,APNS服务器会记录下这个deviceToken,加入到一个列表中,可以通过查询这个列表,获取失效的推送令牌,从数据库中清除这些失效的令牌就可以避免下次推送时被加入到推送数组中来。连接这项服务很简单和推送工程类似,只不过地址不同,开发环境为feedback.push.apple.com ,测试环境为feedback.sandbox.push.apple.com端口都是2196。APNS服务器返回的数据格式为:

|
Timestamp |
A timestamp (as a four-byte |
|
Token length |
The length of the device token as a two-byte integer value in network order. |
|
Device token |
The device token in binary format. |
为了进行这项服务,我写了一个CI框架的控制器
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
<?phpdefined('BASEPATH') OR exit('No direct script access allowed');class Admin extends CI_Controller { public function __construct() { parent::__construct(); // 加载数据库 $this->load->database(); $this->load->model('apns_model'); } public function apnsfeedback() { $ctx = stream_context_create(); $passphrase = 'xxxxx'; stream_context_set_option($ctx, 'ssl', 'local_cert', 'xxxxxxx.pem'); stream_context_set_option($ctx, 'ssl', 'passphrase', $passphrase); $fp = stream_socket_client('ssl://feedback.push.apple.com:2196', $error, $errorString, 60, STREAM_CLIENT_CONNECT|STREAM_CLIENT_PERSISTENT, $ctx); if (!$fp) { echo "Failed to connect feedback server: $err $errstr\n"; return; } else { echo "Connection to feedback server OK\n"; echo "<br>"; } while ($devcon = fread($fp, 38)) { $arr = unpack("H*", $devcon); $rawhex = trim(implode("", $arr)); // $feedbackTime = hexdec(substr($rawhex, 0, 8)); // $feedbackDate = date('Y-m-d H:i', $feedbackTime); // $feedbackLen = hexdec(substr($rawhex, 8, 4)); $feedbackDeviceToken = substr($rawhex, 12, 64); if (!empty($feedbackDeviceToken)) { echo "Invalid token $feedbackDeviceToken\n"; echo "<br>"; $this->apns_model->deletebytoken($feedbackDeviceToken); } } fclose($fp); }} |
通过循环读取38个字节的数据,可以查询出所有失效的令牌并在数据库中删除。
不过需要注意的是:一旦你读取了数据,APNS就会清除已有的列表,下次查询时返回的是自从上次查询后再次累积的无效的令牌。
The feedback service’s list is cleared after you read it. Each time you connect to the feedback service, the information it returns lists only the failures that have happened since you last connected.
还有一点就是你胡乱造的deviceToken是不会被服务器记录的。
第二个问题:假如$deviceTokens数组里有很多个元素,有时会发生前面几个令牌推送成功,手机收到了消息,但是后面的令牌没有推送成功,没有收到消息。
关于这个问题,国内的博客也是很少提及,直到在官网上看到下面的几句话:
If you send a notification that is accepted by APNs, nothing is returned.
If you send a notification that is malformed or otherwise unintelligible, APNs returns an error-response packet and closes the connection. Any notifications that you sent after the malformed notification using the same connection are discarded, and must be resent.
上面几句话的意思大致是:每次针对一个deviceToken推送消息时,如果推送失败,没有任何数据返回,如果apns服务器不能识别推送的令牌APNS会返回一个错误消息并关闭当前的连接,所有后续通过同一连接推送的消息都会放弃,必须重新连接跳过无效的再发送。这也解释了为什么后面的推送会失败。下图是返回的数据及对应的错误码,这里需要重点关注错误码8,其对应这无效的令牌。
Codes in error-response packet 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
128
Protocol error (APNs could not parse the notification)
255
None (unknown)
那么如何让APNS服务器返回错误消息呢,实际上我之前的第一种解决方案中,APNS并不会返回错误消息,我只是一厢情愿的在统计发送成功次数,如果需要APNS返回错误消息,需要改变发送数据的格式。发送数据的格式同样推荐参考官方文档,因为国内的博客基本上没好好严格按照文档来打包数据。下面的格式介绍来自官方文档。
Figure A-1 Notification format
Note: All data is specified in network order, that is big endian.
The top level of the notification format is made up of the following, in order:
Table A-1 Top-level fields for remote notifications Field name
Length
Discussion
Command
1 byte
Populate with the number
2.Frame length
4 bytes
The size of the frame data.
Frame data
variable length
The frame contains the body, structured as a series of items.
The frame data is made up of a series of items. Each item is made up of the following, in order:
Table A-2 Fields for remote notification frames Field name
Length
Discussion
Item ID
1 byte
The item identifier, as listed in Table A-3. For example, the item identifier of the payload is
2.Item data length
2 bytes
The size of the item data.
Item data
variable length
The value for the item.
The items and their identifiers are as follows:
Table A-3 Item identifiers for remote notifications Item ID
Item Name
Length
Data
1
Device token
32 bytes
The device token in binary form, as was registered by the device.
A remote notification must have exactly one device token.
2
Payload
variable length, less than or equal to 2 kilobytes
The JSON-formatted payload. A remote notification must have exactly one payload.
The payload must not be null-terminated.
3
Notification identifier
4 bytes
An arbitrary, opaque value that identifies this notification. This identifier is used for reporting errors to your server.
Although this item is not required, you should include it to allow APNs to restart the sending of notifications upon encountering an error.
4
Expiration date
4 bytes
A UNIX epoch date expressed in seconds (UTC) that identifies when the notification is no longer valid and can be discarded.
If this value is non-zero, APNs stores the notification tries to deliver the notification at least once. Specify zero to indicate that the notification expires immediately and that APNs should not store the notification at all.
5
Priority
1 byte
The notification’s priority. Provide one of the following values:
10The push message is sent immediately.The remote notification must trigger an alert, sound, or badge on the device. It is an error to use this priority for a push that contains only the
content-availablekey.
5The push message is sent at a time that conserves power on the device receiving it.Notifications with this priority might be grouped and delivered in bursts. They are throttled, and in some cases are not delivered.
注意上面我标红的几句话,大意是说这个字段作为此次推送消息唯一的标识符,有了这个标识符,APNS就能向我们的服务器报告错误。这也解释了为什么我上面的解决方案没有返回错误信息。下面是新的推送数据打包方式。这里我直接以发送的数组下标$i作为标识符,更优化的方法是使用deviceToken在数据库中对应的id作为唯一标识符。
1$msg= pack("C", 1) . pack("N",$i) . pack("N",$apple_expiry) . pack("n", 32) . pack('H*',str_replace(' ','',$deviceToken)) . pack("n",strlen($payload)) .$payload;如果批量推送中途有一个推送失败,连接会被关闭,需要重新连接推送后面的令牌,所以最好把建立连接的过程封装成一个方法。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
private function create_apns_link(){ $scc = stream_context_create(); stream_context_set_option($scc, 'ssl', 'local_cert', realpath($this->certificate)); stream_context_set_option($scc, 'ssl', 'passphrase', $this->passphrase); $fp = stream_socket_client($this->link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc); $i = 0; while (gettype($fp) != 'resource'){//如果建立失败就每隔100ms重新建立一次,如果失败3次就放弃 if($i < 3){ usleep(100000); $fp = stream_socket_client($link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc); $i++; }else{ break; } } stream_set_blocking ($fp, 0); if($fp){ echo 'Connected to APNS for Push Notification'; } return $fp; } |
至此,我把前面讲到的推送过程封装到一个CodeIgniter类库中
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
<?phpdefined('BASEPATH') or exit('No direct script access allowed');class Apns{ private $certificate="xxxx.pem"; private $passphrase="xxxx"; protected $CI; public function __construct() { $this->CI=&get_instance(); // 加载数据库 $this->CI->load->database(); $this->CI->load->model('apns_model'); } public function send($deviceTokens,$data){ $fp = $this->create_apns_link(); $body['aps'] = $data; $apple_expiry = 0; $payload = json_encode($body); $num=count($deviceTokens); for ($i=0; $i <$num ; $i++) { $deviceToken=$deviceTokens[$i]; $msg = pack("C", 1) . pack("N", $i) . pack("N", $apple_expiry) . pack("n", 32) . pack('H*', str_replace(' ', '',$deviceToken)) . pack("n", strlen($payload)) . $payload; $rtn=fwrite($fp, $msg); usleep(400000);//每次推送过后,因为php是同步的apns服务器不会立即返回错误消息,所以这里等待400ms $errorcode=$this->checkAppleErrorResponse($fp);//检查是否存在错误消息 if($errorcode){ if ($errorcode=='8') {//如果令牌无效就删除 $this->CI->apns_model->deletebytoken($deviceToken); } if($i<$num-1){//如果还没推送完,需要重新建立连接推送后面的 $fp = $this->create_apns_link(); } } } fclose($fp); return true; } private function create_apns_link(){ $scc = stream_context_create(); stream_context_set_option($scc, 'ssl', 'local_cert', realpath($this->certificate)); stream_context_set_option($scc, 'ssl', 'passphrase', $this->passphrase); $fp = stream_socket_client($this->link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc); $i = 0; while (gettype($fp) != 'resource'){//如果建立失败就每隔100ms重新建立一次,如果失败3次就放弃 if($i < 3){ usleep(100000); $fp = stream_socket_client($link, $err,$errstr, 60, STREAM_CLIENT_CONNECT, $scc); $i++; }else{ break; } } stream_set_blocking ($fp, 0); if($fp){ echo 'Connected to APNS for Push Notification'; } return $fp; } private function checkAppleErrorResponse($fp) { //byte1=always 8, byte2=StatusCode, bytes3,4,5,6=identifier(rowID). // Should return nothing if OK. //NOTE: Make sure you set stream_set_blocking($fp, 0) or else fread will pause your script and wait // forever when there is no response to be sent. $apple_error_response = fread($fp, 6); if ($apple_error_response) { // unpack the error response (first byte 'command" should always be 8) $error_response = unpack('Ccommand/Cstatus_code/Nidentifier', $apple_error_response); return $error_response['status_code']; } return false; } } |
总结:官方文档才是王道啊,英语好才是王道啊!!!!
参考
Binary Provider API
APNS SSL operation failed with code 1
基于Codeigniter框架实现的APNS批量推送—叮咚,查水表的更多相关文章
- 基于C++ 苹果apns消息推送实现(2)
1.该模块的用途C++ 和 Openssl 代码 它实现了一个简单的apns顾客 2.配套文件:基于boost 的苹果apns消息推送实现(1) 3.最初使用的sslv23/sslv2/sslv3仅仅 ...
- ZH奶酪:基于ionic.io平台的ionic消息推送功能实现
Hybrid App越来越火,Ionic的框架也逐渐被更多的人熟知. 在mobile app中,消息推送是很必要的一个功能. 国内很多ionic应用的推送都是用的极光推送,最近研究了一下Ionic自己 ...
- 【经验】ansible 批量推送公钥
1.使用 ssh-keygen -t rsa生成密钥对 ssh-keygen -t rsa 2.推送单个公钥到远程机器 格式: ssh-copy-id -i ~/.ssh/id_rsa.pub use ...
- 自动化运维工具ansible学习+使用ansible批量推送公钥到远程主机
目录: 一.ansible简介 1.1.ansible是什么 1.2.ansible如何工作 1.3.ansible优缺点 1.4.ansible安装方式 1.5.ansible文件简单介绍 1.6. ...
- 移动互联网实战--Apple的APNS桩推送服务的实现(2)
前记: 相信大家在搞IOS推送服务的开发时, 会直接使用javapns api来简单实现, 调试也直连Apple的APNS服务(产品/测试版)来实现. 很少有人会写个APNS的桩服务, 事实也是如此. ...
- oc学习之路----APNS消息推送从证书到代码(2015年4月26号亲试可用)
前言:看这篇博客之前要准备:首先的有一个99刀的个人开发者账号或者199刀的企业开发者账号,其次你用的是apns消息推送,(本人之前四处打听有没有其他消息推送的方法:收获如下:首先如果想做到apns的 ...
- 最简单的基于FFmpeg的推流器(以推送RTMP为例)
===================================================== 最简单的基于FFmpeg的推流器系列文章列表: <最简单的基于FFmpeg的推流器(以 ...
- 最简单的基于Flash的流媒体示例:RTMP推送和接收(ActionScript)
===================================================== Flash流媒体文章列表: 最简单的基于Flash的流媒体示例:RTMP推送和接收(Acti ...
- shell脚本批量推送公钥
目的:新建管理机,为了实现批量管理主机,设置密匙登陆 原理:.通过密钥登陆,可以不用密码 操作过程: 1.生成密匙 ssh-keygen 2.查看密匙 ls ~/.ssh/ 有私匙id_rsa公匙 ...
随机推荐
- Web jquery表格组件 JQGrid 的使用 - 从入门到精通 开篇及索引
因为内容比较多,所以每篇讲解一些内容,最后会放出全部代码,可以参考.操作中总会遇到各式各样的问题,个人对部分问题的研究在最后一篇 问题研究 里.欢迎大家探讨学习. 代码都经过个人测试,但仍可能有各种未 ...
- React项目(二):生命游戏
引子 这是16年最后的一个练手项目,一贯的感觉就是,做项目容易,写说明文档难.更何况是一个唤起抑郁感觉的项目,码下的每个字,心就如加了一个千斤的砝码. 2016年,有些事我都已忘记,但我现在还记得.2 ...
- Linux下按程序查实时流量 network traffic
实然看到下载速度多达几M/s,但实际上并没有什么占用带宽的进程. 相查看每个程序占用的网络流量, 但系统自带的 System Monitor 只能查看全局的流量, 不能具体看某个程序的...... k ...
- 用Canvas实现动画效果
1.清除Canvas的内容 clearRect(x,y,width,height)函数用于清除图像中指定矩形区域的内容 <!doctype html> <html> <h ...
- iPhone屏幕尺寸/launch尺寸/icon尺寸
屏幕尺寸 6p/6sp 414 X 736 6/6s 375 X 667 5/5s 320 X 568 4/4s 320 X 480 la ...
- linux下如何关闭防火墙?如何查看防火墙当前的状态
从配置菜单关闭防火墙是不起作用的,索性在安装的时候就不要装防火墙查看防火墙状态:/etc/init.d/iptables status暂时关闭防火墙:/etc/init.d/iptables stop ...
- 使用Grunt构建自动化开发环境
1.准备工作 1)首页确保电脑上网,以及能够访问https://registry.npmjs.org/,因需从此网站中下载安装相应的插件; 2)电脑安装Node.js,Grunt及Grunt插件都是基 ...
- mac php环境启动
mac 环境下,用brew安装php相关环境启动命令 说明 这里php,mysql,nginx都是用brew安装,安装目录默认,在Cellar下面 php-fpm 带配置重启 /*注意权限,加 sud ...
- web自动化工具-Browsersync
web自动化工具-Browsersync browser-sync才是神器中的神器,和livereload一样支持监听所有文件.可是和livereload简单粗暴的F5刷新相比,browsersync ...
- secureCRT The remote system refused the connection.
转 http://blog.csdn.net/lifengxun20121019/article/details/13627757 我在实践远程登录工具SecureCRT的时候遇到了这个问题 Ubun ...

