我们的IOS移动应用要实现消息推送,告诉用户有多少条消息未读,类似下图的效果(笑果),特把APNS和Erlang相关解决方案笔记于此备忘.
上面图片中是Apple Notification在UI展现的形式之一,Notification共有三种形式:图标显示更新数字(badge),提示信息(alert),提示音(sound);
iOS Apple Push Notification Services (APNs)官方的开发文档位置在:[Apple Push Notification Services],iOS团队的Matthijs Hollemans写的入门文档:Apple Push Notification Services in iOS 6 Tutorial 从这两份文档中,可以了解APNs的设计和开发的各种细节.首先是APNS的设计初衷:当用户没有启动应用,或者没有开机那么应用Server想要推送的消息就无法到达,需要其他的机制来完成消息的投递.
Apple Push Notification service (APNs)把消息推送到设备上,设备上有应用已经注册过要接受此类消息.这里会有三种角色:Provider, APNs,Device . 见下图:
上图说明:消息提供者(Provider),Provider接入到APNs,把最新的消息推送到 APNs,然后由APNs推送到目标设备(Device)的指定应用(Client APP).
从上面的图,可以提出很多问题,特别是考虑到一些极端情况的时候,很有意思,可以通过这些问题驱动阅读Apple开发文档:
- Provider,APNs,Device 这三者之间的信任关系是怎么建立?
- 如何标识消息是给哪台机器的哪个应用的?消息传递的协议是怎么设计的(如何承载要发送的消息)?
- 在Device离线的情况下,Provider提交N条要发送给该Device的消息到APNs,APNs如何处理?
- 对于一些极端情况:比如Device做了系统恢复,应用卸载,Device硬件损坏,APNs有哪些应对机制?
首先解决三者的信任关系,Provider(APP Server)的开发方要从Apple Dev Center获得SSL证书, 每个证书一个应用,甚至开发和生产环境的证书都要分别申请. Provider要在APNs中进行认证注册,目前注册使用的是应用程序的唯一标识(bundle identifier).Provider Connection的是对应到指定应用的,certificate中包含了应用程序的标识信息(bundle ID),APNs维护了一个废弃列表,如果一个Provider上了名单,APNs就会移除对该应用的信任. Provider和APNs通信协议是二进制协议,使用TCP流协议建立SSL(TLS)安全连接,官方文档称这种信任为Connection Trust.
对于用户设备APNs使用的是Token Trust.用户安装一个APP的时候,如果APP需要消息推送功能通常在安装成功之后会经由用户设备发起注册请求,用户设备将此请求转发到APNs, APNs生成唯一的device certificate,其中包含了device token.Device Token 中包含了设备的唯一标识,使用Token key加密Device Token返回到Device.用户设备把device token返回给发起注册请求的应用程序,应用程序把Device Token的信息传递给Provider.用户设备上安装的APP从APNs 获得device token之后每一次连接到ANPs都要提供这个token. APNs解密device token并验证这个token是从连接过来的设备生成的:APNs保证实际连接过来的设备标识和certificate文件中里面包含的标识一致.
Provider提交到APNS的notification两个必要的信息:把什么消息投递给谁,即包含设备标识(Device Token)和实际消息体(Payload).APNS使用token Key解密token,从中提取设备ID来决定最终消息投递到哪个设备.Device Token有一个非常贴切的类比:手机号,它包含的信息可以让APNs来定位安装了指定应用的设备.APNs还使用Device Token来路由消息,Payload的消息组织形式是类JSON的,它包含的信息包括推送给设备什么内容以及如何提示;Payload内容大小限制是 256 bytes.
注意:Device Token和设备的UDID不是一回事,用户恢复系统,重装都会导致device token变化.
APNs有一个 Feedback Service的设计,它维护应用消息推送失败的设备列表,如果应用已经卸载了就无法投递成功,这样Feedback Service里面就会有记录.Provider的开发方应定期从该服务拉取这个失败列表来调整自己的发送行为:不要再给一个总是失败的设备推送消息了.如果设备离线,notification会在APNs上保存有限的一段时间,设备上线之后完成推送.如果设备离线期间同一个应用推送了多条notification,那么只会保存最新的notification,如果设备长期离线,任何离线消息都会被抛弃掉.这样如果iPhone掉海里面,需要推送给它的消息在过期之后就会被清理掉,不会长久占用APNs的资源.
经过上面的分析基本可以列出Erlang实现消息推送的技术要点了:
[1] JSON数据解析构造 mochijson mochijson2之类的模块就可以搞定 mochijson:encode --> list_to_binary
[3] 二进制协议实现 (Apple Binary Iterface)
Packet = [<<1:8, MsgId/binary, Expiry:4/big-unsigned-integer-unit:8,
32:16/big,
BinToken/binary,
PayloadLength:16/big,
BinPayload/binary>>]
BinToken/binary,
PayloadLength:16/big,
BinPayload/binary>>]
[4] deviceToken -> binary 需要hexstr_to_bin的方法,这个代码片段之前说过多次了
bin_to_hexstr(Bin) -> lists:flatten([io_lib:format("~2.16.0B", [X]) || X <- binary_to_list(Bin)]). hexstr_to_bin(S) -> hexstr_to_bin(S, []). hexstr_to_bin([], Acc) -> list_to_binary(lists:reverse(Acc)); hexstr_to_bin([X,Y|T], Acc) -> {ok, [V], []} = io_lib:fread("~16u", [X,Y]), hexstr_to_bin(T, [V | Acc]).
[5] 维护TCP连接,重连机制
按照上面的要点完成了基本的验证之后,在Github上找到了开源项目apns4erl (地址:https://github.com/inaka/apns4erl),这个项目对APNS服务做了良好的实现和封装.下面介绍下apns4erl的使用:
开源项目APNS4erl
证书制作:
APP Server和Apple Server中间建立信任关系需要通过各种证书,apns4erl作者在项目中提供了生成证书的脚本,不过在项目首页提到.cer和.p12文件生成pem证书的脚本地址是错的,实际位置是:
执行下面的脚本就一步一步即可:
#!/bin/sh # Usage: # test_certs {cert_file} {private_key_file} # Example: # test_certs aps_developer_indetity.cer aps_developer_identity.p12 mkdir -p priv/temp openssl pkcs12 -in "$2" -out priv/temp/key-enc.pem openssl rsa -in priv/temp/key-enc.pem -out priv/temp/key.pem openssl x509 -inform der -in "$1" -out priv/temp/cert.pem cat priv/temp/cert.pem priv/temp/key.pem > priv/cert.pem rm -rf priv/temp make test
下面是测试代码,注意send_badge/1方法就是我们需要的效果:
-module(t). -compile(export_all). -define(APNS_NAME,app_apns). -include("apns.hrl"). -include("localized.hrl"). conn_apns() -> ssl:start(), apns:start(), apns:connect( ?APNS_NAME, fun handle_apns_error/2, fun handle_apns_delete_subscription/1 ). send_message()-> apns:send_message(?APNS_NAME, "devicetoken31d1df3a324bb72c1ff2bcb3b87d33fd1a2b7578b359fb5494eff", "hello,这是一号话务员"). send_message(Msg) -> apns:send_message(my_connection_name, #apns_msg{ alert = Msg , badge = 5, sound = "beep.wav" , expiry = 1348000749, device_token = "devicetoken31d1df3a324bb72c1ff2bcb3b87d33fd1a2b7578b359fb5494eff" }). send_badge(Number)-> apns:send_badge(qiaoqiao_apns,"devicetoken31d1df3a324bb72c1ff2bcb3b87d33fd1a2b7578b359fb5494eff", Number). handle_apns_error(MsgId, Status) -> error_logger:error_msg("error: ~p - ~p~n", [MsgId, Status]). handle_apns_delete_subscription(Data) -> error_logger:info_msg("delete subscription: ~p~n", [Data]).
APNS相关资料:
[0] iOSDeveloper Library: Apple Push Notification Service (APNS)
[1] Apple Push Notification Services in iOS 6 Tutorial
[2] Apple Push Notification Services in iOS 6 Tutorial 中文
http://www.raywenderlich.com/zh-hans/24732
http://www.raywenderlich.com/zh-hans/24732
[3] iOS 和 Android 的后台推送原理各是什么?有什么区别?
[4] 苹果产品是如何实现推送功能的呢?
[5] 为什么 Android 的后台推送不如 iOS 的推送使用广泛?
[6] Is the device token as unique as the device ID?
[7] If the user restores backup data to a new device or computer, or reinstalls the operating system, the device token changes. https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/IPhoneOSClientImp.html
[8] Apple Push Notifications with Erlang
[9] Sending Apple Push Notifications with Erlang
最后小图一张: