最近兼职公司已经众筹成功的无线门铃的消息推送出现了问题,导致有些用户接收不到推送的消息,真是吓死宝宝了,毕竟自己一手包办的后台服务,影响公司信誉是多么的尴尬,容我简单介绍一下我们的需求:公司开发的是一款无线门铃系统,如果有人在门外按了门铃开关,门铃开关会发射一个信号,屋里的接收网关接收到信号会发出响声,同时也会推送一条消息到用户手机,即使这个手机是远程的,也就是主人不在家也知道有人按了家里的门铃。这里后台需要解决的问题是搭建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(
'ssl://gateway.push.apple.com:2195' , $err ,
$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 );
<code class="php variable">$num= count ( $deviceTokens );
$countOK =0; //统计发送成功的条数
for ( $i =0; $i <code class="php variable">$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 == <code class="php variable">$num)
return TRUE;
else
return FALSE;
}
|
就是上面的代码导致了后来推送出现了一系列问题。
第一个大问题是:这里默认了所有的推送令牌都是有效的,而实际上,如果用户直接删除了app或者app升级都有可能造成后台数据库里的deviceToken没有发生更新,从而使推送令牌失效。但是有人按了门铃,后台还是会把它当成有效的deviceToken纳入到$deviceTokens中,如何清除失效过期的deviceToken是个必须考虑的问题。
查阅相关资料发现APNS服务有一个The Feedback Service的服务,国内的博客基本上忽略了这个环节,很少有资料提及,还是谷歌找个官方网站比较靠谱。下面简要介绍一下这个服务:
在进行APNS远程推送时,如果由于用户卸载了app而导致推送失败,APNS服务器会记录下这个deviceToken,加入到一个列表中,可以通过查询这个列表,获取失效的推送令牌,从数据库中清除这些失效的令牌就可以避免下次推送时被加入到推送数组中来。连接这项服务很简单和推送工程类似,只不过地址不同,开发环境为<span style="font-family: 宋体, SimSun; font-size: 18px;">feedback.push.apple.com</span>
,测试环境为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
|
<?php defined( '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*" , $devc
|