# 推送原理解析
原文鏈接:[LRH1993_github](https://raw.githubusercontent.com/LRH1993/android_interview/master/android/advance/push.md)、[HTML版](https://lrh1993.gitbooks.io/android_interview_guide/content/android/advance/push.html)
## Android推送服務的幾種實現方式
現實生活中, 推送服務就像訂雜志一樣, 只要留下你的地址, 雜志就能如期送到你手里, 可以認為每個人都有唯一的一個地址, 但在目前的網絡上, 這是辦不到的, 因為不是每個人都有一個唯一的地址, 服務器想要給我們推送一條消息, 必須知道我們的地址, 但服務器不知道我們在哪.
說到推送服務, 我所知道的實現方案有如下幾種:
### 輪詢
客戶端定期詢問服務器有沒有新的消息, 這樣服務器不用管客戶端的地址是什么, 客戶端來問, 直接告訴它就行.
這種方案最簡單, 對于一些不追求實時性的客戶端來說, 很適合, 只需要把時間間隔設定成幾個小時取一次, 就能很方便的解決問題.
但對于即時通訊產品來說, 這種方案完全不能用. 假設即時通訊軟件在網絡暢通的情況下發送的消息要求對方10s內就能收到, 如果用輪詢, 那么客戶端要每隔5s連一次服務器, 如果在移動端, 手機的電量和流量很快就會被消耗殆盡.
### SMS通知
這種方案在移動端是有可能的, 讓客戶端攔截手機短信, 服務器在有新消息時給用戶的手機號發一條特殊的短信, 客戶端攔截短信后發現是正常短信就放行, 如果是特殊短信就連接服務器取消息.
運營商不會配合, 用戶也不會放心, 這方案普通公司玩不起.
### 長連接
這大概是目前情況下最佳的方案了, 客戶端主動和服務器建立TCP長連接之后, 客戶端定期向服務器發送心跳包, 有消息的時候, 服務器直接通過這個已經建立好的TCP連接通知客戶端.
### XMPP, MQTT等不算推送技術
在網上搜索資料的時候, 經常看見*XMPP協議實現的Android推送*和*MQTT協議實現的Android推送*, 我個人覺得這兩種說法都怪怪的, XMPP和MQTT二者都是協議, 盡管我不清楚嚴格來講這倆協議工作在哪一層, 但是絕對是在傳輸層之上的, 姑且認為他倆在TCP/IP四層模型的應用層吧, 閉口不提傳輸層的實現, 而是扯應用層, 這種說法真是令我費解, 所以我個人認為XMPP, MQTT等等不算推送技術.
> 關于為什么TCP/IP是四層模型, 感謝評論區指出, 對應的是 應用層, 傳輸層, 網絡層, 網絡接口層, 也有說法把網絡接口層分成兩層, 這樣就有了五層, 因為TCP/IP是事實上的模型, 所以說法不一很正常, 主流說法是四層.
關于這個XMPP, 我想很多人都是參考Openfire和Smack那套東西, 我一年前嘗試用aSmack和Openfire做IM, 不過那個時候什么都不懂, 做的東西很爛, 唯一懂的就是Openfire這東西相當老了, 我看有一些開源的推送解決方案都是在這套東西的基礎上改的, 想想這工作量, 挺可怕的.
## 細說TCP長連接與心跳
長連接方案乍一聽怪怪的, 什么是長連接? 定時發送心跳, 這和輪詢有什么區別? 心跳是干什么的? 同樣是定期和服務器溝通, 為什么長連接就比輪詢更加優秀? 手機休眠了TCP連接不會斷掉嗎?
這是我在剛開始研究推送技術的時候的問題, 雖然有些還是沒有很準確的答案, 但了解的大概可以分享一下, 有什么錯誤歡迎指出.
### 什么是長連接
先說短連接, 短連接是通訊雙方有數據交互時就建立一個連接, 數據發送完成后,則斷開此連接.

長連接就是大家建立連接之后, 不主動斷開. 雙方互相發送數據, 發完了也不主動斷開連接, 之后有需要發送的數據就繼續通過這個連接發送.
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微信智能心跳方案](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207243549&idx=1&sn=4ebe4beb8123f1b5ab58810ac8bc5994&scene=4#wechat_redirect)
### 心跳包的作用
網上很多文章介紹長連接的時候都說:
> 因為是長連接, 所以需要定期發送心跳包.
> 心跳包是用來通知服務器客戶端當前狀態.
提出這些說法的人其實自己也是一知半解. 這些說法其實都對, 但是沒有答到點上. 就好像別人問: "你為什么要去食堂"? 這人回答: "檢查自己還能不能找到食堂". 這個答案說不上錯了, 但是其實這人是去食堂吃飯的, 證明自己認得路只是個附贈品.
明確一點, 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微信智能心跳方案](http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207243549&idx=1&sn=4ebe4beb8123f1b5ab58810ac8bc5994&scene=4#wechat_redirect).
### 服務器如何處理心跳包
如果客戶端心跳間隔是固定的, 那么服務器在連接閑置超過這個時間還沒收到心跳時, 可以認為對方掉線, 關閉連接. 如果客戶端心跳會動態改變, 如上節提到的微信心跳方案, 應當設置一個最大值, 超過這個最大值才認為對方掉線. 還有一種情況就是服務器通過TCP連接主動給客戶端發消息出現寫超時, 可以直接認為對方掉線.
這個就需要具體業務具體分析了, 也許還有更優的策略, 這里就不寫了.
### 心跳包和輪詢的區別
心跳包和輪詢看起來類似, 都是客戶端主動聯系服務器, 但是區別很大.
- 輪詢是為了獲取數據, 而心跳是為了保活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](http://developer.android.com/training/efficient-downloads/efficient-network-access.html#RadioStateMachine)中提到了一個叫Radio State Machine的東西, 我翻譯成*無線電波狀態機*, 也不知道正確的翻譯是什么.
移動網絡下, 每一個TCP連接底層都應該是有RRC連接, 而RRC連接會喚醒基帶, 基帶會喚醒CPU處理TCP數據, 這是我個人的理解.
至于wifi下如何工作, 我暫時沒有找到資料.
上面說了這么多, 其實意思就是TCP數據包能喚醒手機. 至于UDP, 我不確定.
而推送中最重要的部分就是讓手機盡量休眠, 只有在服務器需要它處理數據時才喚醒它, 這正好符合我們的要求.
### 移動網絡下的耗電
Google在[Optimizing Downloads for Efficient Network Access](http://developer.android.com/training/efficient-downloads/efficient-network-access.html#RadioStateMachine)中提到了一個叫Radio State Machine的東西.
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個應用, 每個應用都發送心跳包, 每個應用的服務器都可能喚醒手機, 那手機還休不休眠了?
- 計算機基礎
- 簡答1
- 簡答2
- 專案
- 淺談0與1
- 淺談TCP_IP
- 淺談HTTP
- 淺談HTTPS
- 數據結構與算法
- 常見數據結構簡介
- 常用算法分析
- 常見排序算法
- Java數據結構類問題簡答
- 專案
- HashMap
- 淺談二叉樹
- 算法題
- 算法001_TopN問題
- 算法002_漢諾塔
- 編程思想
- 雜說
- 觀點_優秀程序設計的18大原則
- 設計模式_創建型
- 1_
- 2_
- 設計模式_結構型
- 1_
- 2_
- 設計模式_行為型
- 1_
- 2_
- Java相關
- 簡答1
- 簡答2
- 專案
- 淺談String
- 淺談Java泛型
- 淺談Java異常
- 淺談動態代理
- 淺談AOP編程
- 淺談ThreadLocal
- 淺談Volatile
- 淺談內存模型
- 淺談類加載
- 專案_數據結構
- 淺談SpareArray
- Android相關
- Android面試題
- 專案
- 推送原理解析
- Lint
- 自定義Lint
- Lint使用
- 優化案
- Apk體積優化
- Kotlin相關
- 簡答1
- 簡答2
- 三方框架相
- Okhttp3源碼分析
- ButterKnife源碼分析
- Glide4源碼分析
- Retrofit源碼分析
- RxJava源碼分析
- ARouter源碼分析
- LeakCanary源碼分析
- WMRouter源碼分析
- 跨平臺相關
- ReactNative
- Flutter
- Hybrid
- 優質源
- 資訊源
- 組件源
- 推薦