Android即時通訊系列文章(2)網絡通信協議選型:應以什么樣的標準去選擇適合你應用的網絡通信協議?

前言

在前一篇文章《多進程:為什么要把消息服務拆分到一個獨立的進程?》中我們出于保證連接的穩定性的目的,將應用拆分成了「主進程」和「通訊進程」,并為二者定義了相互通信的接口。即便如此,我們也只是實現了客戶端一側的進程間通信,而要實現與完整聊天系統中另一端的角色——服務端的通信,則需依靠「網絡通信協議」來協助完成,在此我們選用的是WebSocket協議。

什么是WebSocket?

WebSocket一詞,從詞面上可以拆解為 Web & Socket 兩個單詞,Socket我們并不陌生,其是對處于網絡中不同主機上的應用進程之間進行雙向通信的端點的抽象,是應用程序通過網絡協議進行通信的接口,一個Socket對應著通信的一端,由IP地址和端口組合而成。需要注意的是,Socket并不是具體的一種協議,而是一個邏輯上的概念。

那么WebSocket和Socket之間存在著什么聯系呢,是否可以理解為是Socket概念在Web環境的移植呢?為了解答這個疑惑,我們先來回顧一下,在Java平臺上進行Socket編程的流程:

  1. 服務端創建ServerSocket實例并綁定本地端口進行監聽
  2. 客戶端創建Socket實例并指定要連接的服務端的IP地址和端口
  3. 客戶端發起連接請求,服務端成功接受之后,雙方就建立了一個端對端的TCP連接,在該連接上可以雙向通信。而后服務端繼續處于監聽狀態,接受其他客戶端的連接請求。

上述流程還可以簡化為:

  1. 服務端監聽
  2. 客戶端請求
  3. 連接確認

與之類似,WebSocket服務端與客戶端之間的通信過程可以描述為:

  • 服務端創建包含有效主機與端口的WebSocket實例,隨后啟動并等待客戶端連接
  • 客戶端創建WebSocket實例,并為該實例提供一個URL,該URL代表希望連接的服務器端點
  • 客戶端通過HTTP請求握手建立連接之后,后面就使用剛才發起HTTP請求的TCP連接進行雙向通信。


    1.png

WebSocket協議最初是HTML5規范的一部分,但后來移至單獨的標準文檔中以使規范集中化,其借鑒了Socket的思想,通過單個TCP連接,為Web瀏覽器端與服務端之間提供了一種全雙工通信機制。WebSocket協議旨在與現有的Web基礎體系結構良好配合,基于此設計原則,協議規范定義了WebSocket協議握手流程需借助HTTP協議進行,并被設計工作在與HTTP(80)和HTTPS(443)相同的端口,也支持HTTP代理和中間件,以保證能完全向后兼容。

由于WebSocket本身只是一個應用層協議,原則上只要遵循這個協議的客戶端均可使用,因此我們才得以將其運用到我們的Android客戶端。

什么是全雙工通信?

簡單來講,就是通信雙方(客戶端和服務端)可同時向對方發送消息。為什么這一點很重要呢?因為傳統的基于HTTP協議的通信是單向的,只能由客戶端發起,服務端無法主動向客戶端推送信息。一旦面臨即時通訊這種對數據實時性要求很高的場景,當服務端有數據更新而客戶端要獲知,就只能通過客戶端輪詢的方式,具體又可分為以下兩種輪詢策略:

  • 短輪詢
    即客戶端定時向服務端發送請求,服務端收到請求后馬上返回響應并關閉連接。
    優點:實現簡單
    缺點:
    1.并發請求對服務端造成較大壓力
    2.數據可能沒有更新,造成無效請求
    3.頻繁的網絡請求導致客戶端設備電量、流量快速消耗
    4.定時操作存在時間差,可能造成數據同步不及時
    5.每次請求都需要攜帶完整的請求頭
2.png
  • 長輪詢
    即服務端在收到請求之后,如果數據無更新,會阻塞請求,直至數據更新或連接超時才返回。
    優點:相較于短輪詢減少了HTTP請求的次數,節省了部分資源。
    缺點:
    1.連接掛起同樣會消耗資源
    2.冗余請求頭問題依舊存在
    3.png

與上述兩個方案相比,WebSocket的優勢在于,當連接建立之后,后續的數據都是以幀的形式發送。除非某一端主動斷開連接,否則無需重新建立連接。因此可以做到:

1.減輕服務器的負擔
2.極大地減少不必要的流量、電量消耗
3.提高實時性,保證客戶端和服務端數據的同步
4.減少冗余請求頭造成的開銷

4.png
5.png

除了WebSocket,實現移動端即時通訊的還有哪些技術?

  • XMPP
    全稱(Extensible Messaging and Presence Protocol,可擴展通訊和表示協議),是一種基于XML的協議,它繼承了在XML環境中靈活的發展性。
    XMPP中定義了三個角色,客戶端,服務器,網關。通信能夠在這三者的任意兩個之間雙向發生。服務器同時承擔了客戶端信息記錄,連接管理和信息的路由功能。網關承擔著與異構即時通信系統的互聯互通,異構系統可以包括SMS(短信),MSN,ICQ等。基本的網絡形式是單客戶端通過TCP/IP連接到單服務器,然后在之上傳輸XML。
    優點
    1.超強的可擴展性。經過擴展以后的XMPP可以通過發送擴展的信息來處理用戶的需求。
    2.易于解析和閱讀。方便了開發和查錯。
    3.開源。在客戶端、服務器、組件、源碼庫等方面,都已經各自有多種實現。
    缺點
    1.數據負載太重。過多的冗余標簽、低效的解析效率使得XMPP在移動設備上表現不佳。

應用場景舉例:點對點單聊約球
我剛畢業時入職的公司曾接手開發一個線上足球約戰的社交平臺APP項目,當時為了提高約球時的溝通效率,考慮為應用引入聊天模塊,并優先實現點對點單聊功能。那時市面上的即時通訊SDK方案還尚未成熟,綜合當時團隊成員的技術棧,決定采用XMPP+Openfire+Smack作為自研技術搭建聊天框架。
Openfire基于XMPP協議,采用Java開發,可用于構建高效的即時通信服務器端,單臺服務器可支持上萬并發用戶。Openfire安裝和使用都非常簡單,并利用Web進行管理。由于是采用開放的XMPP協議,因此可以使用各種支持XMPP協議的IM客戶端軟件登錄服務。
Smack是一個開源的、易于使用的XMPP客戶端Java類庫,提供了一套可擴展的API。

  • MQTT
    全稱(Message Queuing Telemetry Transport,消息隊列遙測傳輸協議),是一種基于發布/訂閱模式的“輕量級”通訊協議,其構建于TCP/IP協議之上。MQTT最大優點在于,可以以極少的代碼和有限的帶寬,為連接遠程設備提供實時可靠的消息服務。作為一種低開銷、低帶寬占用的即時通訊協議,使其在物聯網、小型設備、移動應用等方面有較廣泛的應用。
    特點
    1.基于發布/訂閱模型。提供一對多的消息發布,解除應用程序耦合。
    2.低開銷。MQTT客戶端很輕巧,只需要最少的資源,同時MQTT消息頭也很小,可以優化網絡帶寬。
    3.可靠的消息傳遞。MQTT定義了3種消息發布服務質量,以支持消息可靠性:至多一次,至少一次,只有一次。
    4.對不可靠網絡的支持。專為受限設備和低帶寬、高延遲或不可靠的網絡而設計。

