最近,業務中有使用到
socket.io
,進行客戶端與服務端的實時通訊。socket.io
提供的API
易上手,對新手友好,這就極大提高了開發者的效率。不過,期間也有遇到很多socket.io
中的坑,例如,中文亂碼問題,服務端NPE問題 等。有些涉及到底層的問題,就勢必要理解socket.io
設計原理,進行排查。所以,總結了一下相關的概念,方便今後更快定位問題。
1.概述
socket.io
是基於 Websocket 的Client-Server 實時通訊庫。
socket.io
底層是基於engine.io這個庫。
因此,在介紹socket.io
之前,先簡單的介紹一些關於engine.io
相關的知識,方便深入理解。
依賴關係詳見:
2. Engine.io 基礎
engine.io
為 socket.io
提供跨瀏覽器/跨裝置的雙向通訊的底層庫。engine.io
使用了 Websocket
和 XHR
方式封裝了一套 socket
協議。 在低版本的瀏覽器中,不支援Websocket
,為了相容使用長輪詢(polling)替代。
相關原始碼如下:
3. Engine.io 工作流程
Client
import eio from './engine.io-client'
// 建立一個socket長連線
let socket = new eio.Socket('ws://localhost');
複製程式碼
根據流程圖,可以看出:
- 建立長連線的方式有三種:
websocket
、xhr
、jsonp
。其中,後兩種使用長輪詢的方式進行模擬。 - 所謂的長輪詢是指,客戶端傳送一次
request
,當服務端有訊息推送時會push一條response
給客戶端。客戶端收到response
後,會再次傳送request
,重複上述過程,直到其中一端主動斷開連線為止。
4. webSocket 請求頭資訊
下圖是建立成功的socket長連線:
引數說明
Request URL
請求服務端地址Request Method
請求方式 (支援get/post/option)Status Code
101 Switching Protocols
規範解釋: 當收到101請求狀態碼時,表明服務端理解並同意客戶端請求,更改
Upgrade
header欄位。服務端也必須在response
中,生成對應的Upgrade
值。
Connection
設定upgrade
header,通知服務端,該request
型別需要進行升級為websocket
。 upgrade_mechanism 規範Host
服務端 hostnameOrigin
客戶端 hostname:portSec-WebSocket-Extensions
客戶端向服務端發起請求擴充套件列表(list),供服務端選擇並在響應中返回Sec-WebSocket-Key
祕鑰的值是通過規範中定義的演算法進行計算得出,因此是不安全的,但是可以阻止一些誤操作的websocket請求。Sec-WebSocket-Protocol
指定有限使用的Websocket協議,可以是一個協議列表(list)。服務端在response
中返回列表中支援的第一個值。Sec-WebSocket-Version
指定通訊時使用的Websocket協議版本。最新版本:13,歷史版本Upgrade
通知服務端,指定升級協議型別為websocket
5. engine.io 協議解析
- 客戶端通過
engine.io
的url建立通訊連線 - 服務端在
response
中返回一個open
的packet,JSON編碼資料格式如下: 1. sid: session id (String
) 2. upgrades: 傳輸型別(Array
) 3. pingTimeout: 服務端通訊超時配置,客戶端用於超時檢測(Number
) 4. pingInterval: 服務端通訊定時器配置,客戶端用於超時檢測(Number
) - 收到客戶端傳送的
ping
packets時,服務端必須定時傳送pong
packets - 客戶端與服務端可以隨意交換
message
pakcets Polling
傳輸可以傳送一個close
pakcet來關閉socket
,因為他們可能會一直opening
或closing
6. URLs
engine.io
URL的組成如下:
/engine.io/[?\<query string>]
- engine.io: 只允許由庫自身進行修改
- query string: 可選欄位,並提供了四個保留欄位:
1. transport: 宣告傳輸方式,可選值:<
polling
,websocket
> 2. j: 如果傳輸方式為polling
,但是需要JSONP
的響應,則j
必須設定為JSONP
響應的index
3. sid: 如果客戶端已經分配了一個session id
,則sid
必須包含在query string中 4. b64: 如果客戶端不支援XHR2
,則必須在query string中加上b64=1
標識,以通知服務端所有二進位制資料應該進行base64編碼
7. 編碼方式
engine.io
有兩種編碼方式:
- packet
- payload
Packet
編碼包可以是UTF8或二進位制資料,編碼格式如下:
<包型別id>[<data>]
例如:
2probe
包型別id(packet type id)是一個整型,具體含義如下:
- 0 open 當開啟一個新傳輸時,服務端檢測併傳送
- 1 close 請求關閉傳輸,但不是主動斷開連線
- 2 ping
客戶端發出,服務端應該返回包含相同資料的
pong
packet進行應答 - 3 pong
服務端發出,用以響應客戶端的
ping
packet - 4 message
真實資料,客戶端和服務端應該呼叫回撥中的
data
// 服務端傳送
send('4HelloWorld')
// 客戶端接收資料並呼叫回撥
socket.on('message', function (data) { console.log(data); });
// 客戶端傳送
send('4HelloWorld')
// 服務端接收資料並呼叫回撥
socket.on('message', function (data) { console.log(data); })
複製程式碼
- 5 upgrade
在
engine.io
切換傳輸之前,它會測試伺服器和客戶端是否可以通過此傳輸進行通訊。如果此測試成功,客戶端將傳送升級資料包,請求伺服器重新整理舊傳輸上的快取並切換到新傳輸。 - 6 noop
noop
packet。主要用於在收到傳入的websocket
連線時強制輪詢週期。- 客戶端通過新的傳輸連線
- 客戶端傳送
2send
- 服務端接收併傳送
3probe
- 客戶端結束併傳送
5
- 服務端重新整理並關閉舊的傳輸連線並切換到新傳輸連線
Payload
Payload
是繫結在一起的一系列編碼分組。格式如下:
<length1>:<packet1>[<length2>:<packet2>[...]]
- length: 表示
packet
的字元長度 - packet: 真實資料包
Transports
engine.io
支援三種傳輸方式:
websocket
polling
jsonp
- xhr
Polling
長輪詢傳輸包括客戶端向伺服器重複發出GET請求以獲取資料,以及具有從客戶端到伺服器的有效負載的POST請求以傳送資料。
XHR
服務端必須支援CORS
JSONP
伺服器實現必須使用有效的JavaScript進行響應。 URL包含必須在響應中使用的query string引數j
。 j
是整數。
JSONP包的格式:
___eio[<
j
> ](" <encoded payload> ");
例如:
___eio[4]("packet data");