PHP基于socket的ios 推送的实现 The Implement of iOS Push

  1. 预备知识
    1. 通用提供者要求
    2. 客户端的多语言支持
    3. 链接管理的最佳实践
    4. 二进制接口和通知格式
  2. 反馈服务
  3. 实现
    1. stream_select
    2. new static
    3. 响应错误包直接响应为异常对象
    4. 不同等级的日志
    5. 连接
    6. 构造推送消息
    7. 向APNs发送通知
    8. 批量的推送处理——长连接
    9. Question
    10. TODO
    11. 参考,也许对你有用

本类库的目的是为iOS客户端提供内容推送。苹果的推送服务(APNS)是建立在推送服务跟设备之间的,类库的作用就是将要推送给客户端的数据,提交到苹果的推送服务器。这些设备包括iPhone, iPad and the iPod Touch。

实现本身没什么说的,因为这里面牵扯到一些socket的东西,拿出来捋一捋。

TU CAO ME AT GITHUB : https://github.com/mickeyouyou/AppleNotificationPush

预备知识

通用提供者要求

服务器与苹果推送服务通过二进制接口通讯,这对服务器来说是一个高速、高容量的接口,它采用与二进制内容相结合的流媒体TCP套接字设计,二进制接口是异步的。

该二进制接口线上环境地址为gateway.push.apple.com,端口为2195;开发环境为ssl://gateway.sandbox.push.apple.com,端口为2195

对于每个接口,使用TLS(或SSL)建立安全通信通道。 SSL需要来自会员在中心生成的证书,要建立一个值得信赖的供应商的身份,在连接时使用APNs提供的证书使用对等验证。

作为一个通知的提供商,你负责远程通知的以下方面:

客户端的多语言支持

如果你想支持多种语言的通知,但又不使用aps字典中的loc-keyloc-args属性,你需要本地化来自服务器的消息文本,要做到这一点,你需要找出从客户端应用程序的当前语言首选项。注册,调度,并处理用户通知提示获取这些信息的方法。

链接管理的最佳实践

你可以建立多个连接到同一个网关或多个网关实例。如果你需要发送大量远程通知,把他们分散到几个不同网关的连接。这样相比使用一个单独的连接,可以提高性能:它可以让你发送远程通知更快,它可以让的APNs更快的发送通知到设备。

在多个通知的情况下,保持和APNs的连接打开;不要反复打开和关闭连接。APNs对快速连接和断开的视为拒绝服务攻击。你应该留下一个连接打开,除非你知道这接下来将是空闲时间,如果是每天发送通知给用户,每天使用一个新的连接也是可以的。

二进制接口和通知格式

此二进制接口为二进制内容自然采用普通的TCP 套接字, 为了获得最佳性能,批量多个通知在通过接口单个传输,明确或使用TCP/ IP的Nagle算法。消息格式如下图:

推送消息格式

注意:所有数据都被指定了顺序,即大端(Big Endian 具体查看最下面的参考文档)

通知格式的顶层是由以下部分组成,依次是:

字段 长度 备注
Command 1 byte 2
Frame length 4 bytes frame data 的长度
Frame data variable length 该帧包含体,构造为一系列项目

该帧数据是由一系列的项目组成。每个项目由以下部分组成,依次是:

字段 长度 备注
Item ID 1 byte 该项目标识符。例如,有效负载的项目数为2
Item data length 2 bytes 项目数据的大小
Item data 自定义长度 项目内容

项目和它们的标识符如下:

项目ID 项目名称 长度 数据
1 Device token 32 bytes 设备的二进制格式,有设备注册
2 Payload 自定义长度,小于或等于2kb json格式的荷载数据;不能以null填充
3 Notification identifier 4 bytes
4 Expiration date 4 bytes
5 Priority 1 byte

如果服务器发送的通知被苹果推送服务正确接收,此情况不会有任何返回。

如果你发送的通知格式不正确或者不能理解, APNs将会发送一个错误响应包,并关闭连接。在使用相同的连接格式错误的通知后发出的任何通知将被丢弃,并且必须重新发送。错误响应包格式如下图:

错误响应包数据结构

该包含有一个值为8的命令,之后跟一字节长的状态码和错误格式的通知标识符。
下表列出了可能返回的状态码和对应的含义:

状态码 描述
0 没有错误发生
1 处理错误
2 缺失DiviceToken
3 缺少topic
4 缺少荷载数据
5 无效的token大小
6 无效的topic大小
7 无效的荷载数据大小
8 无效的token
10 连接关闭
255 未知错误

状态码为10表示苹果推送服务器关闭了连接(比如维护)。错误响应包中的通知标识符表示上一次同事被成功发送,之后发送的通知将会被丢弃,必须重新发送,一旦你收到状态码,应该停止使用当前连接,重新开启一个连接。

值得注意的是,正式环境和开发环境的device token值是不一样的。

反馈服务

苹果推送服务包含一个反馈服务,能将失败的远程通知反馈给你。当应用在设备上不存在时,该通知肯定不会被发送,反馈服务将该设备的标识符添加到它的列表中。通知在发送之前过期的情况不会被视为发送失败,也不会影响反馈服务。通过使用该反馈信息,以停止发送那些无法发送的远程通知,减少不必要的消息开销,并提高整个系统的性能。

每天查询反馈服务,获取设备标识符列表 。使用时间戳来验证该设备令牌没有被重新注册,因为被产生的反馈项。对于尚未重新登记的设备,停止发送通知。APN监控供应商为他们检查发送远程通知,以不存在的应用程序在设备上的反馈服务并避免勤奋。

注意:反馈服务为每个推送主题维护一个单独列表。如果你有多个应用程序,你必须为每一个应用连接反馈服务一次,使用相应的证书,以获得所有的反馈。

