我們的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
最后小圖一張:
