Android推送技術研究

Shawon發表於2016-03-10

前言

最近研究Android推送的實現, 研究了兩天一夜, 有了一點收穫, 寫下來既為了分享, 也為了吐槽. 需要說明的是有些東西偏底層硬體和通訊行業, 我對這些一竅不通, 只能說說自己的理解.

為什麼要研究Android推送技術? 主要還是畢業設計要做一個即時通訊app, 我是不喜歡做什麼社交app的, 也就象牙塔裡的人想得出來, 說實話有這功夫還不如鑽研一個小技術點, 把一個點研究透徹, 比搞個大而全, 還無用的東西好得多, 不過誰叫我們們是普通人, 沒得選呢.

Android推送服務的幾種實現方式

現實生活中, 推送服務就像訂雜誌一樣, 只要留下你的地址, 雜誌就能如期送到你手裡, 可以認為每個人都有唯一的一個地址, 但在目前的網路上, 這是辦不到的, 因為不是每個人都有一個唯一的地址, 伺服器想要給我們推送一條訊息, 必須知道我們的地址, 但伺服器不知道我們在哪.

說到推送服務, 我所知道的實現方案有如下幾種:

輪詢

客戶端定期詢問伺服器有沒有新的訊息, 這樣伺服器不用管客戶端的地址是什麼, 客戶端來問, 直接告訴它就行.

這種方案最簡單, 對於一些不追求實時性的客戶端來說, 很適合, 只需要把時間間隔設定成幾個小時取一次, 就能很方便的解決問題.

但對於即時通訊產品來說, 這種方案完全不能用. 假設即時通訊軟體在網路暢通的情況下傳送的訊息要求對方10s內就能收到, 如果用輪詢, 那麼客戶端要每隔5s連一次伺服器, 如果在移動端, 手機的電量和流量很快就會被消耗殆盡.

SMS通知

這種方案在移動端是有可能的, 讓客戶端攔截手機簡訊, 伺服器在有新訊息時給使用者的手機號發一條特殊的簡訊, 客戶端攔截簡訊後發現是正常簡訊就放行, 如果是特殊簡訊就連線伺服器取訊息.

運營商不會配合, 使用者也不會放心, 這方案普通公司玩不起.

長連線

這大概是目前情況下最佳的方案了, 客戶端主動和伺服器建立TCP長連線之後, 客戶端定期向伺服器傳送心跳包, 有訊息的時候, 伺服器直接通過這個已經建立好的TCP連線通知客戶端.

XMPP, MQTT等不算推送技術

在網上搜尋資料的時候, 經常看見XMPP協議實現的Android推送和MQTT協議實現的Android推送, 我個人覺得這兩種說法都怪怪的, XMPP和MQTT二者都是協議, 儘管我不清楚嚴格來講這倆協議工作在哪一層, 但是絕對是在傳輸層之上的, 姑且認為他倆在TCP/IP五層模型的應用層吧, 閉口不提傳輸層的實現, 而是扯應用層, 這種說法真是令我費解, 所以我個人認為XMPP, MQTT等等不算推送技術.

關於這個XMPP, 我想很多人都是參考Openfire和Smack那套東西, 我一年前嘗試用aSmack和Openfire做IM, 不過那個時候什麼都不懂, 做的東西很爛, 唯一懂的就是Openfire這東西相當老了, 我看有一些開源的推送解決方案都是在這套東西的基礎上改的, 想想這工作量, 挺可怕的.

細說TCP長連線與心跳

長連線方案乍一聽怪怪的, 什麼是長連線? 定時傳送心跳, 這和輪詢有什麼區別? 心跳是幹什麼的? 同樣是定期和伺服器溝通, 為什麼長連線就比輪詢更加優秀? 手機休眠了TCP連線不會斷掉嗎?

這是我在剛開始研究推送技術的時候的問題, 雖然有些還是沒有很準確的答案, 但瞭解的大概可以分享一下, 有什麼錯誤歡迎指出.

什麼是長連線

先說短連線, 短連線是通訊雙方有資料互動時就建立一個連線, 資料傳送完成後,則斷開此連線.

persistent connection

長連線就是大家建立連線之後, 不主動斷開. 雙方互相傳送資料, 發完了也不主動斷開連線, 之後有需要傳送的資料就繼續通過這個連線傳送.

TCP連線在預設的情況下就是所謂的長連線, 也就是說連線雙方都不主動關閉連線, 這個連線就應該一直存在.