應用場景舉例:賠率更新、賽事直播聊天室
我第二家入職的公司的主打產品是一款提供模擬競猜、賽事直播的體育類APP,其中核心的功能模塊就是提供各種賽事的最新比分賠率數據,最初采用的即是上文所說的低效的HTTP輪詢方案,效果可想而知。后面技術重構后改用了MQTT,極大地減少了對網絡環境的依賴,提高了數據的實時性和可靠性。再往后搭建直播模塊時,考慮到聊天室這種一對多的消息發布場景同樣適合用MQTT解決,于是沿用了原先的技術方案擴展了新的聊天室模塊。

  • WebSocket
    而相較之下,WebSocket的特點包括:
    1.較少的控制開銷。在連接創建后,服務器和客戶端之間交換數據時,用于協議控制的數據包頭部相對較小。
    2.更好的二進制支持。Websocket定義了二進制幀,相對HTTP,可以更輕松地處理二進制內容。
    3.可以支持擴展。Websocket定義了擴展,用戶可以擴展協議、實現部分自定義的子協議,如以上所說的XMPP協議、MQTT協議等。

WebSocket協議在Android客戶端的實現

實現WebSocket協議很簡單,廣為Android開發者使用的網絡請求框架——OkHttp對WebSocket通信流程進行了封裝,提供了簡明的接口用于WebSocket的連接建立、數據收發、連接保活、連接關閉等,使我們可以專注于業務實現而無須關注通信細節,簡單到我們只需要實現以下兩步:

  • 創建WebSocket實例并提供一個URL以指定要連接的服務器地址
  • 提供一個WebSocket連接事件監聽器,用于監聽事件回調以處理連接生命周期的每個階段

WebSocket URL的構成與Http URL很相似,都是由協議、主機、端口、路徑等構成,區別就是WebSocket URL的協議名采用的是ws://和wss://,wss://表明是安全的WebSocket連接。

6.png

首先我們在項目中引入OkHttp庫的依賴:

implementation("com.squareup.okhttp3:okhttp:4.9.0")

其次,我們須指定要連接的服務器地址,此處可以使用WebSocket的官方服務器地址:

/** WebSocket服務器地址 */
private var serverUrl: String = "ws://echo.websocket.org"

@Synchronized
fun connect() {
    val request = Request.Builder().url(serverUrl).build()
    val okHttpClient = OkHttpClient.Builder().callTimeout(20, TimeUnit.SECONDS).build()  
    ...
}

接著,我們調用OkHttpClient實例的newWebSocket(request: Request, listener: WebSocketListener)方法,該方法需傳入兩個參數,第一個是上文構建的Request對象,第二個是WebSocket連接事件的監聽器,WebSocket協議包含四個主要的事件:

  • Open:客戶端和服務器之間建立了連接后觸發
  • Message:服務端向客戶端發送數據時觸發。發送的數據可以是純文本或二進制數據
  • Close:服務端與客戶端之間的通信結束時觸發。
  • Error:通信過程中發生錯誤時觸發。

每個事件都通過分別實現對應的回調來進行處理。OkHttp提供的監聽器包含以下回調:

abstract class WebSocketListener {
  open fun onOpen(webSocket: WebSocket, response: Response) {}
  open fun onMessage(webSocket: WebSocket, text: String) {}
  open fun onMessage(webSocket: WebSocket, bytes: ByteString) {}
  open fun onClosing(webSocket: WebSocket, code: Int, reason: String) {}
  open fun onClosed(webSocket: WebSocket, code: Int, reason: String) {}
  open fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {}
}
object WebSocketConnection : WebSocketListener()
    @Synchronized
    fun connect() {
        ...
        webSocketClient = okHttpClient.newWebSocket(request, this)
    }
    ...
}

以上的事件通常在連接狀態發生變化時被動觸發,另一方面,如果用戶想主動執行某些操作,WebSocket也提供了相應的接口以給用戶顯式調用。WebSocket協議包含兩個主要的操作:

  • send( ) :向服務端發送消息,包括文本或二進制數據
  • close( ):主動請求關閉連接。

可以看到,OkHttp提供的WebSocket接口也提供了這兩個方法:

interface WebSocket {
    ...
    fun send(text: String): Boolean
    fun send(bytes: ByteString): Boolean
    fun close(code: Int, reason: String?): Boolean
    ...
}    

