基於APNs最新HTTP/2介面實現iOS的高效能訊息推送(服務端篇)

jsjsjjs發表於2018-07-30

本文原作者:liuyan731,原文地址:liuyan731.github.io/2017/12/05/How-To-Use-APNs-Pushy,內容有改動。

1、前言

本文要分享的訊息推送指的是當iOS端APP被關閉或者處於後臺時,還能收到訊息/資訊/指令的能力。

這種在APP處於後臺或關閉情況下的訊息推送能力,通常在以下場景下非常有用:

1)IM即時通訊聊天應用:聊天訊息通知、音視訊聊天呼叫等,典型代表有:微信、QQ、易信、米聊、釘釘、Whatsup、Line;

2)新聞資訊應用:最新資訊通知等,典型程式碼有:網易新聞客戶端、騰訊新聞客戶端;

3)SNS社交應用:轉發/關注/贊等通知,典型代表有:微博、知乎;

4)郵箱客戶端:新郵件通知等,典型代表有:QQ郵箱客戶端、Foxmail客戶端、網易郵箱大師;

5)金融支付應用:收款通知、轉賬通知等,典型代表有:支付寶、各大銀行的手機銀行等;

…. ….

除了以上典型場景下,訊息推送這種能力已經被越來越多的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

2、相關文章

有關iOS客戶端APNs訊息推送技術的介紹文章:

iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等

信鴿團隊原創:一起走過 iOS10 上訊息推送(APNS)的坑

瞭解iOS訊息推送一文就夠:史上最全iOS Push技術詳解

移動端實時訊息推送技術淺析

掃盲貼:淺談iOS和Android後臺實時訊息推送的原理和區別

有關訊息推送技術服務端架構方面的文章:

絕對乾貨:基於Netty實現海量接入的推送服務技術要點

極光推送系統大規模高併發架構的技術實踐分享

魅族2500萬長連線的實時訊息推送架構的技術實踐分享

專訪魅族架構師:海量長連線的實時訊息推送系統的心得體會

一個基於長連線的安全可擴充套件的訂閱/推送服務實現思路

實踐分享:如何構建一套高可用的移動端訊息推送系統?

Go語言構建千萬級線上的高併發訊息推送系統實踐(來自360公司)

騰訊信鴿技術分享:百億級實時訊息推送的實戰經驗

百萬線上的美拍直播彈幕系統的實時推送技術實踐之路

京東京麥商家開放平臺的訊息推送架構演進之路

>> 更多同類文章 ……

3、提一下Android端的訊息推送

論壇裡做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

4、說一說為什麼不使用第3方推送服務SDK?

目前主流的iOS第3方推送SDK有:友盟推送、極光推送、信鴿推送等。

使用第3方推送的優點主要是:

1)簡單:開箱即用,無需關注技術細節;

2)統計:提供了推送資料的統計能力等;

3)效能:無需關注效能負載,因為第3方都幫你實現好了,你只要呼叫它的介面即可。

使用第3方推送的缺點也很明顯:

1)到達率:雖然第3方移動端訊息推送產品都宣傳到達率能夠達到 90%及以上,但是實際使用起來,發現遠遠達不到;

2)實時性:第3方移動端訊息推送產品的推送通道是共用的,會面向多個推送客戶,如果某一個客戶PUSH推送量特別大,那麼其他的移動端訊息推送訊息實時性可能就會受到影響;

3)不可控:雖然各種技術細節無需你關注是個優點,但它也同時是個缺點——因為你不可控的東西太多了,想要做一個定製化的需求就力不從心了;

4)被限流:因為第3方的推送服務多是免費提供,所以介面呼叫等都是有限制要求的(即使紙面上沒有說出來),限流是一定要做的,不然這些成本誰抗的住?

針對以上問題,58同城團隊在《58同城高效能移動端訊息推送技術架構演進之路》也有討論。

更為關鍵的是,如果是實現iOS的訊息推送,蘋果官方提供的APNs服務已經足夠簡單,如果不是為了專案趕進度或偷懶,自已來實現是更靠譜的選擇,簡單的事情沒有必要複雜化,這也正是本文作者的選擇。

好了,言歸正傳,繼續聊回使用pushy實現iOS高效能推送這個話題。

5、APNs和Pushy

蘋果裝置的訊息推送是依靠蘋果的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的文件和說明很全,討論也很活躍,作者基本有問必答,大部分疑問都可以找到答案,使用難度也不大。

6、在Java端使用Pushy進行APNs訊息推送

6.1 首先加入包

6.2 身份認證

蘋果APNs提供了兩種認證的方式:基於JWT的身份資訊token認證和基於證照的身份認證。Pushy也同樣支援這兩種認證方式,這裡我們使用證照認證方式,關於token認證方式可以檢視Pushy的文件。

如何獲取蘋果APNs身份認證證照可以查考官方文件

6.3 Pushy使用

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的基本用法,在我們的生產環境中情況可能會更加複雜,我們可能需要知道什麼時候所有推送都完成了,可能需要對推送成功訊息進行計數,可能需要防止記憶體不足,也可能需要對不同的傳送結果進行不同處理….

不多說,上程式碼(請看下節…)。

