引言
在一個完善的即時通訊應用中,websocket是極其關鍵的一環,它為web應用的客戶端和服務端提供了一種全雙工的通信機制,但由於它本身以及其底層依賴的TCP連接的不穩定性,開發者不得不為其設計一套完整的保活、驗活、重連方案,才能在實際應用中保證應用的即時性和高可用性。就重連而言,其速度嚴重影響了上層應用的“即時性”和用戶體驗,試想打開網絡一分鍾后,微信還不能收發消息的話,是不是要抓狂?
因此,如何在網絡變更時快速恢復websocket的可用,就變得尤為重要。
快速了解
websocet
Websocket誕生於2008年,在2011年成為國際標准,現在所有的瀏覽器都已支持。它是一種全新的應用層協議,是專門為web客戶端和服務端設計的真正的全雙工通信協議,
可以類比HTTP協議來了解websocket協議。它們的不同點:
· HTTP的協議標識符是http,websocket的是ws
· HTTP請求只能由客戶端發起,服務器無法主動向客戶端推送消息,而websocket可以
· HTTP請求有同源限制,不同源之間通信需要跨域,而websocket沒有同源限制
相同點:
· 都是應用層的通信協議
· 默認端口一樣,都是80或443
· 都可以用於瀏覽器和服務器間的通信
· 都基於TCP協議
兩者和TCP的關系圖:

圖片來源
重連過程拆解
首先考慮一個問題,何時需要重連?
最容易想到的是websocket連接斷了,為了接下來能收發消息,我們需要再發起一次連接。但在很多場景下,即便websocket連接沒有斷開,實際上也不可用了,比如設備切換網絡、鏈路中間路由崩潰、服務器負載持續過高無法響應等,這些場景下的websocket都沒有斷開,但對上層來說,都沒辦法正常的收發數據了。因此在重連前,我們需要一種機制來感知連接是否可用、服務是否可用,而且要能快速感知,以便能夠快速從不可用狀態中恢復。
一旦感知到了連接不可用,那便可以棄舊圖新了,棄用並斷開舊連接,然后發起一次新連接。這兩個步驟看似簡單,但若想達到快,且不是那么容易的。
首先是斷開舊連接,對客戶端來說,如何快速快速斷開?協議規定客戶端必須要和服務器協商后才能斷開websocket連接,但是當客戶端已經聯系不上服務器、無法協商時,如何斷開並快速恢復?
其次是快速發起新連接。此快非彼快,這里的快並非是立即發起連接,立即發起連接會對服務器帶來不可預估的影響。重連時通常會采用一些退避算法,延遲一段時間后再發起重連。但如何在重連間隔和性能消耗間做出權衡?如何在“恰當的時間點”快速發起連接?
帶着這些疑問,我們來細看下這三個過程。

快速感知何時需要重連
需要重連的場景可以細分為三種,一是連接斷開了,二是連接沒斷但是不可用,三是連接對端的服務不可用了。
第一種場景很簡單,連接直接斷開了,肯定需要重連了。
而對於后兩者,無論是連接不可用,還是服務不可用,對上層應用的影響都是不能再收發即時消息了,所以從這個角度出發,感知何時需要重連的一種簡單粗暴的方法就是通過心跳包超時:發送一個心跳包,如果超過特定的時間后還沒有收到服務器回包,則認為服務不可用,如下圖中左側的方案;這種方法最直接。那如果想要
快速感知
呢,就只能多發心跳包,加快心跳頻率。但是心跳太快對移動端流量、電量的消耗又會太多,所以使用這種方法沒辦法做到快速感知,可以作為檢測連接和服務可用的兜底機制。

