JS實時通訊三把斧系列之二: socket.io
介紹完上一篇文章websocket
,我們把視線轉移到第二個RTC利器:socket.io
。估計有童鞋就會問,websocket
和socket.io
有啥區別啊?
在瞭解socket.io
之前,我們先聊聊websocket
(長連線)的實現背景。
1、長連線的實現背景
在現實產品中,並不是所有的客戶端都支援長連線的,或者換句話說,在websocket
協議出來之前,是有兩種方式去實現websocket
類似的功能的。
Flash
: 使用Flash是一種簡單的方法。不過很明顯的缺點就是Flash並不會安裝在所有客戶端上,比如iPhone/iPad。AJAX Long-Polling
:AJAX長輪詢已經被用來模擬websocket
有一段時間了。這是一種有效的技術,但並沒有對訊息傳送進行優化。雖然我不會把AJAX
長輪詢當做一種hack
技術,但它確實不是一個最優方法
那麼如果單純地使用websocket
的話,那些不支援的客戶端怎麼辦呢?難道直接放棄掉?當然不是。Guillermo Rauch
大神寫了socket.io
這個庫,對websocket
進行封裝,從而讓長連線滿足所有的場景,不過當然得配合使用對應的客戶端程式碼。
socket.io
將會使用特性檢測的方式來決定以websocket/ajax長輪詢/flash
等方式建立連線,那麼socket.io
是如何做到這些的呢?我們帶著以下幾個問題去學習:
- socket.io到底有什麼新特性?
- socket.io是怎麼實現特性檢測的?
- socket.io有哪些坑呢?
- socket.io的實際應用是怎樣的,需要注意些什麼?
如果有童鞋對上述問題已經清楚,想必就沒有往下讀的必要了。
2、socket.io
的介紹
讀過第一篇文章的童鞋都知道了websocket
的功能,那麼socket.io
相對於websocket
,在此基礎上封裝了一些什麼新東西呢?
socket.io
其實是有一套封裝了websocket
的協議,叫做engine.io
協議,在此協議上實現了一套底層雙向通訊的引擎Engine.io
。
而socket.io
則是建立在engine.io
上的一個應用層框架而已。所以我們研究的重點便是engine.io
協議。
在socket.io的README中提到了其實現的一些新特性(問題一):
- 可靠性 連線依然可以建立即使應用環境存在: 代理或者負載均衡器 個人防火牆或者反病毒軟體
- 支援自動連線: 除非特別指定,否則一個斷開的客戶端會一直重連伺服器直到伺服器恢復可用狀態
- 斷開連線檢測:在Engine.io層實現了一個心跳機制,這樣允許客戶端和伺服器知道什麼時候其中的一方不能響應。該功能是通過設定在服務端和客戶端的定時器實現的,在連線握手的時候,伺服器會主動告知客戶端心跳的間隔時間以及超時時間
- 二進位制的支援:任何序列化的資料結構都可以用來傳送
- 跨瀏覽器的支援:該庫甚至支援到IE8
- 支援複用:為了在應用程式中將建立的關注點隔離開來,Socket.io允許你建立多個namespace,這些namespace擁有單獨的通訊通道,但將共享相同的底層連線
- 支援Room:在每一個namespace下,你可以定義任意數量的通道,我們稱之為"房間",你可以加入或者離開房間,甚至廣播訊息到指定的房間。
Note
Socket.IO
不是websocket
的實現,雖然Socket.IO
確實在可能的情況下會去使用Websocket
作為一個transport
,但是它新增了很多後設資料到每一個報文中:報文的型別以及namespace和ack Id。這也是為什麼websocket
客戶端不能夠成功連線上 Socket.IO 伺服器,同樣一個 Socket.IO 客戶端也連線不上Websocket伺服器的原因。
3、engine.io
協議的介紹
完整的engine.io
協議的握手過程如下圖:
當前engine.io
協議的版本是3,我們根據上圖來大致介紹engine.io
協議
3.1、協議請求欄位
我們看到的是請求的url
和websocket
不大一樣,解釋一下:
EIO=3
: 表示的是使用的是Engine.io協議版本3transport=polling/websocket
: 表示使用的長連線方式是輪詢還是websockett=xxxxx
: 程式碼中使用yeast根據時間戳生成一個唯一的字串sid=xxxx
: 客戶端和伺服器建立連線之後獲取到的session id,客戶端拿到之後必須在每次請求中追加這個欄位
除了上述的3個欄位,協議還描述了下面幾個欄位:
- j: 如果
transport
是polling
,但是要求有一個JSONP
的響應,那麼j就應該設定為JSONP
響應的索引值 - b64: 如果客戶端不支援
XHR
,那麼客戶端應該設定b64=1
傳給伺服器,告知伺服器所有的二進位制資料應該以base64編碼後再傳送。
另外engine.io
預設的path
是/engine.io
,socket.io
在初始化的時候設定為了/socket.io
,所以大家看到的path就都是/socket.io
了
function Server(srv, opts){
if (!(this instanceof Server)) return new Server(srv, opts);
if ('object' == typeof srv && srv instanceof Object && !srv.listen) {
opts = srv;
srv = null;
}
opts = opts || {};
this.nsps = {};
this.parentNsps = new Map();
this.path(opts.path || '/socket.io');
3.2、資料包編碼要求
engine.io
協議的資料包編碼有自己的一套格式,在協議介紹上engine.io-protocol
,定義了兩種編碼型別:
- packet
- payload
3.2.1、packet
一個編碼過的packet
是下面這種格式:
<packet type id>[<data>]
然後協議定義了下面幾種packet type
(採用數字進行標識):
- 0(open): 當開始一個新的transport的時候,服務端會傳送該型別的
packet
- 1(close): 請求關閉這個transport但是不要自己關閉關閉連線
- 2(ping): 由客戶端傳送的ping包,服務端必須回應一個包含相同資料的
pong
包 - 3(pong): 響應ping包,服務端傳送
- 4(message): 實際訊息,在客戶端和服務端都可以監聽message事件獲取訊息內容
- 5(upgrade):在engine.io切換transport之前,它會用來測試服務端和客戶端是否在該transport上通訊。如果測試成功,客戶端會傳送一個upgrade包去讓伺服器重新整理它的快取並切換到新的transport
- 6(noop): 主要用來強制一個輪詢迴圈當收到一個websocket連線的時候
3.2.2、payload
那payload
也有對應的格式要求:
- 如果當只有傳送
string
並且不支援XHR
的時候,其編碼格式是:<length1>:<packet1>[<length2>:<packet2>[...]]
- 當不支援
XHR2
並且傳送二進位制資料,但是使用base64
編碼字串的時候,其編碼格式是:<length of base64 representation of the data + 1 (for packet type)>:b<packet1 type><packet1 data in b64>[...]
- 當支援XHR2的時候,所有的資料都被編碼成二進位制,格式是:
<0 for string data, 1 for binary data><Any number of numbers between 0 and 9><The number 255><packet1 (first type, then data)>[...]
- 如果傳送的內容混雜著UTF-8的字元和二進位制資料,字串的每個字元被寫成一個字元編碼,用1個位元組表示。
TIPS: payload
的編碼要求不適用於websocket
的通訊
針對上面的編碼要求,我們隨便舉個例子,之前在第一條polling請求的時候,服務端編碼傳送了這個資料:
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}2:40
根據上面的知識,我們知道第一次服務端會傳送一個open
的資料包,所以組裝出來的packet
是:
0
然後服務端會告知客戶端去嘗試升級到websocket
,並且告知對應的sid,於是整合後便是:
0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}
接著根據payload的編碼格式,因為是string,且長度是97個位元組,所以是:
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}
接著第二部分資料是message
包型別,並且資料是0,所以是40,長度為2位元組,所以是2:40,最後就拼成剛才大家看到的結果。
Tips
ping/pong
的間隔時間是服務端告知客戶的:"pingInterval":25000,"pingTimeout":60000
,也就是說心跳時間預設是25
秒,並且等待pong
響應的時間預設是60s
。
3.3、升級協議的必備過程
協議定義了transport
升級到websocket
需要經歷一個必須的過程,如下圖:
websocket
的測試開始於傳送probe
,如果伺服器也響應probe
的話,客戶端就必須傳送一個upgrade
包。
為了確保不會丟包,只有在當前transport
的所有buffer
被重新整理並且transport
被認為paused
的時候才可以傳送upgrade
包。服務端收到upgrade
包的時候,服務端必須假設這是一個新的通道併傳送所有已存的快取到這個通道上
在Chrome
上的效果如下:
4、engine.io
的程式碼實現
熟悉了engine.io
協議之後,我們看看程式碼是怎麼實現主流程的。
客戶端的engine.io
的主要實現流程我們在上面文字介紹了,結合程式碼engine.io
,畫了這麼一個客戶端流程圖:
服務端的程式碼和客戶端非常相似,其實現流程圖如下:
5、socket.io
的應用以及坑
5.1、搭配nginx
使用
在實際應用中,socket.io
伺服器都會部署在nginx
後面,所以我們需要配置nginx
幾個配置:
- 需要新增下面兩行配置:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade";
如果有多個例項啟動的話,需要保證某個ip連線到某個例項之後,一直保持和該例項的連線,而不是被負載均衡隨機分配例項,還需要配置下面一行:
upstream {
ip_hash; // 主要這行,該行還必須在ip:port之前,否則會有警告出現
ip:port;
ip:port;
....
}
二者有任何一個沒有配置,客戶端都會出現下圖中類似的錯誤:
5.2、io.use
中介軟體的詭異行為
在實際應用中發現如下注釋的行為,請參考demo程式碼io.js
// socket.io這邊有個很奇怪的,如果我使用io.use的話,那麼只有連線根ns的客戶端才會收到這個錯誤的packet
// 而不會讓某個ns的客戶端收到,但是這個中介軟體的函式卻是監聽所有的socket的。
io.use((socket, next) => {
console.log('middleware has triggered.......')
// if (socket.request.headers.cookie) return next();
next(new Error('Authentication error'));
})
現象: 在頁面中點選連線到根ns,測試服務端的根中介軟體的問題按鈕是會收到錯誤訊息,但是點選其他ns卻不會收到錯誤訊息。
雖然每個ns下是有提供了ns的中介軟體,但是我們更希望有一個普遍的中介軟體去使用,但是很明顯socket.io目前是沒有這樣實現的。關於這個問題如果也影響了大家的實現的話,這個只能改動原始碼,具體改哪裡,童鞋們自行去思考吧。
5.3、根ns
另外需要注意的是,使用socket.io
的話,是有預設的namespace(/)
,所以無論客戶端連線的ns
是哪一個,都會先進入根ns
的,並且會記錄下這個客戶端的。換句話說,如果你有2個客戶端連線ns1
,3個客戶端連線ns2
的話,那麼在/下就會有5個客戶端,並且在根ns
下監聽connection
事件也是會進入的。
相關文章
- socket.IO通訊
- 基於 socket.io 快速實現一個實時通訊應用
- socket.io通訊原理
- Socket.IO IM通訊元件元件
- 【Spring Boot】整合Netty Socket.IO通訊框架Spring BootNetty框架
- 實時通訊系列目錄篇之SignalR詳解SignalR
- 【SignalR全套系列】之在.Net Core 中實現SignalR實時通訊SignalR
- 訊息的即時推送——net實現、websocket實現以及socket.io實現Web
- dart系列之:實時通訊,在瀏覽器中使用WebSocketsDart瀏覽器Web
- socket.io讓每個人都可以開發屬於自己的即時通訊
- 魔方實時通訊im元件元件
- Oracle實時程式通訊(轉)Oracle
- WebRTC---網路實時通訊Web
- Uniapp 使用 GoEasy 實現 websocket 實時通訊APPGoWeb
- 深入學習作用域和閉包—全面(JS系列之二)JS
- 前端音視訊WebRTC實時通訊的核心前端Web
- 實時通訊技術大亂鬥
- 在Spring Boot中實現WebSocket實時通訊Spring BootWeb
- 魔方實時通訊一對一音視訊元件元件
- 即時通訊
- Web實時通訊,SignalR真香,不用愁了WebSignalR
- 影片通訊近實時生成字幕專案實踐
- js訊息訂閱和釋出實現元件之間通訊JS元件
- 騰訊互動白板+即時通訊+實時音視訊,Android學生端接入Android
- Binder面試系列之二面試
- 【workerman】uniapp+thinkPHP5使用GatewayWorker實現實時通訊APPPHPGateway
- docker系列(五):網路通訊Docker
- 多程式通訊系列問題
- iOS動畫系列之二:帶時分秒指標的時鐘動畫(下)iOS動畫指標
- laravel整合workerman實現websocket多端及時通訊LaravelWeb
- flutter 呼叫環信sdk 實現即時通訊Flutter
- 微信小程式+mqtt.js實現實時接收訊息微信小程式MQQTJS
- Nodejs教程27:Node.js專案之二:實現路由NodeJSNode.js路由
- 說說在 Vue.js 中如何實現元件間通訊Vue.js元件
- 使用socket.io和node.js搭建websocket應用Node.jsWeb
- IO通讀JS高程系列(2)--基本概念JS
- IO通讀JS高程系列(1)--基本概念JS
- 全民直播時代——基於WebRTC開發實時通訊服務Web