7、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(” …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表格,瞭解出錯情況,及時調整。

8、來看看Pushy的效能

作者在Google討論組中說Pushy推送可以單核單執行緒達到10k/s-20k/s,如下圖所示:

▲ 作者關於建立多client的建議及Pushy效能描述

但是可能是網路或其他原因,我的測試結果沒有這麼好,把測試結果貼出來,僅供參考(時間ms)。

ps:由於是測試,沒有大量的裝置可以用於群發推送測試,所以以往一個裝置傳送多條推送替代。這裡短時間往一個裝置傳送大量的推送,APNs會報TooManyRequests錯誤,Too many requests were made consecutively to the same device token。所以會有少量訊息無法發出。

ps:這裡的推送時間,沒有加上client初始化的時間。

ps:訊息推送時間與被推訊息的大小有關係,這裡我在測試時沒有控制訊息變數(都是我瞎填的,都是很短的訊息)所以資料僅供參考。

關於Pushy效能優化也可以看看官網作者的建議:Threads, concurrent connections, and performance

大家有測試的資料也可以分享出來一起討論一下。

8、思考和小結

蘋果APNs一直在更新優化,一直在擁抱新技術(HTTP/2,JWT等),是一個非常了不起的服務。

自己來直接呼叫APNs服務來達到生成環境要求還是有點困難。Turo給我們提供了一個很好的Java庫:Pushy。Pushy還有一些其他的功能與用法(Metrics、proxy、Logging…),總體來說還是非常不錯的。

同時感覺我們使用Pushy還可以調優…

附錄:更多訊息推送技術文章

[1] 有關IM/推送技術原理和服務端架構等:

iOS的推送服務APNs詳解:設計思路、技術原理及缺陷等

信鴿團隊原創:一起走過 iOS10 上訊息推送(APNS)的坑

Android端訊息推送總結:實現原理、心跳保活、遇到的問題等

掃盲貼:認識MQTT通訊協議

一個基於MQTT通訊協議的完整Android推送Demo

IBM技術經理訪談:MQTT協議的制定歷程、發展現狀等

求教android訊息推送:GCM、XMPP、MQTT三種方案的優劣

移動端實時訊息推送技術淺析

掃盲貼:淺談iOS和Android後臺實時訊息推送的原理和區別

絕對乾貨:基於Netty實現海量接入的推送服務技術要點

移動端IM實踐:谷歌訊息推送服務(GCM)研究(來自微信)

為何微信、QQ這樣的IM工具不使用GCM服務推送訊息?

極光推送系統大規模高併發架構的技術實踐分享

從HTTP到MQTT:一個基於位置服務的APP資料通訊實踐概述

魅族2500萬長連線的實時訊息推送架構的技術實踐分享

專訪魅族架構師:海量長連線的實時訊息推送系統的心得體會

深入的聊聊Android訊息推送這件小事

基於WebSocket實現Hybrid移動應用的訊息推送實踐(含程式碼示例)

一個基於長連線的安全可擴充套件的訂閱/推送服務實現思路

實踐分享:如何構建一套高可用的移動端訊息推送系統?

Go語言構建千萬級線上的高併發訊息推送系統實踐(來自360公司)

騰訊信鴿技術分享:百億級實時訊息推送的實戰經驗

百萬線上的美拍直播彈幕系統的實時推送技術實踐之路

京東京麥商家開放平臺的訊息推送架構演進之路

瞭解iOS訊息推送一文就夠:史上最全iOS Push技術詳解

基於APNs最新HTTP/2介面實現iOS的高效能訊息推送(服務端篇)

>> 更多同類文章 ……

[2] 有關IM/訊息推送的通訊格式、協議的選擇等:

Protobuf通訊協議詳解:程式碼演示、詳細原理介紹等

一個基於Protocol Buffer的Java程式碼演示

簡述傳輸層協議TCP和UDP的區別

為什麼QQ用的是UDP協議而不是TCP協議?

移動端即時通訊協議選擇:UDP還是TCP?

如何選擇即時通訊應用的資料傳輸格式

強列建議將Protobuf作為你的即時通訊應用資料傳輸格式

全方位評測:Protobuf效能到底有沒有比JSON快5倍?

移動端IM開發需要面對的技術問題(含通訊協議選擇)

簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端

理論聯絡實際:一套典型的IM通訊協議設計詳解

58到家實時訊息系統的協議設計等技術實踐分享

詳解如何在NodeJS中使用Google的Protobuf

技術掃盲:新一代基於UDP的低延時網路傳輸層協議——QUIC詳解

金蝶隨手記團隊分享:還在用JSON? Protobuf讓資料傳輸更省更快(原理篇)

金蝶隨手記團隊分享:還在用JSON? Protobuf讓資料傳輸更省更快(實戰篇)

>> 更多同類文章 ……

[3] 有關Android端IM/訊息推送的心跳保活處理等:

應用保活終極總結(一):Android6.0以下的雙程式守護保活實踐

應用保活終極總結(二):Android6.0及以上的保活實踐(程式防殺篇)

應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)

Android程式保活詳解:一篇文章解決你的所有疑問

Android端訊息推送總結:實現原理、心跳保活、遇到的問題等

深入的聊聊Android訊息推送這件小事

為何基於TCP協議的移動端IM仍然需要心跳保活機制?

微信團隊原創分享:Android版微信後臺保活實戰分享(程式保活篇)

微信團隊原創分享:Android版微信後臺保活實戰分享(網路保活篇)

移動端IM實踐:實現Android版微信的智慧心跳機制

移動端IM實踐:WhatsApp、Line、微信的心跳策略分析

>> 更多同類文章 ……

(本文同步釋出於:http://www.52im.net/thread-1820-1-1.html


相關文章