如果要檢測連接不可用,除了用心跳檢測,還可以通過判斷網絡狀態來實現,因為斷網、切換
wifi、切換網絡是導致連接不可用的最直接原因,所以在網絡狀態由offline變為online時,大多數情況下需要重連下,但也不一定,因為webscoket底層是基於TCP的,TCP連接不能敏銳的感知到應用層的網絡變化,所以有時候即便網絡斷開了一小會,對websocket連接是不會有影響的,網絡恢復后,仍然能夠正常地進行通信。因此在網絡由斷開到連接上時,立即判斷下連接是否可用,可以通過發一個心跳包判斷,如果能夠正常收到服務器的心跳回包,則說明連接仍是可用的,如果等待超時后仍沒有收到心跳回包,則需要重連,如上圖中的右側。這種方法的優點是速度快,在網絡恢復后能夠第一時間感知連接是否可用,不可用的話可以快速執行恢復,但它只能覆蓋應用層網絡變化導致websocket不可用的情況。
綜上,定時發送心跳包檢測的方案貴在穩定,能夠覆蓋所有場景,但速度不太可;而判斷網絡狀態的方案速度快,無需等待心跳間隔,較為靈敏,但覆蓋場景較為局限。因此,我們可以結合兩種方案:定時以不太快的頻率發送心跳包,比如40s/次、60s/次等,具體可以根據應用場景來定,然后在網絡狀態由offline變為online時立即發送一次心跳,檢測當前連接是否可用,不可用的話立即進行恢復處理。這樣在大多數情況下,上層的應用通信都能較快從不可用狀態中恢復,對於少部分場景,有定時心跳作為兜底,在一個心跳周期內也能夠恢復。
快速斷開舊連接
通常情況下,在發起下一次連接前,如果舊連接還存在的話,應該先把舊連接斷開,這樣一來可以釋放客戶端和服務器的資源,二來可以避免之后誤從舊連接收發數據。
我們知道websocket底層是基於TCP協議傳輸數據的,連接兩端分別是服務器和客戶端,而TCP的TIME_WAIT狀態是由服務器端維持的,因此在大多數正常情況下,應該由服務器發起斷開底層TCP連接,而不是客戶端。也就是說,要斷開websocket連接時,如果是服務器收到指示要斷開websocket,那它應該立即發起斷開TCP連接;如果是客戶端收到指示要斷開websocket,那它應該發信號給服務器,然后等待底層TCP連接被服務器斷開或直至超時。
那如果客戶端想要斷開舊的websocket,可以分websocket連接可用和不可用兩種情況來討論。當舊連接可用時,客戶端可以直接給服務器發送斷開信號,然后服務器發起斷開連接即可;當舊連接不可用時,比如客戶端切換了wifi,客戶端發送了斷開信號,但是服務器收不到,客戶端只能遲遲等待,直至超時才能被允許斷開。超時斷開的過程相對來說是比較久的,那有沒有辦法可以快點斷開?
上層應用無法改變只能由服務器發起斷開連接這種協議層面的規則,所以只能從應用邏輯入手,比如在上層通過業務邏輯保證舊連接完全失效,模擬連接斷開,然后在發起新連接,恢復通訊。這種方法相當於嘗試斷開舊連接不行時,直接棄之,然后就能快速進入下一流程,所以在使用時一定要確保在業務邏輯上舊連接已完全失效,比如:保證丟掉從舊連接收到所有數據、舊連接不能阻礙新連接的建立,舊連接超時斷開后不能影響新連接和上層業務邏輯等等。
快速發起新連接
有IM開發經驗的同學應該有所了解,遇到因網絡原因導致的重連時,是萬萬不能立即發起一次新連接的,否則當出現網絡抖動時,所有的設備都會立即同時向服務器發起連接,這無異於黑客通過發起大量請求消耗網絡帶寬引起的拒絕服務攻擊,這對服務器來說簡直是災難。所以在重連時通常采用一些退避算法,延遲一段時間再發起重連,如下圖中左側的流程。

如果要快速連上呢?最直接的做法就是縮短重試間隔,重試間隔越短,在網絡恢復后就能越快的恢復通訊。但是太頻繁的重試對性能、帶寬、電量的消耗就比較嚴重。如何在這之間做一個較好的權衡呢?
一種比較合理的方式是隨着重試次數增多,逐漸增大重試間隔;另一方面監聽網絡變化,在網絡狀態由offline變為online這種比較可能重連上的時刻,可以適當地減小重連間隔,如上圖中的右側(隨重試次數的增多,重連間隔也會變大),兩種方式配合使用。
除此之外,還可以結合業務邏輯,根據成功重連上的可能性適當的調整間隔,如網絡未連接時或應用在后台時重連間隔可以調大一些,網絡正常的狀態下可以適當調小一些等等,加快重連上的速度。
結尾
最后總結一下,本文在開頭將websocket斷網重連細分為三個步驟:確定何時需要重連、斷開舊連接和發起新連接。然后分別分析了在websocket的不同狀態下、不同的網絡狀態下,如何快速完成這個三個步驟:首先通過定時發送心跳包的方式檢測當前連接是否可用,同時監測網絡恢復事件,在恢復后立即發送一次心跳,快速感知當前狀態,判斷是否需要重連;其次正常情況下由服務器斷開舊連接,與服務器失去聯系時直接棄用舊連接,上層模擬斷開,來實現快速斷開;最后發起新連接時使用退避算法延遲一段時間再發起連接,同時考慮到資源浪費和重連速度,可以在網絡離線時調大重連間隔,在網絡正常或網絡由offline變為online時縮小重連間隔,使之盡可能快地重連上。
喜歡這篇文章?歡迎打賞~~