但是網路中的情況是複雜的, 這個連線可能會被切斷. 比如客戶端到伺服器的鏈路因為故障斷了, 或者伺服器當機了, 或者是你家網線被人剪了, 這些都是一些莫名其妙的導致連線被切斷的因素, 還有幾種比較特殊的:

NAT超時

因為IPv4地址不足, 或者我們想通過無線路由器上網, 我們的裝置可能會處在一個NAT裝置的後面, 生活中最常見的NAT裝置是家用路由器.

NAT裝置會在IP封包通過裝置時修改源/目的IP地址. 對於家用路由器來說, 使用的是網路地址埠轉換(NAPT), 它不僅改IP, 還修改TCP和UDP協議的埠號, 這樣就能讓內網中的裝置共用同一個外網IP. 舉個例子, NAPT維護一個類似下表的NAT表

內網地址 外網地址
192.168.0.2:5566 120.132.92.21:9200
192.168.0.3:7788 120.132.92.21:9201
192.168.0.3:8888 120.132.92.21:9202

NAT裝置會根據NAT表對出去和進來的資料做修改, 比如將192.168.0.3:8888發出去的封包改成120.132.92.21:9202, 外部就認為他們是在和120.132.92.21:9202通訊. 同時NAT裝置會將120.132.92.21:9202收到的封包的IP和埠改成192.168.0.3:8888, 再發給內網的主機, 這樣內部和外部就能雙向通訊了, 但如果其中192.168.0.3:8888 == 120.132.92.21:9202這一對映因為某些原因被NAT裝置淘汰了, 那麼外部裝置就無法直接與192.168.0.3:8888通訊了.

我們的裝置經常是處在NAT裝置的後面, 比如在大學裡的校園網, 查一下自己分配到的IP, 其實是內網IP, 表明我們在NAT裝置後面, 如果我們在寢室再接個路由器, 那麼我們發出的資料包會多經過一次NAT.

國內移動無線網路運營商在鏈路上一段時間內沒有資料通訊後, 會淘汰NAT表中的對應項, 造成鏈路中斷.

網路狀態切換

手機網路和WIFI網路切換, 網路斷開和連上等情況, 也會使長連線斷開. 這裡原因可能比較多, 但結果無非就是IP變了, 或者被系統通知連線斷了.

DHCP的租期

目前測試發現安卓系統對DHCP的處理有Bug, DHCP租期到了不會主動續約並且會繼續使用過期IP, 這個問題會造成TCP長連線偶然的斷連.

引自Android微信智慧心跳方案

心跳包的作用

網上很多文章介紹長連線的時候都說:

因為是長連線, 所以需要定期傳送心跳包.
心跳包是用來通知伺服器客戶端當前狀態.

提出這些說法的人其實自己也是一知半解. 這些說法其實都對, 但是沒有答到點上. 就好像別人問: “你為什麼要去食堂”? 這人回答: “檢查自己還能不能找到食堂”. 這個答案說不上錯了, 但是其實這人是去食堂吃飯的, 證明自己認得路只是個附贈品.

明確一點, TCP長連線本質上不需要心跳包來維持, 大家可以試一試, 讓兩臺電腦連上同一個wifi, 然後讓其中一臺做伺服器, 另一臺用一個普通的沒有設定KeepAlive的Socket連上伺服器, 只要兩臺電腦別斷網, 路由器也別斷電, DHCP正常續租, 就這麼放著, 過幾個小時再用其中一臺電腦通過之前建立的TCP連線給另一臺發訊息, 另一臺肯定能收到.

那為什麼要有心跳包呢? 其實主要是為了防止上面提到的NAT超時, 既然一些NAT裝置判斷是否淘汰NAT對映的依據是一定時間沒有資料, 那麼客戶端就主動發一個資料.

當然, 如果僅僅是為了防止NAT超時, 可以讓伺服器來傳送心跳包給客戶端, 不過這樣做有個弊病就是, 萬一連線斷了, 伺服器就再也聯絡不上客戶端了. 所以心跳包必須由客戶端傳送, 客戶端發現連線斷了, 還可以嘗試重連伺服器.

所以心跳包的主要作用是防止NAT超時, 其次是探測連線是否斷開.

鏈路斷開, 沒有寫操作的TCP連線是感知不到的, 除非這個時候傳送資料給伺服器, 造成寫超時, 否則TCP連線不會知道斷開了. 主動kill掉一方的程式, 另一方會關閉TCP連線, 是系統代程式給伺服器發的FIN. TCP連線就是這樣, 只有明確的收到對方發來的關閉連線的訊息(收到RST也會關閉, 大家都懂), 或者自己意識到發生了寫超時, 否則它認為連線還存在.

心跳包的時間間隔

