搞懂現(xiàn)代Web端即時(shí)通訊技術(shù)一文就夠:WebSocket、socket.io、SSE

本文引用自“ 豆米博客”的《JS實(shí)時(shí)通信三把斧》系列文章,有優(yōu)化和改動(dòng)。
1、引言
有關(guān)Web端即時(shí)通訊技術(shù)的文章我已整理過很多篇,閱讀過的讀者可能都很熟悉,早期的Web端即時(shí)通訊方案,受限于Web客戶端的技術(shù)限制,想實(shí)現(xiàn)真正的“即時(shí)”通信,難度相當(dāng)大。
傳統(tǒng)的Web端即時(shí)通訊技術(shù)從短輪詢到長(zhǎng)連詢,再到Comet技術(shù),在如此原始的HTML標(biāo)準(zhǔn)之下,為了實(shí)現(xiàn)所謂的“即時(shí)”通信,技術(shù)上可謂絞盡腦汁,極盡所能。
自從HTML5標(biāo)準(zhǔn)發(fā)布之后,WebSocket這類技術(shù)橫空出世,實(shí)現(xiàn)Web端即時(shí)通訊技術(shù)的便利性大大提前,以往想都不敢想的真正全雙工實(shí)時(shí)通信,如此早已成為可能。
本文將專門介紹WebSocket、socket.io、SSE這幾種現(xiàn)代的Web端即時(shí)通訊技術(shù),從適用場(chǎng)景到技術(shù)原理,通俗又不失深度的文字,特別適合對(duì)Web端即時(shí)通訊技術(shù)有一定了解,且想深入學(xué)習(xí)WebSocket等現(xiàn)代Web端“實(shí)時(shí)”通信技術(shù),卻又不想花時(shí)間去深讀枯燥的IETF技術(shù)手冊(cè)的讀者。
?

學(xué)習(xí)交流:
- 即時(shí)通訊/推送技術(shù)開發(fā)交流5群:215477170?[推薦]
- 移動(dòng)端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文同步發(fā)布于:http://www.52im.net/thread-3695-1-1.html)
2、本文作者
“豆米”:現(xiàn)居杭州,熱愛前端,熱愛互聯(lián)網(wǎng),豆米是“洋芋(土豆-豆)”和“米喳(米)”的簡(jiǎn)稱。
作者博客:https://blog.5udou.cn/
作者Github:https://github.com/linxiaowu66/
3、知識(shí)預(yù)備
如果你對(duì)Web端即時(shí)通訊技術(shù)的前世今生不曾了解,建議先讀以下文章:
《新手入門貼:史上最全Web端即時(shí)通訊技術(shù)原理詳解》
《Web端即時(shí)通訊技術(shù)盤點(diǎn):短輪詢、Comet、Websocket、SSE》
《詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE、Websocket》
《網(wǎng)頁(yè)端IM通信技術(shù)快速入門:短輪詢、長(zhǎng)輪詢、SSE、WebSocket》
如果你對(duì)本文將要介紹的技術(shù)已有了解,建議進(jìn)行專項(xiàng)學(xué)習(xí),以便深入掌握:
《Comet技術(shù)詳解:基于HTTP長(zhǎng)連接的Web端實(shí)時(shí)通信技術(shù)》
《SSE技術(shù)詳解:一種全新的HTML5服務(wù)器推送事件技術(shù)》
《WebSocket詳解(三):深入WebSocket通信協(xié)議細(xì)節(jié)》
《理論聯(lián)系實(shí)際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》
《WebSocket從入門到精通,半小時(shí)就夠!》
4、WebSocket

在這里不打算詳細(xì)介紹整個(gè)WebSocket協(xié)議的內(nèi)容,根據(jù)我本人以前協(xié)議的學(xué)習(xí)思路,我挑重點(diǎn)使用問答方式來介紹該協(xié)議,這樣讀起來就不那么枯燥。
4.1 基本情況
協(xié)議運(yùn)行在OSI的哪層?
應(yīng)用層,WebSocket協(xié)議是一個(gè)獨(dú)立的基于TCP的協(xié)議。 它與HTTP唯一的關(guān)系是它的握手是由HTTP服務(wù)器解釋為一個(gè)Upgrade請(qǐng)求。
協(xié)議運(yùn)行的標(biāo)準(zhǔn)端口號(hào)是多少?
默認(rèn)情況下,WebSocket協(xié)議使用端口80用于常規(guī)的WebSocket連接、端口443用于WebSocket連接的在傳輸層安全(TLS)RFC2818之上的隧道化口。
4.2 協(xié)議是如何工作的?
協(xié)議的工作流程可以參考下圖:

其中幀的一些重要字段需要解釋一下:
1)Upgrade:`upgrade`是HTTP1.1中用于定義轉(zhuǎn)換協(xié)議的`header`域。它表示,如果服務(wù)器支持的話,客戶端希望使用現(xiàn)有的「網(wǎng)絡(luò)層」已經(jīng)建立好的這個(gè)「連接(此處是 TCP 連接)」,切換到另外一個(gè)「應(yīng)用層」(此處是 WebSocket)協(xié)議;
2)Connection:`Upgrade`固定字段。Connection還有其他字段,可以自己給自己科普一下;
3)Sec-WebSocket-Key:用來發(fā)送給服務(wù)器使用(服務(wù)器會(huì)使用此字段組裝成另一個(gè)key值放在握手返回信息里發(fā)送客戶端);
4)Sec-WebSocket-Protocol:標(biāo)識(shí)了客戶端支持的子協(xié)議的列表;
5)Sec-WebSocket-Version:標(biāo)識(shí)了客戶端支持的WS協(xié)議的版本列表,如果服務(wù)器不支持這個(gè)版本,必須回應(yīng)自己支持的版本;
6)Origin:作安全使用,防止跨站攻擊,瀏覽器一般會(huì)使用這個(gè)來標(biāo)識(shí)原始域;
7)Sec-WebSocket-Accept:服務(wù)器響應(yīng),包含Sec-WebSocket-Key 的簽名值,證明它支持請(qǐng)求的協(xié)議版本。
關(guān)于Sec-WebSocket-Key和Sec-WebSocket-Accept的計(jì)算是這樣的:
所有兼容RFC 6455 的WebSocket 服務(wù)器都使用相同的算法計(jì)算客戶端挑戰(zhàn)的答案:將Sec-WebSocket-Key 的內(nèi)容與標(biāo)準(zhǔn)定義的唯一GUID字符(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)串拼接起來,計(jì)算出SHA1散列值,結(jié)果是一個(gè)base-64編碼的字符串,把這個(gè)字符串發(fā)給客戶端即可。
用代碼就是實(shí)現(xiàn)如下:
const key = crypto.createHash('sha1')
??????.update(req.headers['sec-websocket-key'] + constants.GUID, 'binary')
??????.digest('base64')
至于為什么需要這么一個(gè)步驟,可以參考《理論聯(lián)系實(shí)際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》一文。
引用如下:
Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基礎(chǔ)的防護(hù),減少惡意連接、意外連接。
作用大致歸納如下:
1)避免服務(wù)端收到非法的websocket連接(比如http客戶端不小心請(qǐng)求連接websocket服務(wù),此時(shí)服務(wù)端可以直接拒絕連接);
2)確保服務(wù)端理解websocket連接。因?yàn)閣s握手階段采用的是http協(xié)議,因此可能ws連接是被一個(gè)http服務(wù)器處理并返回的,此時(shí)客戶端可以通過Sec-WebSocket-Key來確保服務(wù)端認(rèn)識(shí)ws協(xié)議。(并非百分百保險(xiǎn),比如總是存在那么些無聊的http服務(wù)器,光處理Sec-WebSocket-Key,但并沒有實(shí)現(xiàn)ws協(xié)議。。。);
3)用瀏覽器里發(fā)起ajax請(qǐng)求,設(shè)置header時(shí),Sec-WebSocket-Key以及其他相關(guān)的header是被禁止的。這樣可以避免客戶端發(fā)送ajax請(qǐng)求時(shí),意外請(qǐng)求協(xié)議升級(jí)(websocket upgrade);
4)可以防止反向代理(不理解ws協(xié)議)返回錯(cuò)誤的數(shù)據(jù)。比如反向代理前后收到兩次ws連接的升級(jí)請(qǐng)求,反向代理把第一次請(qǐng)求的返回給cache住,然后第二次請(qǐng)求到來時(shí)直接把cache住的請(qǐng)求給返回(無意義的返回);
5)Sec-WebSocket-Key主要目的并不是確保數(shù)據(jù)的安全性,因?yàn)镾ec-WebSocket-Key、Sec-WebSocket-Accept的轉(zhuǎn)換計(jì)算公式是公開的,而且非常簡(jiǎn)單,最主要的作用是預(yù)防一些常見的意外情況(非故意的)。
強(qiáng)調(diào):Sec-WebSocket-Key/Sec-WebSocket-Accept?的換算,只能帶來基本的保障,但連接是否安全、數(shù)據(jù)是否安全、客戶端/服務(wù)端是否合法的 ws客戶端、ws服務(wù)端,其實(shí)并沒有實(shí)際性的保證。
4.3 協(xié)議傳輸?shù)膸袷绞鞘裁?
幀格式定義的格式如下:

各個(gè)字段的解釋如下:
1)FIN: 1bit,用來表明這是一個(gè)消息的最后的消息片斷,當(dāng)然第一個(gè)消息片斷也可能是最后的一個(gè)消息片斷;
2)RSV1,RSV2,RSV3: 分別都是1位,如果雙方之間沒有約定自定義協(xié)議,那么這幾位的值都必須為0,否則必須斷掉WebSocket連接。在ws中就用到了RSV1來表示是否消息壓縮了的;
3)opcode:4 bit,表示被傳輸幀的類型:
- %x0 表示連續(xù)消息片斷;
-??%x1 表示文本消息片斷;
-??%x2 表未二進(jìn)制消息片斷;
-??%x3-7 為將來的非控制消息片斷保留的操作碼;
-??%x8 表示連接關(guān)閉;
-??%x9 表示心跳檢查的ping;
-??%xA 表示心跳檢查的pong;
-??%xB-F 為將來的控制消息片斷的保留操作碼。
4)Mask: 1 bit。定義傳輸?shù)臄?shù)據(jù)是否有加掩碼,如果設(shè)置為1,掩碼鍵必須放在masking-key區(qū)域,客戶端發(fā)送給服務(wù)端的所有消息,此位都是1;
5)Payload length:傳輸數(shù)據(jù)的長(zhǎng)度,以字節(jié)的形式表示:7位、7+16位、或者7+64位。如果這個(gè)值以字節(jié)表示是0-125這個(gè)范圍,那這個(gè)值就表示傳輸數(shù)據(jù)的長(zhǎng)度;如果這個(gè)值是126,則隨后的兩個(gè)字節(jié)表示的是一個(gè)16進(jìn)制無符號(hào)數(shù),用來表示傳輸數(shù)據(jù)的長(zhǎng)度;如果這個(gè)值是127,則隨后的是8個(gè)字節(jié)表示的一個(gè)64位無符合數(shù),這個(gè)數(shù)用來表示傳輸數(shù)據(jù)的長(zhǎng)度。多字節(jié)長(zhǎng)度的數(shù)量是以網(wǎng)絡(luò)字節(jié)的順序表示。負(fù)載數(shù)據(jù)的長(zhǎng)度為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)之和,擴(kuò)展數(shù)據(jù)的長(zhǎng)度可能為0,因而此時(shí)負(fù)載數(shù)據(jù)的長(zhǎng)度就為應(yīng)用數(shù)據(jù)的長(zhǎng)度;
6)Masking-key:0或4個(gè)字節(jié),客戶端發(fā)送給服務(wù)端的數(shù)據(jù),都是通過內(nèi)嵌的一個(gè)32位值作為掩碼的;掩碼鍵只有在掩碼位設(shè)置為1的時(shí)候存在;
7)Extension data: x位,如果客戶端與服務(wù)端之間沒有特殊約定,那么擴(kuò)展數(shù)據(jù)的長(zhǎng)度始終為0,任何的擴(kuò)展都必須指定擴(kuò)展數(shù)據(jù)的長(zhǎng)度,或者長(zhǎng)度的計(jì)算方式,以及在握手時(shí)如何確定正確的握手方式。如果存在擴(kuò)展數(shù)據(jù),則擴(kuò)展數(shù)據(jù)就會(huì)包括在負(fù)載數(shù)據(jù)的長(zhǎng)度之內(nèi);
8)Application data: y位,任意的應(yīng)用數(shù)據(jù),放在擴(kuò)展數(shù)據(jù)之后,應(yīng)用數(shù)據(jù)的長(zhǎng)度=負(fù)載數(shù)據(jù)的長(zhǎng)度-擴(kuò)展數(shù)據(jù)的長(zhǎng)度;
9)Payload data: (x+y)位,負(fù)載數(shù)據(jù)為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)長(zhǎng)度之和;
更多細(xì)節(jié)請(qǐng)參考RFC6455-數(shù)據(jù)幀,這里不作贅述。
針對(duì)上面的各個(gè)字段的介紹,有一個(gè)Mask的需要說一下。
掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機(jī)數(shù)。掩碼操作不會(huì)影響數(shù)據(jù)載荷的長(zhǎng)度。
掩碼、反掩碼操作都采用如下算法。
首先,假設(shè):
1)original-octet-i:為原始數(shù)據(jù)的第i字節(jié);
2)transformed-octet-i:為轉(zhuǎn)換后的數(shù)據(jù)的第i字節(jié);
3)j:為i mod 4的結(jié)果;
4)masking-key-octet-j:為mask key第j字節(jié)。
算法描述為:?original-octet-i 與 masking-key-octet-j 異或后,得到 transformed-octet-i。
即:?j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
用代碼實(shí)現(xiàn):
const mask = (source, mask, output, offset, length) => {
??for(vari = 0; i < length; i++) {
????output[offset + i] = source[i ] ^ mask[i & 3];
??}
};
解掩碼是反過來的操作:
const unmask = (buffer, mask) => {
??// Required until [url=https://github.com/nodejs/node/issues/9006]https://github.com/nodejs/node/issues/9006[/url] is resolved.
??const length = buffer.length;
??for(vari = 0; i < length; i++) {
????buffer[i ] ^= mask[i & 3];
??}
};
同樣的為什么需要掩碼操作,也可以參考之前的那篇文章:《理論聯(lián)系實(shí)際:從零理解WebSocket的通信原理、協(xié)議格式、安全性》,完整的我就不列舉了。
需要注意的重點(diǎn),我引用一下:
WebSocket協(xié)議中,數(shù)據(jù)掩碼的作用是增強(qiáng)協(xié)議的安全性。但數(shù)據(jù)掩碼并不是為了保護(hù)數(shù)據(jù)本身,因?yàn)樗惴ū旧硎枪_的,運(yùn)算也不復(fù)雜。除了加密通道本身,似乎沒有太多有效的保護(hù)通信安全的辦法。
那么為什么還要引入掩碼計(jì)算呢,除了增加計(jì)算機(jī)器的運(yùn)算量外似乎并沒有太多的收益(這也是不少同學(xué)疑惑的點(diǎn))。
答案還是兩個(gè)字: 安全。但并不是為了防止數(shù)據(jù)泄密,而是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。
5、socket.io
5.1 本節(jié)引言