反馈服务也有一个和发送通知相似的二进制接口,你可以通过下面的地址和端口连接到反馈服务,feedback.push.apple.com ,端口2196;开发环境的反馈服务可以通过下面的连接:feedback.sandbox.push.apple.com,端口2196。就像发送服务的二进制接口一样,使用SSL/TLS建立安全的通讯通道。使用相同的证书验证。要建立一个值得信赖的供应商的身份,在连接APNs时用此证书使用对等验证。

一旦你建立连接,传送立即开始,你不用发送任何指令给APNs,从反馈服务接口读取流数据直到没有数据可读为止。接收到的数据格式如下图:

反馈的二进制格式

时间戳 token长度 device token
一个四字节长的时间戳,表示当APNs确定应用不再设备上。 两个字节长的设备标识符整形长度 设备标识符的二进制格式

反馈列表内容将在你读完之后清除,每次连接到反馈服务,它将返回自上次连接之后发送的失败列表。

实现

stream_select

因为使用socket,在恰当的时候要从socket中拿数据,用stream_select()等待sockets打开的连接事件。stream_select()调用系统的select(2)函数来工作:前面三个参数是你要使用的streams的数组;你可以对其读取,写入和获取异常(分别针对三个参数)。stream_select()可以通过设置$timeout(秒)参数来等待事件发生-事件发生时,相应的sockets数据将写入你传入的参数。

new static

响应错误包直接响应为异常对象

因为你在处理推送数据出错的情况下,苹果推送服务器会返回一个错误响应包(error-Response Packet),此处的设计将此对象化之后作为异常返回。

不同等级的日志

在设计的时候我就考虑了要将日志按不同等级保存,既方便开发,又方便排查问题。
日志,每个类库为了方便自己的调试,肯定需要自己的日志记录,也是为了方便使用者调试,我们需要在用的时候将日志流交给使用者,或者有使用者进行配置就能方便的开始调试,这就是本类库日志的设计思路,而并不是其他的直接输出到流的方法。

连接

public function connect()
{
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', $this->certificate);
stream_context_set_option($ctx, 'ssl', 'passphrase', $this->passphrase);
$this->socket = @stream_socket_client($this->url, $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
if (!$this->socket) {
throw new ApnException("ERROR: '{$errno}' - {$errstr}");
}
stream_set_blocking($this->socket, 0);
stream_set_write_buffer($this->socket, 0);
return true;
}

构造推送消息

$message = new \AppleNotificationPush\Message\Message();
$message->setAlert('SEARCH_ACTIVITY title ');
$message->setSound('default');
$message->setIdentifier(mt_rand(10, 1000));
$message->setDeviceToken('f83f0f2fc1efc0a549601437128bf2d94fea83b4c31b0750d3d9f8f98d5e7a87');
$message->setPriority(10);
$message->setCustomerData(array('id'=> 123));
$message->setCustomerData(array('url' => 'sdfd'));

不管是消息的正文部分还是自定义部分,都支持链式操作:

$message->setAlert('SEARCH_ACTIVITY title ')->setSound('default')->setIdentifier(mt_rand(10, 1000))->setPriority(10);
$message->setDeviceToken('f83f0f2fc1efc0a549601437128bf2d94fea83b4c31b0750d3d9f8f98d5e7a87');
$message->setCustomerData(array(
'id' => mt_rand(10, 2000),
'content' => 'SEARCH_ACTIVITY content',
'url' => 'MY_ORDER_ACTIVITY_UNRECEIVE',
'extra' => '[]'
));

向APNs发送通知

$certificate = "path/to/cetificate.pem";
$connection = new Connection(1, $certificate, C('jpush.apns.password'));
$notification = new Notification($connection);
$notification->sendMessage($message);

经过处理之后待请求APNS的数据格式:

string(233) " 6�K)�[��:��U��"L���.��9�g���{"aps":{"alert":"MY_ORDER_ACTIVITY_UNRECEIVE title1441592096","sound":"default"},"content":"SEARCH_ACTIVITY content","url":"MY_ORDER_ACTIVITY_UNRECEIVE","extra":["http:\/\/www.baidu.com"],"id":45}"

批量的推送处理——长连接

$push = new Push($environment, $certificate, C('jpush.apns.password'));
$push->connect();
foreach($app_uid_relations as $app_uid_relation) {
// insert table
$push->send($app_uid_relation['device_token'], $message);
}
$push->disconnect();

Question

使用的是SSL/TLS安全协议跟apns苹果的推送服务器通讯。所以,其中的关键就是php对socket方式的实现,当然从php的角度,有两种实现方式,一种是fsockopen,另一种就是我用到的stream_socket_client,再设置些参数。

比如测试环境的APNS地址:ssl://gateway.sandbox.push.apple.com:2195,我们指定了基于TCP的SSL协议,当然,也可以指定为TLS,因为是他的升级版。

但这其中有个问题,也是我在过程中思考过的一个问题,因为本人之前做过微信退款相关的功能接口,在请求的时候https://mch.tenpay.com/refundapi/gateway/refund.xml
其实在之前的博文中也提到过,

比如微信退款的一个接口,其实他用到的也是https,其实就是上面说的SSL/TLS协议,他们的方式虽然不同,但是在请求的时候设置证书,设置密码参数等方式还是一样的,但为什么微信退款可以直接用curl 使用证书和密码就可以访问,而苹果服务器却需要使用socket的方式呢?

因为他们所处的协议层不一样,HTTPS(微信退款)是应用层的实现,而苹果的实现方式基于传输层,基于分别不同的协议,导致他们比如以下的区别点:

TODO

参考,也许对你有用

script>