既然心跳包的主要作用是防止NAT超時, 那麼這個間隔就大有文章了.

傳送心跳包勢必要先喚醒裝置, 然後才能傳送, 如果喚醒裝置過於頻繁, 或者直接導致裝置無法休眠, 會大量消耗電量, 而且行動網路下進行網路通訊, 比在wifi下耗電得多. 所以這個心跳包的時間間隔應該儘量的長, 最理想的情況就是根本沒有NAT超時, 比如剛才我說的兩臺在同一個wifi下的電腦, 完全不需要心跳包. 這也就是網上常說的長連線, 慢心跳.

現實是殘酷的, 根據網上的一些說法, 中移動2/3G下, NAT超時時間為5分鐘, 中國電信3G則大於28分鐘, 理想的情況下, 客戶端應當以略小於NAT超時時間的間隔來傳送心跳包.

wifi下, NAT超時時間都會比較長, 據說寬頻的閘道器一般沒有空閒釋放機制, GCM有些時候在wifi下的心跳比在行動網路下的心跳要快, 可能是因為wifi下聯網通訊耗費的電量比行動網路下小.

關於如何讓心跳間隔逼近NAT超時的間隔, 同時自動適應NAT超時間隔的變化, 可以參看Android微信智慧心跳方案.

心跳包和輪詢的區別

心跳包和輪詢看起來類似, 都是客戶端主動聯絡伺服器, 但是區別很大.

  • 輪詢是為了獲取資料, 而心跳是為了保活TCP連線.
  • 輪詢得越頻繁, 獲取資料就越及時, 心跳的頻繁與否和資料是否及時沒有直接關係
  • 輪詢比心跳能耗更高, 因為一次輪詢需要經過TCP三次握手, 四次揮手, 單次心跳不需要建立和拆除TCP連線.

TCP喚醒Android

這部分內容我只知道結論, 不知道具體的知識
大家有沒有想過, 手機的簡訊功能和微信的功能差不多, 為什麼微信會比簡訊耗電這麼多? 當然不是因為簡訊一條0.1元. 手機簡訊是通過什麼獲取推送的呢?
下面這段出處不明的話也許可以給大家啟示

首先Android手機有兩個處理器, 一個叫Application Processor(AP), 一個叫Baseband Processor(BP). AP是ARM架構的處理器,用於執行Android系統; BP用於執行實時作業系統(RTOS), 通訊協議棧執行於BP的RTOS之上. 非通話時間, BP的能耗基本上在5mA左右,而AP只要處於非休眠狀態, 能耗至少在50mA以上, 執行圖形運算時會更高. 另外LCD工作時功耗在100mA左右, WIFI也在100mA左右. 一般手機待機時, AP, LCD, WIFI均進入休眠狀態, 這時Android中應用程式的程式碼也會停止執行.

Android為了確保應用程式中關鍵程式碼的正確執行, 提供了Wake Lock的API, 使得應用程式有許可權通過程式碼阻止AP進入休眠狀態. 但如果不領會Android設計者的意圖而濫用Wake Lock API, 為了自身程式在後臺的正常工作而長時間阻止AP進入休眠狀態, 就會成為待機電池殺手.

完全沒必要擔心AP休眠會導致收不到訊息推送. 通訊協議棧執行於BP,一旦收到資料包, BP會將AP喚醒, 喚醒的時間足夠AP執行程式碼完成對收到的資料包的處理過程. 其它的如Connectivity事件觸發時AP同樣會被喚醒. 那麼唯一的問題就是程式如何執行向伺服器傳送心跳包的邏輯. 你顯然不能靠AP來做心跳計時. Android提供的Alarm Manager就是來解決這個問題的. Alarm應該是BP計時(或其它某個帶石英鐘的晶片,不太確定,但絕對不是AP), 觸發時喚醒AP執行程式程式碼. 那麼Wake Lock API有啥用呢? 比如心跳包從請求到應答, 比如斷線重連重新登陸這些關鍵邏輯的執行過程, 就需要Wake Lock來保護. 而一旦一個關鍵邏輯執行成功, 就應該立即釋放掉Wake Lock了. 兩次心跳請求間隔5到10分鐘, 基本不會怎麼耗電. 除非網路不穩定. 頻繁斷線重連, 那種情況辦法不多.

上面所說的通訊協議, 我猜應該是無線資源控制協議(Radio Resource Control), RRC應該工作在OSI參考模型中的第三層網路層, 而TCP, UDP工作在第四層傳輸層, 上文說的BP, 應該就是手機中的基帶, 也有叫Radio的, 我有點搞不清楚Radio怎麼翻譯. Google在Optimizing Downloads for Efficient Network Access中提到了一個叫Radio State Machine的東西, 我翻譯成無線電波狀態機, 也不知道正確的翻譯是什麼.

