《移動IM開發指南》系列文章將會介紹一個IM APP的方方面面,包括技術選型、登陸優化等。此外,本文作者會結合他在網易雲信多年iOS IM SDK開發的經驗,深度分析實際開發中的各種常見問題。
推薦閱讀
心跳指令是什麼?
在使用 TCP 長連線的 IM 服務設計中,往往都會涉及到心跳。心跳一般是指某端(絕大多數情況下是客戶端)每隔一定時間向對端傳送自定義指令,以判斷雙方是否存活,因其按照一定間隔傳送,類似於心跳,故被稱為心跳指令。
為什麼需要在應用層做心跳?
那麼為什麼需要在應用層做心跳,難道 TCP 不是個可靠連線嗎?我們不能夠依賴 TCP 做斷線檢測嗎?比如使用 TCP 的 KeepAlive 機制來實現。應用層心跳是目前的最佳實踐嗎?怎麼樣的心跳才是最佳實踐?
是不是以前從來沒有仔細考慮過這些問題,僅僅只是個簡單的心跳而已啊!
對於客戶端而言,使用 TCP 長連線來實現業務的最大驅動力在於:在當前連線可用的情況下,每一次請求都只是簡單的資料傳送和接受,免去了 DNS 解析,連線建立等時間,大大加快了請求的速度,同時也有利於接受伺服器的實時訊息。
但前提是連線可用。如果連線無法很好地保持,每次請求就會變成撞大運:運氣好,通過長連線傳送請求並收到反饋。運氣差,當前連線已失效,請求遲遲沒有收到反饋直到超時,又需要一次連線建立的過程,其效率甚至還不如 HTTP。而連線保持的前提必然是檢測連線的可用性,並在連線不可用時主動放棄當前連線並建立新的連線。
基於這個前提,必須要有一種機制用於檢測連線可用性。同時行動網路的特殊性也要求客戶端需要在空餘時間傳送一定的信令,避免連線被回收。詳見《微信和運營商的撕B》。
而對於伺服器而言,能夠及時獲悉連線可用性也非常重要:一方面伺服器需要及時清理無效連線以減輕負載,另一方面也是業務的需求,如遊戲副本中伺服器需要及時處理玩家掉線帶來的問題。
上面說了保持連線的重要性,那麼現在回到具體實現上。為什麼我們需要使用應用層心跳來做檢測,而不是直接使用 TCP 的特性呢?
我們知道 TCP 是一個基於連線的協議,其連線狀態是由一個狀態機進行維護,連線完畢後,雙方都會處於 established 狀態,這之後的狀態並不會主動進行變化。這意味著如果上層不進行任何呼叫,一直使 TCP 連線空閒,那麼這個連線雖然沒有任何資料,但仍是保持連線狀態,一天,一星期,甚至一個月,即使在這期間中間路由崩潰重啟無數次。舉個現實中經常遇到的栗子:當我們 ssh 到自己的 VPS 上,然後不小心踢掉網線,此時的網路變化並不會被 TCP 檢測出,當我們重新插回網線,仍舊可以正常使用 ssh,同時此時並沒有發生任何 TCP 的重連。
有人會說 TCP 不是有 KeepAlive 機制麼,通過這個機制來實現不就可以了嗎?但是事實上,TCP KeepAlive 的機制其實並不適用於此。Keep Alive 機制開啟後,TCP 層將在定時時間到後傳送相應的 KeepAlive 探針以確定連線可用性。一般時間為 7200 s,失敗後重試 10 次,每次超時時間 75 s。顯然預設值無法滿足我們的需求,而修改過設定後就可以滿足了嗎?答案仍舊是否定的。因為 TCP KeepAlive 是用於檢測連線的死活,而心跳機制則附帶一個額外的功能:檢測通訊雙方的存活狀態。兩者聽起來似乎是一個意思,但實際上卻大相徑庭。考慮一種情況,某臺伺服器因為某些原因導致負載超高,CPU 100%,無法響應任何業務請求,但是使用 TCP 探針則仍舊能夠確定連線狀態,這就是典型的連線活著但業務提供方已死的狀態,對客戶端而言,這時的最好選擇就是斷線後重新連線其他伺服器,而不是一直認為當前伺服器是可用狀態,一直向當前伺服器傳送些必然會失敗的請求。
從上面我們可以知道,KeepAlive 並不適用於檢測雙方存活的場景,這種場景還得依賴於應用層的心跳。應用層心跳有著更大的靈活性,可以控制檢測時機,間隔和處理流程,甚至可以在心跳包上附帶額外資訊。從這個角度而言,應用層的心跳的確是最佳實踐。
如何實現心跳指令?
從上面我們可以得出結論,目前而言,應用層心跳的確是檢測連線有效性,雙方是否存活的最佳實踐,那麼剩下的問題就是怎麼實現。
最簡單粗暴做法當然是定時心跳,如每隔 30 秒心跳一次,15 秒內沒有收到心跳回包則認為當前連線已失效,斷開連線並進行重連。這種做法最直接,實現也簡單。唯一的問題是比較耗電和耗流量。以一個協議包 5 個位元組計算,一天收發 2880 個心跳包,一個月就是 5 * 2 * 2880 * 30 = 0.8 M 的流量,如果手機上多裝幾個 IM 軟體,每個月光心跳就好幾兆流量沒了,更不用說頻繁的心跳帶來的電量損耗。
既然頻繁心跳會帶來耗電和耗流量的弊端,改進的方向自然是減少心跳頻率,但也不能過於影響連線檢測的實時性。基於這個需求,一般可以將心跳間隔根據程式狀態進行調整,當程式在後臺時(這裡主要考慮安卓),儘量拉長心跳間隔,5 分鐘,甚至 10 分鐘都可以。而當 App 在前臺時則按照原來規則操作。連線可靠性的判斷也可以放寬,避免一次心跳超時就認為連線無效的情況,使用錯誤積累,只在心跳超時 n 次後才判定當前連線不可用。當然還有一些小 trick 比如從收到的最後一個指令包進行心跳包週期計時而不是固定時間,這樣也能夠一定程度減少心跳次數。
以上就是網易雲信對於心跳指令的理解和實踐,《移動IM開發指南》第三篇文章將為大家介紹如何優化登入模組,敬請期待。