當onOpen方法回調時,即是連接建立成功,可以傳輸數據了。此時我們便可以調用WebSocket實例的send()方法發送文本消息或二進制消息,WebSocket官方服務器會將數據通過onMessage(webSocket: WebSocket, bytes: ByteString)或onMessage(webSocket: WebSocket, text: String)回調原樣返回給我們。

WebSocket是如何建立連接的?

我們可以通過閱讀OkHttp源碼獲知,newWebSocket(request: Request, listener: WebSocketListener)方法內部是創建了一個RealWebSocket實例,該類是WebSocket接口的實現類,創建實例成功后便調用connect(client: OkHttpClient)方法開始異步建立連接。

override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
  val webSocket = RealWebSocket(
      taskRunner = TaskRunner.INSTANCE,
      originalRequest = request,
      listener = listener,
      random = Random(),
      pingIntervalMillis = pingIntervalMillis.toLong(),
      extensions = null, // Always null for clients.
      minimumDeflateSize = minWebSocketMessageToCompress
  )
  webSocket.connect(this)
  return webSocket
}

連接建立的過程主要是向服務器發送了一個HTTP請求,該請求包含了額外的一些請求頭信息:

val request = originalRequest.newBuilder()
    .header("Upgrade", "websocket")
    .header("Connection", "Upgrade")
    .header("Sec-WebSocket-Key", key)
    .header("Sec-WebSocket-Version", "13")
    .header("Sec-WebSocket-Extensions", "permessage-deflate")
    .build()

這些請求頭的意義如下:

Connection: Upgrade:表示要升級協議
Upgrade: websocket:表示要升級到websocket協議。
Sec-WebSocket-Version:13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,里面包含服務端支持的版本號。
Sec-WebSocket-Key:與后面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。

當返回的狀態碼為101時,表示服務端同意客戶端協議轉換請求,并將其轉換為Websocket協議,該過程稱之為Websocket協議握手(websocket Protocol handshake),協議升級完成后,后續的數據交換則遵照WebSocket的協議。

前面我們一直說「握手」,握手究竟指的是什么呢?在計算機領域的語境中,握手通常是指確保服務器與其客戶端同步的過程。握手是WebSocket協議的基本概念。

為了直觀展示,以上實例中傳輸的消息均以文本為例,WebSocket還支持二進制數據的傳輸,而這就要依靠「數據傳輸協議」來完成了,這是下一篇文章的內容,敬請期待。

總結

為了完成與服務端的雙向通信,我們選取了WebSocket協議作為網絡通信協議,并通過對比傳統HTTP協議和其他相關的即時通訊技術,總結出,在為移動設備下應用選擇的合適的網絡通信協議時,可以有以下的參考標準:

  • 支持全雙工通信
  • 支持二進制數據傳輸
  • 支持擴展
  • 跨語言、跨平臺實現

同時,也對WebSocket協議在Android端的實現提供了示例,并對WebSocket協議握手流程進行了初步窺探,當然,這只是第一步,往后的心跳保活、斷線重連、消息隊列等每一個都可以單獨作為一個課題,會在后面陸續推出的。

參考

WebSocket 官網
http://www.websocket.org/index.html
WebSocket 百度百科
https://baike.baidu.com/item/WebSocket/1953845?fr=aladdin
學習WebSocket簡單易學
https://www.tutorialspoint.com/websockets/index.htm
WebSocket詳解(一):初步認識WebSocket技術
http://www.52im.net/thread-331-1-1.html
輪詢、長輪詢、長連接、websocket
https://www.cnblogs.com/huchong/p/8595644.html
WEB端即時通訊:HTTP長連接、長輪詢(long polling)詳解
http://www.52im.net/thread-224-1-1.html
WebSockets vs 長輪詢(一)
https://webrtc.org.cn/20190704-webrtc-protocol/
MQTT 入門介紹
https://www.runoob.com/w3cnote/mqtt-intro.html
Android網絡編程要學的東西與Http協議學習
https://www.kancloud.cn/kancloud/android-tutorial/87221

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容