行動網路下, 每一個TCP連線底層都應該是有RRC連線, 而RRC連線會喚醒基帶, 基帶會喚醒CPU處理TCP資料, 這是我個人的理解.

至於wifi下如何工作, 我暫時沒有找到資料.

上面說了這麼多, 其實意思就是TCP資料包能喚醒手機. 至於UDP, 我不確定.

而推送中最重要的部分就是讓手機儘量休眠, 只有在伺服器需要它處理資料時才喚醒它, 這正好符合我們的要求.

行動網路下的耗電

Google在Optimizing Downloads for Efficient Network Access中提到了一個叫Radio State Machine的東西.

Android推送技術研究

mobile radio state machine

說的應該就是基帶的工作狀態, 在Radio Standby下幾乎不耗電, 但是一旦有需要處理的事情, 比如手機裡某個app要訪問網路(從上一節可以推測: 收到RRC指令也會導致喚醒), 就會進入到Radio Full Power中, 由Standby轉為Full Power這一喚醒過程很耗電, Full Power下基帶空閒後5s進入Radio Low Power, 如果又空閒12s才進入Standby. 主要的意思就是不要頻繁的喚醒基帶去請求網路, 因為只要一喚醒, 就至少會讓基帶在Full Power下工作5s, 在Low Power下工作12s, 而且喚醒過程很耗電. 所以在行動網路下, 心跳需要儘量的慢才好, 不過以當前這種情況, 想慢下來幾乎不可能.

不過這也帶來另外一個問題, 假如手機裡有10個應用, 每個應用都傳送心跳包, 每個應用的伺服器都可能喚醒手機, 那手機還休不休眠了?

實際實現遇到的問題

瞭解完了我就開始動手做demo, 伺服器使用Apache的Mina, 客戶端也用這個

Mina

這個框架挺好用, 就是遇到些很奇怪的事情, 我兩天前看的, 所以也可能是我自己的問題.

一個是Android端發一個漢字給伺服器, 伺服器filter崩潰, 發超過一個漢字, 客戶端filter崩潰, 寫個IoFilter做一下編解碼就好了. 另外User Guide裡面的程式碼也有錯誤. 第二個是IoSessionConfig的寫超時設定了完全不起作用.

小米手機的神奇Socket

後來又發現客戶端只要在後臺超過一定時間, 對socket的寫操作就會變得非常詭異, 表現為socket把資料吞了, 告知應用資料已經被對方接收, 但是伺服器什麼都沒收到, 而且伺服器傳送的訊息客戶端也收不到. 只要讓app進到前臺, 之前消失的資料會一股腦發給伺服器, 客戶端會收到伺服器重傳的訊息.

我開始還以為是Android的休眠機制把wifi斷了, 我把各種WifiLock, WakeLock都持有了, 還是出這種情況. 後來無意間發現小米針對每個app都有個後臺執行時允許聯網的開關, 我把它開啟了, 果然好了一陣子, 後來又開始重複之前的情況, 我還以為是Mina的IO執行緒被kill了還是怎麼, 用DDMS看了執行緒資訊沒問題. 不放心, 又用純Socket實現了客戶端, 還是有問題, 再在之前的基礎上加上1分鐘的心跳, 還是有問題.

小米手機的神奇bug

這次真是我運氣好, 我又看了一眼後臺執行時允許聯網的開關, 發現demo app的這個開關剛剛還被我開啟了, 這下又關上了, 我懷疑是小米的這個功能有bug, 我是記得有小米員工提到這東西有伺服器下發白名單的, 我認為是伺服器下發資料把我的改動給覆蓋了, 我把幾個app的後臺聯閘道器了, 重啟手機之後, 他們又開了.

最後我改了個10s的心跳間隔, 在心跳的時候, 把後臺允許聯閘道器掉, 復現了那個神奇的socket行為, 大概確定是MIUI的bug.

睡了一覺起來, MIUI的工程師聯絡了我, 確認是bug. 順便提醒一下用小米做測試機的開發者和使用者, 這個bug的臨時解決方案是: 用神隱模式裡的自定義配置, 把自己想改的設定好就行.

想起一年前什麼都不懂就跑去小米麵試就好笑, 我這水平完全就是坑人, 然而沒想到這次被小米坑了.

我心中最佳的推送技術

RRC那套東西, 你懂的, 基帶Standby模式下也保持著的連線.

相關文章