日常開(kāi)發(fā)中我們出于保證連接的穩(wěn)定性的目的,將應(yīng)用拆分成了「主進(jìn)程」和「通訊進(jìn)程」,并為二者定義了相互通信的接口。
即便如此,我們也只是實(shí)現(xiàn)了客戶(hù)端一側(cè)的進(jìn)程間通信,而要實(shí)現(xiàn)與完整聊天系統(tǒng)中另一端的角色——服務(wù)端的通信,則需依靠「網(wǎng)絡(luò)通信協(xié)議」來(lái)協(xié)助完成
在此我們選用的是WebSocket協(xié)議。
什么是WebSocket?
WebSocket一詞,從詞面上可以拆解為 Web & Socket 兩個(gè)單詞,Socket我們并不陌生,其是對(duì)處于網(wǎng)絡(luò)中不同主機(jī)上的應(yīng)用進(jìn)程之間進(jìn)行雙向通信的端點(diǎn)的抽象,是應(yīng)用程序通過(guò)網(wǎng)絡(luò)協(xié)議進(jìn)行通信的接口,一個(gè)Socket對(duì)應(yīng)著通信的一端,由IP地址和端口組合而成。需要注意的是,Socket并不是具體的一種協(xié)議,而是一個(gè)邏輯上的概念。
那么WebSocket和Socket之間存在著什么聯(lián)系呢,是否可以理解為是Socket概念在Web環(huán)境的移植呢?為了解答這個(gè)疑惑,我們先來(lái)回顧一下,在JAVA平臺(tái)上進(jìn)行Socket編程的流程:
- 服務(wù)端創(chuàng)建ServerSocket實(shí)例并綁定本地端口進(jìn)行監(jiān)聽(tīng)
- 客戶(hù)端創(chuàng)建Socket實(shí)例并指定要連接的服務(wù)端的IP地址和端口
- 客戶(hù)端發(fā)起連接請(qǐng)求,服務(wù)端成功接受之后,雙方就建立了一個(gè)端對(duì)端的TCP連接,在該連接上可以雙向通信。而后服務(wù)端繼續(xù)處于監(jiān)聽(tīng)狀態(tài),接受其他客戶(hù)端的連接請(qǐng)求。
上述流程還可以簡(jiǎn)化為:
- 服務(wù)端監(jiān)聽(tīng)
- 客戶(hù)端請(qǐng)求
- 連接確認(rèn)
與之類(lèi)似,WebSocket服務(wù)端與客戶(hù)端之間的通信過(guò)程可以描述為:
- 服務(wù)端創(chuàng)建包含有效主機(jī)與端口的WebSocket實(shí)例,隨后啟動(dòng)并等待客戶(hù)端連接
- 客戶(hù)端創(chuàng)建WebSocket實(shí)例,并為該實(shí)例提供一個(gè)URL,該URL代表希望連接的服務(wù)器端
- 客戶(hù)端通過(guò)HTTP請(qǐng)求握手建立連接之后,后面就使用剛才發(fā)起HTTP請(qǐng)求的TCP連接進(jìn)行雙向通信。
WebSocket協(xié)議最初是html5規(guī)范的一部分,但后來(lái)移至單獨(dú)的標(biāo)準(zhǔn)文檔中以使規(guī)范集中化,其借鑒了Socket的思想,通過(guò)單個(gè)TCP連接,為Web瀏覽器端與服務(wù)端之間提供了一種全雙工通信機(jī)制。WebSocket協(xié)議旨在與現(xiàn)有的Web基礎(chǔ)體系結(jié)構(gòu)良好配合,基于此設(shè)計(jì)原則,協(xié)議規(guī)范定義了WebSocket協(xié)議握手流程需借助HTTP協(xié)議進(jìn)行,并被設(shè)計(jì)工作在與HTTP(80)和HTTPS(443)相同的端口,也支持HTTP代理和中間件,以保證能完全向后兼容。
由于WebSocket本身只是一個(gè)應(yīng)用層協(xié)議,原則上只要遵循這個(gè)協(xié)議的客戶(hù)端均可使用,因此我們才得以將其運(yùn)用到我們的Android客戶(hù)端。
【更多音視頻學(xué)習(xí)資料,點(diǎn)擊下方鏈接免費(fèi)領(lǐng)取↓↓,先碼住不迷路~】
點(diǎn)擊領(lǐng)取→音視頻開(kāi)發(fā)基礎(chǔ)知識(shí)和資料包
什么是全雙工通信?
簡(jiǎn)單來(lái)講,就是通信雙方(客戶(hù)端和服務(wù)端)可同時(shí)向?qū)Ψ桨l(fā)送消息。為什么這一點(diǎn)很重要呢?因?yàn)閭鹘y(tǒng)的基于HTTP協(xié)議的通信是單向的,只能由客戶(hù)端發(fā)起,服務(wù)端無(wú)法主動(dòng)向客戶(hù)端推送信息。一旦面臨即時(shí)通訊這種對(duì)數(shù)據(jù)實(shí)時(shí)性要求很高的場(chǎng)景,當(dāng)服務(wù)端有數(shù)據(jù)更新而客戶(hù)端要獲知,就只能通過(guò)客戶(hù)端輪詢(xún)的方式,具體又可分為以下兩種輪詢(xún)策略:
- 短輪詢(xún) 即客戶(hù)端定時(shí)向服務(wù)端發(fā)送請(qǐng)求,服務(wù)端收到請(qǐng)求后馬上返回響應(yīng)并關(guān)閉連接。 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單 缺點(diǎn): 1.并發(fā)請(qǐng)求對(duì)服務(wù)端造成較大壓力 2.數(shù)據(jù)可能沒(méi)有更新,造成無(wú)效請(qǐng)求 3.頻繁的網(wǎng)絡(luò)請(qǐng)求導(dǎo)致客戶(hù)端設(shè)備電量、流量快速消耗 4.定時(shí)操作存在時(shí)間差,可能造成數(shù)據(jù)同步不及時(shí) 5.每次請(qǐng)求都需要攜帶完整的請(qǐng)求頭
長(zhǎng)輪詢(xún) 即服務(wù)端在收到請(qǐng)求之后,如果數(shù)據(jù)無(wú)更新,會(huì)阻塞請(qǐng)求,直至數(shù)據(jù)更新或連接超時(shí)才返回。 優(yōu)點(diǎn):相較于短輪詢(xún)減少了HTTP請(qǐng)求的次數(shù),節(jié)省了部分資源。 缺點(diǎn): 1.連接掛起同樣會(huì)消耗資源 2.冗余請(qǐng)求頭問(wèn)題依舊存在
與上述兩個(gè)方案相比,WebSocket的優(yōu)勢(shì)在于,當(dāng)連接建立之后,后續(xù)的數(shù)據(jù)都是以幀的形式發(fā)送。除非某一端主動(dòng)斷開(kāi)連接,否則無(wú)需重新建立連接。因此可以做到:
1.減輕服務(wù)器的負(fù)擔(dān) 2.極大地減少不必要的流量、電量消耗 3.提高實(shí)時(shí)性,保證客戶(hù)端和服務(wù)端數(shù)據(jù)的同步 4.減少冗余請(qǐng)求頭造成的開(kāi)銷(xiāo)
除了WebSocket,實(shí)現(xiàn)移動(dòng)端即時(shí)通訊的還有哪些技術(shù)?
- XMPP 全稱(chēng)(Extensible Messaging and Presence Protocol,可擴(kuò)展通訊和表示協(xié)議),是一種基于XML的協(xié)議,它繼承了在XML環(huán)境中靈活的發(fā)展性。 XMPP中定義了三個(gè)角色,客戶(hù)端,服務(wù)器,網(wǎng)關(guān)。通信能夠在這三者的任意兩個(gè)之間雙向發(fā)生。服務(wù)器同時(shí)承擔(dān)了客戶(hù)端信息記錄,連接管理和信息的路由功能。網(wǎng)關(guān)承擔(dān)著與異構(gòu)即時(shí)通信系統(tǒng)的互聯(lián)互通,異構(gòu)系統(tǒng)可以包括SMS(短信),MSN,ICQ等。基本的網(wǎng)絡(luò)形式是單客戶(hù)端通過(guò)TCP/IP連接到單服務(wù)器,然后在之上傳輸XML。 優(yōu)點(diǎn) 1.超強(qiáng)的可擴(kuò)展性。經(jīng)過(guò)擴(kuò)展以后的XMPP可以通過(guò)發(fā)送擴(kuò)展的信息來(lái)處理用戶(hù)的需求。 2.易于解析和閱讀。方便了開(kāi)發(fā)和查錯(cuò)。 3.開(kāi)源。在客戶(hù)端、服務(wù)器、組件、源碼庫(kù)等方面,都已經(jīng)各自有多種實(shí)現(xiàn)。 缺點(diǎn) 1.數(shù)據(jù)負(fù)載太重。過(guò)多的冗余標(biāo)簽、低效的解析效率使得XMPP在移動(dòng)設(shè)備上表現(xiàn)不佳。
【更多音視頻學(xué)習(xí)資料,點(diǎn)擊下方鏈接免費(fèi)領(lǐng)取↓↓,先碼住不迷路~】
點(diǎn)擊領(lǐng)取→音視頻開(kāi)發(fā)基礎(chǔ)知識(shí)和資料包
應(yīng)用場(chǎng)景舉例:點(diǎn)對(duì)點(diǎn)單聊約球 我剛畢業(yè)時(shí)入職的公司曾接手開(kāi)發(fā)一個(gè)線上足球約戰(zhàn)的社交平臺(tái)App項(xiàng)目,當(dāng)時(shí)為了提高約球時(shí)的溝通效率,考慮為應(yīng)用引入聊天模塊,并優(yōu)先實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)單聊功能。那時(shí)市面上的即時(shí)通訊SDK方案還尚未成熟,綜合當(dāng)時(shí)團(tuán)隊(duì)成員的技術(shù)棧,決定采用XMPP+Openfire+Smack作為自研技術(shù)搭建聊天框架。 Openfire基于XMPP協(xié)議,采用Java開(kāi)發(fā),可用于構(gòu)建高效的即時(shí)通信服務(wù)器端,單臺(tái)服務(wù)器可支持上萬(wàn)并發(fā)用戶(hù)。Openfire安裝和使用都非常簡(jiǎn)單,并利用Web進(jìn)行管理。由于是采用開(kāi)放的XMPP協(xié)議,因此可以使用各種支持XMPP協(xié)議的IM客戶(hù)端軟件登錄服務(wù)。 Smack是一個(gè)開(kāi)源的、易于使用的XMPP客戶(hù)端Java類(lèi)庫(kù),提供了一套可擴(kuò)展的API。
- MQTT 全稱(chēng)(Message Queuing Telemetry Transport,消息隊(duì)列遙測(cè)傳輸協(xié)議),是一種基于發(fā)布/訂閱模式的“輕量級(jí)”通訊協(xié)議,其構(gòu)建于TCP/IP協(xié)議之上。MQTT最大優(yōu)點(diǎn)在于,可以以極少的代碼和有限的帶寬,為連接遠(yuǎn)程設(shè)備提供實(shí)時(shí)可靠的消息服務(wù)。作為一種低開(kāi)銷(xiāo)、低帶寬占用的即時(shí)通訊協(xié)議,使其在物聯(lián)網(wǎng)、小型設(shè)備、移動(dòng)應(yīng)用等方面有較廣泛的應(yīng)用。 特點(diǎn) 1.基于發(fā)布/訂閱模型。提供一對(duì)多的消息發(fā)布,解除應(yīng)用程序耦合。 2.低開(kāi)銷(xiāo)。MQTT客戶(hù)端很輕巧,只需要最少的資源,同時(shí)MQTT消息頭也很小,可以?xún)?yōu)化網(wǎng)絡(luò)帶寬。 3.可靠的消息傳遞。MQTT定義了3種消息發(fā)布服務(wù)質(zhì)量,以支持消息可靠性:至多一次,至少一次,只有一次。 4.對(duì)不可靠網(wǎng)絡(luò)的支持。專(zhuān)為受限設(shè)備和低帶寬、高延遲或不可靠的網(wǎng)絡(luò)而設(shè)計(jì)。
應(yīng)用場(chǎng)景舉例:賠率更新、賽事直播聊天室 我第二家入職的公司的主打產(chǎn)品是一款提供模擬競(jìng)猜、賽事直播的體育類(lèi)APP,其中核心的功能模塊就是提供各種賽事的最新比分賠率數(shù)據(jù),最初采用的即是上文所說(shuō)的低效的HTTP輪詢(xún)方案,效果可想而知。后面技術(shù)重構(gòu)后改用了MQTT,極大地減少了對(duì)網(wǎng)絡(luò)環(huán)境的依賴(lài),提高了數(shù)據(jù)的實(shí)時(shí)性和可靠性。再往后搭建直播模塊時(shí),考慮到聊天室這種一對(duì)多的消息發(fā)布場(chǎng)景同樣適合用MQTT解決,于是沿用了原先的技術(shù)方案擴(kuò)展了新的聊天室模塊。
- WebSocket 而相較之下,WebSocket的特點(diǎn)包括: 1.**較少的控制開(kāi)銷(xiāo)。**在連接創(chuàng)建后,服務(wù)器和客戶(hù)端之間交換數(shù)據(jù)時(shí),用于協(xié)議控制的數(shù)據(jù)包頭部相對(duì)較小。 2.**更好的二進(jìn)制支持。**Websocket定義了二進(jìn)制幀,相對(duì)HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。 3.**可以支持?jǐn)U展。**Websocket定義了擴(kuò)展,用戶(hù)可以擴(kuò)展協(xié)議、實(shí)現(xiàn)部分自定義的子協(xié)議,如以上所說(shuō)的XMPP協(xié)議、MQTT協(xié)議等。
實(shí)現(xiàn)WebSocket協(xié)議很簡(jiǎn)單,廣為Android開(kāi)發(fā)者使用的網(wǎng)絡(luò)請(qǐng)求框架——OkHttp對(duì)WebSocket通信流程進(jìn)行了封裝,提供了簡(jiǎn)明的接口用于WebSocket的連接建立、數(shù)據(jù)收發(fā)、連接保活、連接關(guān)閉等,使我們可以專(zhuān)注于業(yè)務(wù)實(shí)現(xiàn)而無(wú)須關(guān)注通信細(xì)節(jié),簡(jiǎn)單到我們只需要實(shí)現(xiàn)以下兩步:
- 創(chuàng)建WebSocket實(shí)例并提供一個(gè)URL以指定要連接的服務(wù)器地址
- 提供一個(gè)WebSocket連接事件監(jiān)聽(tīng)器,用于監(jiān)聽(tīng)事件回調(diào)以處理連接生命周期的每個(gè)階段
WebSocket URL的構(gòu)成與Http URL很相似,都是由協(xié)議、主機(jī)、端口、路徑等構(gòu)成,區(qū)別就是WebSocket URL的協(xié)議名采用的是ws://和wss://,wss://表明是安全的WebSocket連接。
首先我們?cè)陧?xiàng)目中引入OkHttp庫(kù)的依賴(lài):
implementation("com.squareup.okhttp3:okhttp:4.9.0")
其次,我們須指定要連接的服務(wù)器地址,此處可以使用WebSocket的官方服務(wù)器地址:
/** WebSocket服務(wù)器地址 */ 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() ... }
接著,我們調(diào)用OkHttpClient實(shí)例的newWebSocket(request: Request, listener: WebSocketListener)方法,該方法需傳入兩個(gè)參數(shù),第一個(gè)是上文構(gòu)建的Request對(duì)象,第二個(gè)是WebSocket連接事件的監(jiān)聽(tīng)器,WebSocket協(xié)議包含四個(gè)主要的事件:
- Open:客戶(hù)端和服務(wù)器之間建立了連接后觸發(fā)
- Message:服務(wù)端向客戶(hù)端發(fā)送數(shù)據(jù)時(shí)觸發(fā)。發(fā)送的數(shù)據(jù)可以是純文本或二進(jìn)制數(shù)據(jù)
- Close:服務(wù)端與客戶(hù)端之間的通信結(jié)束時(shí)觸發(fā)。
- Error:通信過(guò)程中發(fā)生錯(cuò)誤時(shí)觸發(fā)。
每個(gè)事件都通過(guò)分別實(shí)現(xiàn)對(duì)應(yīng)的回調(diào)來(lái)進(jìn)行處理。OkHttp提供的監(jiān)聽(tīng)器包含以下回調(diào):
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) } ... }
以上的事件通常在連接狀態(tài)發(fā)生變化時(shí)被動(dòng)觸發(fā),另一方面,如果用戶(hù)想主動(dòng)執(zhí)行某些操作,WebSocket也提供了相應(yīng)的接口以給用戶(hù)顯式調(diào)用。WebSocket協(xié)議包含兩個(gè)主要的操作:
- send( ) :向服務(wù)端發(fā)送消息,包括文本或二進(jìn)制數(shù)據(jù)
- close( ):主動(dòng)請(qǐng)求關(guān)閉連接。
可以看到,OkHttp提供的WebSocket接口也提供了這兩個(gè)方法:
interface WebSocket { ... fun send(text: String): Boolean fun send(bytes: ByteString): Boolean fun close(code: Int, reason: String?): Boolean ... }
當(dāng)onOpen方法回調(diào)時(shí),即是連接建立成功,可以傳輸數(shù)據(jù)了。此時(shí)我們便可以調(diào)用WebSocket實(shí)例的send()方法發(fā)送文本消息或二進(jìn)制消息,WebSocket官方服務(wù)器會(huì)將數(shù)據(jù)通過(guò)onMessage(webSocket: WebSocket, bytes: ByteString)或onMessage(webSocket: WebSocket, text: String)回調(diào)原樣返回給我們。
【更多音視頻學(xué)習(xí)資料,點(diǎn)擊下方鏈接免費(fèi)領(lǐng)取↓↓,先碼住不迷路~】
點(diǎn)擊領(lǐng)取→音視頻開(kāi)發(fā)基礎(chǔ)知識(shí)和資料包
WebSocket是如何建立連接的?
我們可以通過(guò)閱讀OkHttp源碼獲知,newWebSocket(request: Request, listener: WebSocketListener)方法內(nèi)部是創(chuàng)建了一個(gè)RealWebSocket實(shí)例,該類(lèi)是WebSocket接口的實(shí)現(xiàn)類(lèi),創(chuàng)建實(shí)例成功后便調(diào)用connect(client: OkHttpClient)方法開(kāi)始異步建立連接。
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 }
連接建立的過(guò)程主要是向服務(wù)器發(fā)送了一個(gè)HTTP請(qǐng)求,該請(qǐng)求包含了額外的一些請(qǐng)求頭信息:
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()
這些請(qǐng)求頭的意義如下:
Connection: Upgrade:表示要升級(jí)協(xié)議
Upgrade: websocket:表示要升級(jí)到websocket協(xié)議。
Sec-WebSocket-Version:13:表示websocket的版本。如果服務(wù)端不支持該版本,需要返回一個(gè)Sec-WebSocket-Versionheader,里面包含服務(wù)端支持的版本號(hào)。
Sec-WebSocket-Key:與后面服務(wù)端響應(yīng)首部的Sec-WebSocket-Accept是配套的,提供基本的防護(hù),比如惡意的連接,或者無(wú)意的連接。
當(dāng)返回的狀態(tài)碼為101時(shí),表示服務(wù)端同意客戶(hù)端協(xié)議轉(zhuǎn)換請(qǐng)求,并將其轉(zhuǎn)換為Websocket協(xié)議,該過(guò)程稱(chēng)之為Websocket協(xié)議握手(websocket Protocol handshake),協(xié)議升級(jí)完成后,后續(xù)的數(shù)據(jù)交換則遵照WebSocket的協(xié)議。
前面我們一直說(shuō)「握手」,握手究竟指的是什么呢?在計(jì)算機(jī)領(lǐng)域的語(yǔ)境中,握手通常是指確保服務(wù)器與其客戶(hù)端同步的過(guò)程。握手是WebSocket協(xié)議的基本概念。
為了直觀展示,以上實(shí)例中傳輸?shù)南⒕晕谋緸槔琖ebSocket還支持二進(jìn)制數(shù)據(jù)的傳輸,而這就要依靠「數(shù)據(jù)傳輸協(xié)議」來(lái)完成了,這是下一篇文章的內(nèi)容,敬請(qǐng)期待。
總結(jié)
為了完成與服務(wù)端的雙向通信,我們選取了WebSocket協(xié)議作為網(wǎng)絡(luò)通信協(xié)議,并通過(guò)對(duì)比傳統(tǒng)HTTP協(xié)議和其他相關(guān)的即時(shí)通訊技術(shù),總結(jié)出,在為移動(dòng)設(shè)備下應(yīng)用選擇的合適的網(wǎng)絡(luò)通信協(xié)議時(shí),可以有以下的參考標(biāo)準(zhǔn):
- 支持全雙工通信
- 支持二進(jìn)制數(shù)據(jù)傳輸
- 支持?jǐn)U展
- 跨語(yǔ)言、跨平臺(tái)實(shí)現(xiàn)
同時(shí),也對(duì)WebSocket協(xié)議在Android端的實(shí)現(xiàn)提供了示例,并對(duì)WebSocket協(xié)議握手流程進(jìn)行了初步窺探,當(dāng)然,這只是第一步,往后的心跳保活、斷線重連、消息隊(duì)列等每一個(gè)都可以單獨(dú)作為一個(gè)課題,會(huì)在后面陸續(xù)推出的。