介紹完上一節(jié)WebSocket協(xié)議,我們把視線轉(zhuǎn)移到現(xiàn)代Web端即時(shí)通訊技術(shù)的第二個(gè)利器:socket.io。
估計(jì)有讀者就會(huì)問,WebSocket和socket.io有啥區(qū)別啊?
在了解socket.io之前,我們先聊聊傳統(tǒng)Web端即時(shí)通訊“長(zhǎng)連接”技術(shù)的實(shí)現(xiàn)背景。
5.2 傳統(tǒng)Web長(zhǎng)連接的技術(shù)實(shí)現(xiàn)背景
在現(xiàn)實(shí)的Web端產(chǎn)品中,并不是所有的Web客戶端都支持長(zhǎng)連接的,或者換句話說,在WebSocket協(xié)議出來之前,是三種方式去實(shí)現(xiàn)WebSocket類似的功能的。
這三種方式是:
1)Flash:使用Flash是一種簡(jiǎn)單的方法。不過很明顯的缺點(diǎn)就是Flash并不會(huì)安裝在所有客戶端上,比如iPhone/iPad。
2)Long-Polling:也就是眾所周之的“長(zhǎng)輪詢”,在過去,這是一種有效的技術(shù),但并沒有對(duì)消息發(fā)送進(jìn)行優(yōu)化。雖然我不會(huì)把AJAX長(zhǎng)輪詢當(dāng)做一種hack技術(shù),但它確實(shí)不是一個(gè)最優(yōu)方法;
3)Comet:在過去,這被稱為Web端的“服務(wù)器推”技術(shù),相對(duì)于傳統(tǒng)的 Web 應(yīng)用, 開發(fā) Comet 應(yīng)用具有一定的挑戰(zhàn)性,具體請(qǐng)見《Comet技術(shù)詳解:基于HTTP長(zhǎng)連接的Web端實(shí)時(shí)通信技術(shù)》。
那么如果單純地使用WebSocket的話,那些不支持的客戶端怎么辦呢?難道直接放棄掉?
當(dāng)然不是。Guillermo Rauch大神寫了socket.io這個(gè)庫(kù),對(duì)WebSocket進(jìn)行封裝,從而讓長(zhǎng)連接滿足所有的場(chǎng)景,不過當(dāng)然得配合使用對(duì)應(yīng)的客戶端代碼。
socket.io將會(huì)使用特性檢測(cè)的方式來決定以websocket/ajax長(zhǎng)輪詢/flash等方式建立連接。
那么socket.io是如何做到這些的呢?
我們帶著以下幾個(gè)問題去學(xué)習(xí):
1)socket.io到底有什么新特性?
2)socket.io是怎么實(shí)現(xiàn)特性檢測(cè)的?
3)socket.io有哪些坑呢?
4)socket.io的實(shí)際應(yīng)用是怎樣的,需要注意些什么?
如果有童鞋對(duì)上述問題已經(jīng)清楚,想必就沒有往下讀的必要了。
5.3 socket.io的介紹
通過前面章節(jié),讀者們都知道了WebSocket的功能,那么socket.io相對(duì)于WebSocket,在此基礎(chǔ)上封裝了一些什么新東西呢?
socket.io其實(shí)是有一套封裝了websocket的協(xié)議,叫做engine.io協(xié)議,在此協(xié)議上實(shí)現(xiàn)了一套底層雙向通信的引擎Engine.io。
而socket.io則是建立在engine.io上的一個(gè)應(yīng)用層框架而已。所以我們研究的重點(diǎn)便是engine.io協(xié)議。
在socket.io的README中提到了其實(shí)現(xiàn)的一些新特性(回答了問題一):
1)可靠性:連接依然可以建立即使應(yīng)用環(huán)境存在: 代理或者負(fù)載均衡器 個(gè)人防火墻或者反病毒軟件;
2)支持自動(dòng)連接: 除非特別指定,否則一個(gè)斷開的客戶端會(huì)一直重連服務(wù)器直到服務(wù)器恢復(fù)可用狀態(tài);
3)斷開連接檢測(cè):在Engine.io層實(shí)現(xiàn)了一個(gè)心跳機(jī)制,這樣允許客戶端和服務(wù)器知道什么時(shí)候其中的一方不能響應(yīng)。該功能是通過設(shè)置在服務(wù)端和客戶端的定時(shí)器實(shí)現(xiàn)的,在連接握手的時(shí)候,服務(wù)器會(huì)主動(dòng)告知客戶端心跳的間隔時(shí)間以及超時(shí)時(shí)間;
4)二進(jìn)制的支持:任何序列化的數(shù)據(jù)結(jié)構(gòu)都可以用來發(fā)送;
5)跨瀏覽器的支持:該庫(kù)甚至支持到IE8;
6)支持復(fù)用:為了在應(yīng)用程序中將創(chuàng)建的關(guān)注點(diǎn)隔離開來,Socket.io允許你創(chuàng)建多個(gè)namespace,這些namespace擁有單獨(dú)的通信通道,但將共享相同的底層連接;
7)支持Room:在每一個(gè)namespace下,你可以定義任意數(shù)量的通道,我們稱之為"房間",你可以加入或者離開房間,甚至廣播消息到指定的房間。
注意:Socket.IO不是WebSocket的實(shí)現(xiàn),雖然 Socket.IO確實(shí)在可能的情況下會(huì)去使用WebSocket作為一個(gè)transport,但是它添加了很多元數(shù)據(jù)到每一個(gè)報(bào)文中:報(bào)文的類型以及namespace和ack Id。這也是為什么標(biāo)準(zhǔn)WebSocket客戶端不能夠成功連接上 Socket.IO 服務(wù)器,同樣一個(gè) Socket.IO 客戶端也連接不上標(biāo)準(zhǔn)WebSocket服務(wù)器的原因。
5.4 engine.io協(xié)議介紹
完整的engine.io協(xié)議的握手過程如下圖:

當(dāng)前engine.io協(xié)議的版本是3,我們根據(jù)上圖來大致介紹一下engine.io協(xié)議。
5.4.1)engine.io協(xié)議請(qǐng)求字段:
我們看到的是請(qǐng)求的url和WebSocket不大一樣,解釋一下:
1)EIO=3: 表示的是使用的是Engine.io協(xié)議版本3;
2)transport=polling/websocket: 表示使用的長(zhǎng)連接方式是輪詢還是WebSocket;
3)t=xxxxx: 代碼中使用yeast根據(jù)時(shí)間戳生成一個(gè)唯一的字符串;
4)sid=xxxx: 客戶端和服務(wù)器建立連接之后獲取到的session id,客戶端拿到之后必須在每次請(qǐng)求中追加這個(gè)字段。
除了上述的3個(gè)字段,協(xié)議還描述了下面幾個(gè)字段:
1)j: 如果transport是polling,但是要求有一個(gè)JSONP的響應(yīng),那么j就應(yīng)該設(shè)置為JSONP響應(yīng)的索引值;
2)b64: 如果客戶端不支持XHR,那么客戶端應(yīng)該設(shè)置b64=1傳給服務(wù)器,告知服務(wù)器所有的二進(jìn)制數(shù)據(jù)應(yīng)該以base64編碼后再發(fā)送。
另外engine.io默認(rèn)的path是?/engine.io,socket.io在初始化的時(shí)候設(shè)置為了?/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');
5.4.2)數(shù)據(jù)包編碼要求:
engine.io協(xié)議的數(shù)據(jù)包編碼有自己的一套格式,在協(xié)議介紹上engine.io-protocol,定義了兩種編碼類型: packet和payload。
一個(gè)編碼過的packet是下面這種格式:
<packettype id>[<data>]
然后協(xié)議定義了下面幾種packet type(采用數(shù)字進(jìn)行標(biāo)識(shí)):
1)0(open): 當(dāng)開始一個(gè)新的transport的時(shí)候,服務(wù)端會(huì)發(fā)送該類型的packet;
2)1(close): 請(qǐng)求關(guān)閉這個(gè)transport但是不要自己關(guān)閉關(guān)閉連接;
3)2(ping): 由客戶端發(fā)送的ping包,服務(wù)端必須回應(yīng)一個(gè)包含相同數(shù)據(jù)的pong包;
4)3(pong): 響應(yīng)ping包,服務(wù)端發(fā)送;
5)4(message): 實(shí)際消息,在客戶端和服務(wù)端都可以監(jiān)聽message事件獲取消息內(nèi)容;
6)5(upgrade): 在engine.io切換transport之前,它會(huì)用來測(cè)試服務(wù)端和客戶端是否在該transport上通信。如果測(cè)試成功,客戶端會(huì)發(fā)送一個(gè)upgrade包去讓服務(wù)器刷新它的緩存并切換到新的transport;
7)6(noop): 主要用來強(qiáng)制一個(gè)輪詢循環(huán)當(dāng)收到一個(gè)WebSocket連接的時(shí)候。
那payload也有對(duì)應(yīng)的格式要求:
1)如果當(dāng)只有發(fā)送string并且不支持XHR的時(shí)候,其編碼格式是::[:[...]];
2)當(dāng)不支持XHR2并且發(fā)送二進(jìn)制數(shù)據(jù),但是使用base64編碼字符串的時(shí)候,其編碼格式是::b[...];
3)當(dāng)支持XHR2的時(shí)候,所有的數(shù)據(jù)都被編碼成二進(jìn)制,格式是:<0 for string data, 1 for binary data>[...];
4)如果發(fā)送的內(nèi)容混雜著UTF-8的字符和二進(jìn)制數(shù)據(jù),字符串的每個(gè)字符被寫成一個(gè)字符編碼,用1個(gè)字節(jié)表示。
注意:payload的編碼要求不適用于WebSocket的通信。
針對(duì)上面的編碼要求,我們隨便舉個(gè)例子.
之前在第一條polling請(qǐng)求的時(shí)候,服務(wù)端編碼發(fā)送了這個(gè)數(shù)據(jù):
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":60000}2:40
根據(jù)上面的知識(shí),我們知道第一次服務(wù)端會(huì)發(fā)送一個(gè)open的數(shù)據(jù)包。
所以組裝出來的packet是:
0
然后服務(wù)端會(huì)告知客戶端去嘗試升級(jí)到websocket,并且告知對(duì)應(yīng)的sid。
于是整合后便是:
0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}
接著根據(jù)payload的編碼格式,因?yàn)槭莝tring,且長(zhǎng)度是97個(gè)字節(jié)。
所以是:
97:0{"sid":"Peed250dk55pprwgAAAA","upgrades":"websocket","pingInterval":25000,"pingTimeout":60000}
接著第二部分?jǐn)?shù)據(jù)是message包類型,并且數(shù)據(jù)是0,所以是40,長(zhǎng)度為2字節(jié),所以是2:40,最后就拼成剛才大家看到的結(jié)果。
注意:
ping/pong的間隔時(shí)間是服務(wù)端告知客戶端的:"pingInterval":25000,"pingTimeout":60000,也就是說心跳時(shí)間默認(rèn)是25秒,并且等待pong響應(yīng)的時(shí)間默認(rèn)是60s。
5.5 升級(jí)協(xié)議的必備過程
協(xié)議定義了transport升級(jí)到websocket需要經(jīng)歷一個(gè)必須的過程。
如下圖:

WebSocket的測(cè)試開始于發(fā)送probe,如果服務(wù)器也響應(yīng)probe的話,客戶端就必須發(fā)送一個(gè)upgrade包。
為了確保不會(huì)丟包,只有在當(dāng)前transport的所有buffer被刷新并且transport被認(rèn)為paused的時(shí)候才可以發(fā)送upgrade包。服務(wù)端收到upgrade包的時(shí)候,服務(wù)端必須假設(shè)這是一個(gè)新的通道并發(fā)送所有已存的緩存到這個(gè)通道上
在Chrome上的效果如下:

5.6 engine.io的代碼實(shí)現(xiàn)
熟悉了engine.io協(xié)議之后,我們看看代碼是怎么實(shí)現(xiàn)主流程的。
客戶端的engine.io的主要實(shí)現(xiàn)流程我們?cè)谏厦嫖淖纸榻B了。
結(jié)合代碼engine.io,畫了這么一個(gè)客戶端流程圖:

服務(wù)端的代碼和客戶端非常相似,其實(shí)現(xiàn)流程圖如下:

6、SSE
6.1 本節(jié)引言

本文前兩節(jié)分析了WebSocket和socket.io,現(xiàn)在我們來看看SSE。
很多人也許好奇,有了WebSocket這種實(shí)時(shí)通信,為什么還需要SSE呢?
答案其實(shí)很簡(jiǎn)單:那就是SSE其實(shí)是單向通信,而WebSocket是雙向通信。
比如:在股票行情、新聞推送的這種只需要服務(wù)器發(fā)送消息給客戶端場(chǎng)景中,使用SSE可能更加合適。
另外:SSE是使用HTTP傳輸?shù)?,這意味著我們不需要一個(gè)特殊的協(xié)議或者額外的實(shí)現(xiàn)就可以使用。而WebSocket要求全雙工連接和一個(gè)新的WebSocket服務(wù)器去處理。加上SSE在設(shè)計(jì)的時(shí)候就有一些WebSocket沒有的特性,比如自動(dòng)重連接、event IDs、以及發(fā)送隨機(jī)事件的能力,所以各有各的特長(zhǎng),我們需要根據(jù)實(shí)際應(yīng)用場(chǎng)景,去選擇不同的應(yīng)用方案。
6.2 SSE介紹
SSE的簡(jiǎn)單模型是:一個(gè)客戶端去從服務(wù)器端訂閱一條“流”,之后服務(wù)端可以發(fā)送消息給客戶端直到服務(wù)端或者客戶端關(guān)閉該“流”,所以SSE全稱叫“server-sent-event”。
相比以前的輪詢,SSE可以為B2C帶來更高的效率。
有一張圖片畫出了二者的區(qū)別:

6.3 SSE數(shù)據(jù)幀的格式
SSE必須編碼成utf-8的格式,消息的每個(gè)字段使用"\n"來做分割,并且需要下面4個(gè)規(guī)范定義好的字段。
這4個(gè)字段是:
1)Event: 事件類型;
2)Data: 發(fā)送的數(shù)據(jù);
3)ID: 每一條事件流的ID;
4)Retry: 告知瀏覽器在所有的連接丟失之后重新開啟新的連接等待的時(shí)間,在自動(dòng)重新連接的過程中,之前收到的最后一個(gè)事件流ID會(huì)被發(fā)送到服務(wù)端。
下圖是通過wireshark抓包得到的數(shù)據(jù)包的原始格式:

6.4 SSE通信過程
SSE的通信過程比較簡(jiǎn)單,底層的一些實(shí)現(xiàn)都被瀏覽器給封裝好了,包括數(shù)據(jù)的處理。
大致流程如下:

在瀏覽器中截圖如下:

攜帶的數(shù)據(jù)是JSON格式的,瀏覽器都幫你整合成為一個(gè)Object:

在wireshark中,其通信流程如下。
發(fā)送請(qǐng)求:

得到響應(yīng):

在開始推送信息流之前,服務(wù)器還會(huì)發(fā)送一個(gè)客戶端會(huì)忽略掉的包,這個(gè)具體原因不清楚:

