本文要分享的消息推送指的是當iOS端APP被關閉或者處於後臺時,還能收到消息/信息/指令的能力。php
這種在APP處於後臺或關閉狀況下的消息推送能力,一般在如下場景下很是有用:html
1)IM即時通信聊天應用:聊天消息通知、音視頻聊天呼叫等,典型表明有:微信、QQ、易信、米聊、釘釘、Whatsup、Line;java
2)新聞資訊應用:最新資訊通知等,典型代碼有:網易新聞客戶端、騰訊新聞客戶端;android
3)SNS社交應用:轉發/關注/贊等通知,典型表明有:微博、知乎;ios
4)郵箱客戶端:新郵件通知等,典型表明有:QQ郵箱客戶端、Foxmail客戶端、網易郵箱大師;git
5)金融支付應用:收款通知、轉帳通知等,典型表明有:支付寶、各大銀行的手機銀行等;github
.... ....緩存
除了以上典型場景下,消息推送這種能力已經被愈來愈多的APP做爲基礎能力之一,由於移動互聯網時代下,用戶的「全時在線」能力很是誘人和強大,能隨時隨地即時地將各類重要信息推送給用戶,無疑是很是有意義的。安全
衆所周之,iOS端的這項消息推送能力就是使用蘋果提供的APNs服務來實現(有些iOS小白開發者可能看到各類第3方的iOS端消息推送SDK,總會習慣性地認爲這是徹底由第3方提供的能力,實際上一樣是使用APNs,只是封裝了一下而已)。目前介紹APNs消息推送的文章多討論的是手機端的實現,而服務端的消息要怎麼「推」出來這樣的文章,要麼太老,要麼只是介紹如何調用第3方的服務端SDK接口而已(如極光推廣、友盟推送、騰訊信鴿推送等)。因此本文趁着最近對項目組的老蘋果iOS推送進行升級修改機會,詳細查閱了最新蘋果的APNs接口文檔,同時爲了不重複造輪子(懶),在調研了一些開源經常使用的庫以後,選擇了Turo團隊開發和維護的pushy開源工程來實如今Java服務端調用蘋果最新的APNs HTTP/2接口進行消息推送,並藉此文對Pushy的使用方法進行了總結和記錄,但願對你用。性能優化
補充說明:網上目前能查到的有關iOS端APNs消息推送的Java服務端代碼實現,可能是介紹如何使用Java-APNS這個工程,但這個工程以及相似的其它工程都好久沒有維護了,跟最新的蘋果APNs服務已經很難匹配了。相較而言puhsy這個工程一直比較活躍,也對蘋果的最新APNs跟進的比較及時,於是本文做者在公司的項目進行升級和重構過程當中,絕不猶豫的使用了pushy。
學習交流:
- 即時通信開發交流3羣:185926912 [推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
(本文同步發佈於:http://www.52im.net/thread-1820-1-1.html)
有關iOS客戶端APNs消息推送技術的介紹文章:
《iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等》
《信鴿團隊原創:一塊兒走過 iOS10 上消息推送(APNS)的坑》
有關消息推送技術服務端架構方面的文章:
《Go語言構建千萬級在線的高併發消息推送系統實踐(來自360公司)》
>> 更多同類文章 ……
論壇裏作IM或消息推送服務的朋友都很清楚,相對於蘋果爲iOS包辦好的APNs技術,Android上的消息推送技術亂七八糟、一塌糊塗,緣由是國內的Android廠商將Android原生的GCM(如今叫FCM,跟iOS的APNs是相似的技術)進行了閹割,加上各廠商的省電策略、這全策略各不相同,致使爲了實現IM和其它各類應用中的後臺消息推送,不得不爲了進程保活、網絡保活搞出各類黑科技(固然,自從Android 6.0發佈之後,谷歌爲了打擊這種不道德的行爲,進行了愈來愈嚴格的限制,保活黑科技愈來愈難搞了)。
國內的廠商爲了跟進新版本Android的GCM(如今叫FCM),也都在搞自已的消息推送通道:小米手機有小米推送、魅族手機有魅族推送、華爲手機有華爲推送等等,開發者在放棄保活黑科技之後,只能一家一家接入各廠商的推送通道,而這這又涉到同一廠商的手機版本、不一樣廠商通道的自動識別等,麻煩事亂到你沒法想象,就連第3方推送服務也只能就範——一家一家接入(好比信鴿的《[資訊] 信鴿新版上線:號稱Android首家統一推送服務》)。
爲了解決上述亂象,好消息是去年有政府背景的「統一推送聯盟」成立了(詳見《[資訊] 統一推送聯盟在京成立:結束國內安卓生態混亂》),廣大Android開發者真是翹首以盼,但壞消息是好進展並不順利(你們心知肚明啊,各廠商的利益很差均衡嘛),最近一次跟消息推送服務有關的活動仍是3個月前的《[資訊] 統一推送聯盟2018成員大會如期召開》。雖然進展不大,但總算仍是有但願,Android同行們再等等,總有Android端消息推送一統江湖的方案出現的那天。
固然,本文主要是討論iOS端的消息推送,本節文字只是寫給Android端消息推送感興趣的同行看的,更多Android消息推送技術的文章,請前往:http://www.52im.net/forum.php?mod=collection&action=view&ctid=11
目前主流的iOS第3方推送SDK有:友盟推送、極光推送、信鴿推送等。
使用第3方推送的優勢主要是:
1)簡單:開箱即用,無需關注技術細節;
2)統計:提供了推送數據的統計能力等;
3)性能:無需關注性能負載,由於第3方都幫你實現好了,你只要調用它的接口便可。
使用第3方推送的缺點也很明顯:
1)到達率:雖然第3方移動端消息推送產品都宣傳到達率可以達到 90%及以上,可是實際使用起來,發現遠遠達不到;
2)實時性:第3方移動端消息推送產品的推送通道是共用的,會面向多個推送客戶,若是某一個客戶PUSH推送量特別大,那麼其餘的移動端消息推送消息實時性可能就會受到影響;
3)不可控:雖然各類技術細節無需你關注是個優勢,但它也同時是個缺點——由於你不可控的東西太多了,想要作一個定製化的需求就力不從心了;
4)被限流:由於第3方的推送服務可能是免費提供,因此接口調用等都是有限制要求的(即便紙面上沒有說出來),限流是必定要作的,否則這些成本誰抗的住?
針對以上問題,58同城團隊在《58同城高性能移動端消息推送技術架構演進之路》也有討論。
更爲關鍵的是,若是是實現iOS的消息推送,蘋果官方提供的APNs服務已經足夠簡單,若是不是爲了項目趕進度或偷懶,自已來實現是更靠譜的選擇,簡單的事情沒有必要複雜化,這也正是本文做者的選擇。
好了,言歸正傳,繼續聊回使用pushy實現iOS高性能推送這個話題。
蘋果設備的消息推送是依靠蘋果的APNs(Apple Push Notification service)服務的,APNs的官方簡介以下:
Apple Push Notification service (APNs) is the centerpiece of the remote notifications feature. It is a robust, secure, and highly efficient service for app developers to propagate information to iOS (and, indirectly, watchOS), tvOS, and macOS devices.
(若是英文看起來不方便,能夠看看《iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等》)
IOS設備(tvOS、macOS)上的全部消息推送都須要通過APNs,APNs服務確實很是厲害,天天須要推送上百億的消息,可靠、安全、高效。就算是微信和QQ這種用戶級別的即時通信app在程序沒有啓動或者後臺運行過程當中也是須要使用APNs的(當程序啓動時,使用本身創建的長鏈接),只不過騰訊優化了整條從他們服務器到蘋果服務器的線路而已,因此以爲推送要快(參考知乎)。
項目組老的蘋果推送服務使用的是蘋果之前的基於二進制socket的APNs,同時使用的是一個javapns的開源庫,這個javapns貌似效果不是很好,在網上也有人有過討論。javapns如今也中止維護DEPRECATED掉了。做者建議轉向基於蘋果新APNs服務的庫。
蘋果新APNs基於HTTP/2,經過鏈接複用,更加高效,固然還有其它方面的優化和改善,能夠參考APNs的一篇介紹,講解的比較清楚。
再說一下咱們使用的Pushy,官方簡介以下:
Pushy is a Java library for sending APNs (iOS, macOS, and Safari) push notifications. It is written and maintained by the engineers at Turo......We believe that Pushy is already the best tool for sending APNs push notifications from Java applications, and we hope you'll help us make it even better via bug reports and pull requests.
Pushy的文檔和說明很全,討論也很活躍,做者基本有問必答,大部分疑問均可以找到答案,使用難度也不大。
蘋果APNs提供了兩種認證的方式:基於JWT的身份信息token認證和基於證書的身份認證。Pushy也一樣支持這兩種認證方式,這裏咱們使用證書認證方式,關於token認證方式能夠查看Pushy的文檔。
如何獲取蘋果APNs身份認證證書能夠查考官方文檔。
ps:這裏的setClientCredentials函數也能夠支持傳入一個InputStream和證書密碼。
同時也能夠經過setApnsServer函數來指定是開發環境仍是生產環境:
Pushy是基於Netty的,經過ApnsClientBuilder咱們能夠根據須要來修改ApnsClient的鏈接數和EventLoopGroups的線程數:
關於鏈接數和EventLoopGroup線程數官網有以下的說明,簡單來講,不要配置EventLoopGroups的線程數超過APNs鏈接數:
Because connections are bound to a single event loop (which is bound to a single thread), it never makes sense to give an ApnsClient more threads in an event loop than concurrent connections. A client with an eight-thread EventLoopGroup that is configured to maintain only one connection will use one thread from the group, but the other seven will remain idle. Opening a large number of connections on a small number of threads will likely reduce overall efficiency by increasing competition for CPU time.
關於消息的推送,注意必定要使用異步操做,Pushy發送消息會返回一個Netty Future對象,經過它能夠拿到消息發送的狀況:
APNs服務器能夠保證同時發送1500條消息,當超過這個限制時,Pushy會緩存消息,因此咱們沒必要擔憂異步操做發送的消息過多。
當咱們的消息很是多,達到上億時,咱們也得作一些控制,避免緩存過大,內存不足,Pushy給出了使用Semaphore的解決方法:
The APNs server allows for (at the time of this writing) 1,500 notifications in flight at any time. If we hit that limit, Pushy will buffer notifications automatically behind the scenes and send them to the server as in-flight notifications are resolved.
In short, asynchronous operation allows Pushy to make the most of local resources (especially CPU time) by sending notifications as quickly as possible.
以上僅是Pushy的基本用法,在咱們的生產環境中狀況可能會更加複雜,咱們可能須要知道何時全部推送都完成了,可能須要對推送成功消息進行計數,可能須要防止內存不足,也可能須要對不一樣的發送結果進行不一樣處理....
很少說,上代碼(請看下節...)。
參考Pushy的官方最佳實踐,咱們加入了以下操做:
經過Semaphore來進行流控,防止緩存過大,內存不足;
經過CountDownLatch來標記消息是否發送完成;
使用AtomicLong完成匿名內部類operationComplete方法中的計數;
使用Netty的Future對象進行消息推送結果的判斷。
具體用法參考以下代碼:
publicclassIOSPush {
privatestaticfinalLogger logger = LoggerFactory.getLogger(IOSPush.class);
privatestaticfinalApnsClient apnsClient = null;
privatestaticfinalSemaphore semaphore = newSemaphore(10000);
publicvoidpush(finalList deviceTokens, String alertTitle, String alertBody) {
longstartTime = System.currentTimeMillis();
if(apnsClient == null) {
try{
EventLoopGroup eventLoopGroup = newNioEventLoopGroup(4);
apnsClient = newApnsClientBuilder().setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST)
.setClientCredentials(newFile("/path/to/certificate.p12"), "p12-file-password")
.setConcurrentConnections(4).setEventLoopGroup(eventLoopGroup).build();
} catch(Exception e) {
logger.error("ios get pushy apns client failed!");
e.printStackTrace();
}
}
longtotal = deviceTokens.size();
finalCountDownLatch latch = newCountDownLatch(deviceTokens.size());
finalAtomicLong successCnt = newAtomicLong(0);
longstartPushTime = System.currentTimeMillis();
for(String deviceToken : deviceTokens) {
ApnsPayloadBuilder payloadBuilder = newApnsPayloadBuilder();
payloadBuilder.setAlertBody(alertBody);
payloadBuilder.setAlertTitle(alertTitle);
String payload = payloadBuilder.buildWithDefaultMaximumLength();
finalString token = TokenUtil.sanitizeTokenString(deviceToken);
SimpleApnsPushNotification pushNotification = newSimpleApnsPushNotification(token, "com.example.myApp", payload);
try{
semaphore.acquire();
} catch(InterruptedException e) {
logger.error("ios push get semaphore failed, deviceToken:{}", deviceToken);
e.printStackTrace();
}
finalFuture> future = apnsClient.sendNotification(pushNotification);
future.addListener(newGenericFutureListener>() {
@Override
publicvoidoperationComplete(Future pushNotificationResponseFuture) throwsException {
if(future.isSuccess()) {
finalPushNotificationResponse response = future.getNow();
if(response.isAccepted()) {
successCnt.incrementAndGet();
} else{
Date invalidTime = response.getTokenInvalidationTimestamp();
logger.error("Notification rejected by the APNs gateway: "+ response.getRejectionReason());
if(invalidTime != null) {
logger.error("\t…and the token is invalid as of "+ response.getTokenInvalidationTimestamp());
}
}
} else{
logger.error("send notification device token={} is failed {} ", token, future.cause().getMessage());
}
latch.countDown();
semaphore.release();
}
});
}
try{
latch.await(20, TimeUnit.SECONDS);
} catch(InterruptedException e) {
logger.error("ios push latch await failed!");
e.printStackTrace();
}
longendPushTime = System.currentTimeMillis();
logger.info("test pushMessage success. [共推送"+ total + "個][成功"+ (successCnt.get()) + "個],
totalcost= " + (endPushTime - startTime) + ", pushCost=" + (endPushTime - startPushTime));
}
}
關於多線程調用client:
Pushy ApnsClient是線程安全的,可使用多線程來調用。
關於建立多個client:
建立多個client是能夠加快發送速度的,可是提高並不大,做者建議:
ApnsClient instances are designed to stick around for a long time. They're thread-safe and can be shared between many threads in a large application. We recommend creating a single client (per APNs certificate/key), then keeping that client around for the lifetime of your application.
關於APNs響應信息(錯誤信息):
能夠查看APNs官網的error code表格,瞭解出錯狀況,及時調整。
做者在Google討論組中說Pushy推送能夠單核單線程達到10k/s-20k/s,以下圖所示:
可是多是網絡或其餘緣由,個人測試結果沒有這麼好,把測試結果貼出來,僅供參考(時間ms)。
ps:因爲是測試,沒有大量的設備能夠用於羣發推送測試,因此以往一個設備發送多條推送替代。這裏短期往一個設備發送大量的推送,APNs會報TooManyRequests錯誤,Too many requests were made consecutively to the same device token。因此會有少許消息沒法發出。
ps:這裏的推送時間,沒有加上client初始化的時間。
ps:消息推送時間與被推消息的大小有關係,這裏我在測試時沒有控制消息變量(都是我瞎填的,都是很短的消息)因此數據僅供參考。
關於Pushy性能優化也能夠看看官網做者的建議:Threads, concurrent connections, and performance
你們有測試的數據也能夠分享出來一塊兒討論一下。
蘋果APNs一直在更新優化,一直在擁抱新技術(HTTP/2,JWT等),是一個很是了不得的服務。
本身來直接調用APNs服務來達到生成環境要求仍是有點困難。Turo給咱們提供了一個很好的Java庫:Pushy。Pushy還有一些其餘的功能與用法(Metrics、proxy、Logging...),整體來講仍是很是不錯的。
同時感受咱們使用Pushy還能夠調優...
[1] 有關IM/推送技術原理和服務端架構等:
《iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等》
《信鴿團隊原創:一塊兒走過 iOS10 上消息推送(APNS)的坑》
《Android端消息推送總結:實現原理、心跳保活、遇到的問題等》
《一個基於MQTT通訊協議的完整Android推送Demo》
《求教android消息推送:GCM、XMPP、MQTT三種方案的優劣》
《掃盲貼:淺談iOS和Android後臺實時消息推送的原理和區別》
《移動端IM實踐:谷歌消息推送服務(GCM)研究(來自微信)》
《爲什麼微信、QQ這樣的IM工具不使用GCM服務推送消息?》
《從HTTP到MQTT:一個基於位置服務的APP數據通訊實踐概述》
《基於WebSocket實現Hybrid移動應用的消息推送實踐(含代碼示例)》
《Go語言構建千萬級在線的高併發消息推送系統實踐(來自360公司)》
《瞭解iOS消息推送一文就夠:史上最全iOS Push技術詳解》
《基於APNs最新HTTP/2接口實現iOS的高性能消息推送(服務端篇)》
>> 更多同類文章 ……
[2] 有關IM/消息推送的通訊格式、協議的選擇等:
《一個基於Protocol Buffer的Java代碼演示》
《強列建議將Protobuf做爲你的即時通信應用數據傳輸格式》
《全方位評測:Protobuf性能到底有沒有比JSON快5倍?》
《詳解如何在NodeJS中使用Google的Protobuf》
《技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解》
《金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(原理篇)》
《金蝶隨手記團隊分享:還在用JSON? Protobuf讓數據傳輸更省更快(實戰篇)》
>> 更多同類文章 ……
[3] 有關Android端IM/消息推送的心跳保活處理等:
《應用保活終極總結(一):Android6.0如下的雙進程守護保活實踐》
《應用保活終極總結(二):Android6.0及以上的保活實踐(進程防殺篇)》
《應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)》
《Android端消息推送總結:實現原理、心跳保活、遇到的問題等》
《微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)》
《微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)》
《移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-1820-1-1.html)