斷開連接后的重傳:
?

6.5 SSE的簡(jiǎn)單使用示例
瀏覽器端的使用:
const es = new EventSource('/sse')
服務(wù)端的使用:
const sseStream = new SseStream(req)
sseStream.pipe(res)
sseStream.write({
??id: sendCount,
??event: 'server-time',
??retry: 20000, // 告訴客戶端,如果斷開連接后,20秒后再重試連接
??data: {ts: newDate().toTimeString(), count: sendCount++}
})
更多API使用和demo介紹分別參考:SSE API、demo代碼。
6.6 兼容性及缺點(diǎn)
兼容性:
▲ 上圖來自?https://caniuse.com/?search=Server-Sent-Events
缺點(diǎn):
1)因?yàn)槭欠?wù)器 -> 客戶端的,所以它不能處理客戶端請(qǐng)求流;
2)因?yàn)槭敲鞔_指定用于傳輸U(kuò)TF-8數(shù)據(jù)的,所以對(duì)于傳輸二進(jìn)制流是低效率的,即使你轉(zhuǎn)為base64的話,反而增加帶寬的負(fù)載,得不償失。
7、參考資料
[1]?WebSocket API文檔
[2]?SSE API文檔
[3]?新手入門貼:史上最全Web端即時(shí)通訊技術(shù)原理詳解
[4]?Web端即時(shí)通訊技術(shù)盤點(diǎn):短輪詢、Comet、Websocket、SSE
[5]?SSE技術(shù)詳解:一種全新的HTML5服務(wù)器推送事件技術(shù)
[6]?Comet技術(shù)詳解:基于HTTP長(zhǎng)連接的Web端實(shí)時(shí)通信技術(shù)
[7]?新手快速入門:WebSocket簡(jiǎn)明教程
[8]?WebSocket詳解(三):深入WebSocket通信協(xié)議細(xì)節(jié)
[9]?WebSocket詳解(四):刨根問底HTTP與WebSocket的關(guān)系(上篇)
[10]?WebSocket詳解(五):刨根問底HTTP與WebSocket的關(guān)系(下篇)
[11]?使用WebSocket和SSE技術(shù)實(shí)現(xiàn)Web端消息推送
[12]?詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE、Websocket
[13]?MobileIMSDK-Web的網(wǎng)絡(luò)層框架為何使用的是Socket.io而不是Netty?
[14]?理論聯(lián)系實(shí)際:從零理解WebSocket的通信原理、協(xié)議格式、安全性
[15]?WebSocket從入門到精通,半小時(shí)就夠!
[16]?WebSocket硬核入門:200行代碼,教你徒手?jǐn)]一個(gè)WebSocket服務(wù)器
[17]?網(wǎng)頁(yè)端IM通信技術(shù)快速入門:短輪詢、長(zhǎng)輪詢、SSE、WebSocket
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào)。
同步發(fā)布鏈接是:http://www.52im.net/thread-3695-1-1.html