作為前端工程師,我們幾乎每天都在使用 ajax / fetch 請(qǐng)求與后端進(jìn)行數(shù)據(jù)交互,這種基于請(qǐng)求-響應(yīng)的通訊模式,我們?cè)偈炀毑贿^(guò)了,無(wú)論是C端產(chǎn)品或者是B端產(chǎn)品,都離不開(kāi)這種通訊模式。但是像即時(shí)通訊IM類場(chǎng)景,通常不會(huì)選擇這種“你來(lái)我回”的通信模式,而是會(huì)選擇 WebSocket 這類的全雙工通信模式。
本文會(huì)帶您全方位去了解一下 WebSocket 的本質(zhì),方便您搞清楚“Connection: Upgrade 是什么意思,為什么是它?”、“Upgrade: WebSocket 又是什么意思?這就可以雙向通信了?”、“WebSocket 和 HTTP/TCP 到底有什么關(guān)聯(lián)?八股文背了還是不理解”之類的問(wèn)題,幫助您無(wú)論面試或工作時(shí)被問(wèn)到 WebSocket 都能有更多細(xì)節(jié)可以聊,妥妥的一個(gè)加分項(xiàng)!
最后通過(guò)一個(gè)在線聊天室實(shí)戰(zhàn)案例帶大家熟悉下 WebSocket 的全棧使用,可點(diǎn)擊在線聊天室[3]進(jìn)行體驗(yàn)。
圖片
WebSocket 是一種網(wǎng)絡(luò)通信協(xié)議,它建立在 HTTP 之上,提供了在單個(gè) TCP 連接上進(jìn)行全雙工通信的能力。這意味著服務(wù)器和客戶端可以互相發(fā)送和接收消息,而不需要每次都重新建立連接。WebSocket 最初由 HTML5 規(guī)范定義,具體可以參考WebSockets Living Standard[4]。而現(xiàn)在,WebSocket 已被廣泛支持并應(yīng)用于各種應(yīng)用,包括實(shí)時(shí)聊天、多人在線游戲、股票交易系統(tǒng)等需要實(shí)時(shí)數(shù)據(jù)更新的場(chǎng)景。
作為前端開(kāi)發(fā),對(duì) API 的兼容性還是非常敏感的,我們先來(lái)看看 WebSocket 的兼容性怎么樣。
圖片
可以看到,主流瀏覽器都支持了 WebSocket,IE10 及以上版本也對(duì) WebSocket 提供了完備的支持,所以我們可以大膽地使用起來(lái)!
瀏覽器 JS 運(yùn)行時(shí)提供了 WebSocket[5] 這個(gè) API,可以用來(lái)創(chuàng)建和管理 WebSocket 連接。
使用也非常簡(jiǎn)單,構(gòu)造實(shí)例后只有幾個(gè)簡(jiǎn)單的方法調(diào)用,一看就會(huì)。
// 創(chuàng)建一個(gè)新的 WebSocket 連接const socket = new WebSocket('ws://your-websocket-server-url')// 監(jiān)聽(tīng)連接打開(kāi)socket.onopen = (e) => { console.log('WebSocket is connected.') // 連接打開(kāi)后,你可以發(fā)送消息 socket.send('Hello Server!')}// 監(jiān)聽(tīng)消息socket.onmessage = (e) => { console.log('Message from server: ', e.data) }// 監(jiān)聽(tīng)關(guān)閉socket.onclose = (e) => { console.log('Connection closed.') }// 監(jiān)聽(tīng)錯(cuò)誤socket.onerror = (err) => { console.error('WebSocket Error: ', err) }// 如果你想主動(dòng)關(guān)閉連接,可以調(diào)用 close 方法// socket.close()
wss:// 是 ws:// 的 TLS 加持版,可以類比于 https:// 和 http://
WebSocket 前端的使用非常簡(jiǎn)單,我自然會(huì)聯(lián)想到:如果我做全棧開(kāi)發(fā),用 Nodejs 實(shí)現(xiàn) WebSocket 服務(wù)端,有原生的模塊可以支持嗎?
經(jīng)過(guò)查詢了解到,Node 原生模塊中并未直接支持 WebSocket 服務(wù)端的開(kāi)箱使用,一個(gè)比較流行的庫(kù)是 ws[6]。
那么 ws 這個(gè)庫(kù)是怎么實(shí)現(xiàn) WebSocket 服務(wù)端的呢?怎么才能和瀏覽器的 WebSocket 實(shí)現(xiàn)對(duì)接上?
直接讀源碼肯定是看不懂的,即便看懂了一些過(guò)程調(diào)用,也是懵逼的,我們往下看。
我們知道,通訊是基于協(xié)議的,WebSocket 也有它的專屬協(xié)議。ws 的實(shí)現(xiàn)它也是要遵循這個(gè)協(xié)議,才能和客戶端實(shí)現(xiàn)匹配上,完成通訊。
這個(gè)協(xié)議我們?nèi)ツ睦锟茨兀扛鶕?jù) wikipedia 的介紹,我們知道,WebSocket 的標(biāo)準(zhǔn)化是基于IETF 的 RFC 6455 WebSocket Protocol[7]。大致瀏覽后,我圈出了協(xié)議里一些值得關(guān)注的內(nèi)容。閱讀這類協(xié)議時(shí),我們可以先挑重點(diǎn)看,對(duì)協(xié)議有一個(gè)基本的認(rèn)識(shí)即可。
圖片
圖片
我們了解到一些關(guān)鍵詞:
雖然有了一些關(guān)鍵詞在腦海中,但我們對(duì)整個(gè)通訊過(guò)程肯定還有一連串疑問(wèn)。帶著疑問(wèn),我們繼續(xù)往下看協(xié)議的具體內(nèi)容。
圖片
再往下翻一翻協(xié)議,我們能翻到最關(guān)鍵的部分,這也是面試?yán)锬芎兔嬖嚬俅档膬?nèi)容,請(qǐng)仔細(xì)看!
很多人面試被問(wèn)到 WebSocket,就說(shuō) WebSocket 可以雙向通信,這是和 HTTP 最大的不同。講道理,這種回復(fù)面試官已經(jīng)聽(tīng)膩了。
如果你能告訴面試官,WebSocket 的協(xié)議涉及到以下幾個(gè) HTTP 頭部字段,并簡(jiǎn)述一下各個(gè)字段的簡(jiǎn)單含義,我相信你的面試絕對(duì)加分!
圖片
我們先看請(qǐng)求頭:
再看響應(yīng)頭:
base64-encode(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
引用 wikipedia 的一張圖,可以看得更清楚!
圖片
建立了連接握手后,WebSocket 就可以發(fā)送和接受消息數(shù)據(jù)了,消息是由一個(gè)或多個(gè)幀組成的。我們先看看這兩張圖了解一下數(shù)據(jù)幀的結(jié)構(gòu)。
圖片
圖片
其中 Opcode 是操作碼,具體見(jiàn)下表。
圖片
由于存在數(shù)據(jù)比較大的可能,這時(shí)需要切片傳輸,WebSocket 消息數(shù)據(jù)支持分片傳輸。
WebSocket 能發(fā)送文本數(shù)據(jù)或二進(jìn)制數(shù)據(jù),這個(gè)是體現(xiàn)在 OpCode 上。如果起始幀的 OpCode 是 1,則代表是文本數(shù)據(jù);如果起始幀的 OpCode 是 2,則代表是二進(jìn)制數(shù)據(jù)。
我們看看規(guī)范中,關(guān)閉握手這部分是怎么說(shuō)的。
圖片
WebSocket 任何一端都可以發(fā)起關(guān)閉連接。
當(dāng)一方準(zhǔn)備關(guān)閉連接時(shí),應(yīng)該發(fā)送 Close Frame 開(kāi)始關(guān)閉握手,之后不應(yīng)該再發(fā)送任何數(shù)據(jù);
另一方收到 Close Frame 時(shí),需要回復(fù) Close Frame,并且準(zhǔn)備釋放資源,同時(shí)也應(yīng)該丟棄后續(xù)從這個(gè)連接上可能接收到的數(shù)據(jù)。
發(fā)起方收到 Close Frame 控制幀后,關(guān)閉連接釋放資源,不再接收數(shù)據(jù)。
這種 WebSocket 關(guān)閉握手機(jī)制也是在 TCP 握手機(jī)制上的一種補(bǔ)充,更好地保證端到端通信的可靠性!
有的朋友可能會(huì)考慮到這個(gè)問(wèn)題:當(dāng)客戶端發(fā)送 Close Frame 后,服務(wù)端正常接收到,并且回復(fù) Close Frame,但是由于網(wǎng)絡(luò)問(wèn)題客戶端沒(méi)有服務(wù)端響應(yīng)的 Close Frame,這種情況是怎么關(guān)閉 WebSocket 連接的?
實(shí)際上,TCP 連接也有它的超時(shí)和重試機(jī)制,當(dāng)一段時(shí)間內(nèi)沒(méi)有數(shù)據(jù)傳輸時(shí),也會(huì)斷開(kāi)連接。所以我們無(wú)需擔(dān)心這一點(diǎn)。
當(dāng)這種沒(méi)有成功關(guān)閉握手但是關(guān)閉了 TCP 連接的情況發(fā)生時(shí),onclose事件回調(diào)中收到的錯(cuò)誤碼應(yīng)該是 1006,這一點(diǎn)可以在上面的表格中找到。
正常關(guān)閉是 1000。
在實(shí)際業(yè)務(wù)實(shí)現(xiàn)上,還會(huì)通過(guò) ping-pong 之類的心跳檢測(cè)機(jī)制來(lái)保證可靠性。
有了這些知識(shí)儲(chǔ)備后,再來(lái)看 ws 的實(shí)現(xiàn)源碼,可能就會(huì)有頭緒一點(diǎn)。
當(dāng)你看到這部分,你會(huì)知道它在校驗(yàn)頭部字段是否符合協(xié)議要求,準(zhǔn)備升級(jí)協(xié)議...
圖片
當(dāng)你看到這個(gè) 101 狀態(tài)碼,你會(huì)恍然大悟:“哦,原來(lái)是在這里完成了協(xié)議的升級(jí)!”雖然還有些細(xì)節(jié)看不懂,但是無(wú)傷大雅!
圖片
當(dāng)你看到這里時(shí),你會(huì)知道,如果客戶端嘗試通過(guò)普通的 HTTP 請(qǐng)求來(lái)連接 WebSocket 服務(wù),服務(wù)端應(yīng)該返回 426 Upgrade Required 告訴客戶端,“你該升級(jí)協(xié)議再跟我對(duì)話!”
圖片
本文不是源碼解讀,點(diǎn)到為止,我們往下看。
實(shí)際將 WebSocket 運(yùn)用到生產(chǎn)環(huán)境時(shí),我們一般不會(huì)直接使用 ws 這種協(xié)議實(shí)現(xiàn)庫(kù),而是會(huì)選擇在應(yīng)用層面進(jìn)行了一些封裝的庫(kù),比如 Socket.IO[8]。
圖片
這是因?yàn)樵?WebSocket 實(shí)際使用過(guò)程中,還有很多問(wèn)題要考慮,比如心跳檢測(cè)、優(yōu)雅降級(jí)、房間隔離、命名空間隔離、API 的易用性等。而這些,Socket.IO 已經(jīng)開(kāi)箱支持。
準(zhǔn)確說(shuō),Socket.IO 并非是一個(gè) WebSocket 實(shí)現(xiàn),而是一個(gè)事件驅(qū)動(dòng)的低延遲雙向通訊方案。
它的底層通訊不一定是基于 WebSocket 的,可能會(huì)根據(jù)情況選擇 HTTP 長(zhǎng)輪詢、WebTransport[9]。
WebTransport 是一個(gè)基于 HTTP/3 的通訊技術(shù),可實(shí)現(xiàn)可靠通信和不可靠通信。HTTP/3 底層基于 Google 的 QUIC 協(xié)議,而 QUIC 協(xié)議是基于 UDP 的。
圖片
Socket.IO 有它的約定和規(guī)則,或者叫協(xié)議,只要遵循這個(gè)協(xié)議,就能完成客戶端和服務(wù)端的實(shí)現(xiàn),所以你會(huì)看到,它也有多語(yǔ)言的實(shí)現(xiàn),甚至在客戶端還有小程序的實(shí)現(xiàn)。
圖片
這個(gè)協(xié)議其實(shí)也就對(duì)應(yīng)著Socket.IO 的底層引擎 Engine.IO[10]。
圖片
雖然現(xiàn)在大部分瀏覽器都支持了 WebSocket,但是也不排除某些遠(yuǎn)古項(xiàng)目的存在,它必須運(yùn)行在“古董”瀏覽器版本之上。Socket.IO 考慮到了這一點(diǎn),它的自動(dòng)優(yōu)雅降級(jí)完美解決了這一問(wèn)題。
圖片
Socket.IO 的心跳檢測(cè)機(jī)制和自動(dòng)重連也是實(shí)際業(yè)務(wù)中必不可少的!
更多的特性還有:
當(dāng)我們打開(kāi)一個(gè) Socket.IO 的客戶端頁(yè)面時(shí),會(huì)發(fā)現(xiàn) Network 里發(fā)出了多個(gè)請(qǐng)求,在 101 websocket 連接建立之前,有 4 個(gè) xhr 請(qǐng)求,其中還有一個(gè)是 POST 請(qǐng)求。
圖片
Socket.IO 在升級(jí)機(jī)制中解釋了這一點(diǎn),直接建立可靠可用的 WebSocket 連接并非一件很輕松的事情,通常從 HTTP 開(kāi)始平滑升級(jí)到 WebSocket,對(duì)連接的可靠性和用戶體驗(yàn)來(lái)說(shuō)是更好的。
升級(jí)協(xié)議會(huì)經(jīng)歷這么一些步驟,對(duì)應(yīng)著我們?cè)谏厦婵吹降膸讉€(gè) Network 請(qǐng)求。
圖片
我這個(gè)項(xiàng)目開(kāi)始得很早,所以EIO=3,代表協(xié)議版本號(hào)是3,目前 Engine.IO 已經(jīng)升級(jí)到版本4了。
在 Socket.IO 的 HTTP 長(zhǎng)輪詢模式中,使用長(zhǎng)時(shí)間運(yùn)行的 GET 請(qǐng)求接收數(shù)據(jù),使用短期運(yùn)行的 POST 請(qǐng)求發(fā)送數(shù)據(jù)。
了解了這些機(jī)制,并且查看 API 用法后,就可以開(kāi)始運(yùn)用了,一些高級(jí)用法可以在使用過(guò)程中再去探索!
基于以上理解,我們開(kāi)始搭建博客項(xiàng)目中的聊天室功能,我們會(huì)實(shí)現(xiàn)這些主要能力:
我們首先把依賴安裝好,客戶端使用socket.io-client[11],服務(wù)端使用socket.io[12]即可。
第一步是把 WebSocket 服務(wù)啟動(dòng)。由于本項(xiàng)目開(kāi)始較早,socket.io 版本是 2.5,大家對(duì)照文檔的時(shí)候按 2.x 文檔看就好。
圖片
socket.io 可以利用已經(jīng)存在的 HTTP 服務(wù),由于項(xiàng)目是用 Express 搭建的,我們直接與 Express 共享一個(gè) HTTP 服務(wù)即可。
io 實(shí)例化后,監(jiān)聽(tīng)到 connection 事件就代表有客戶端過(guò)來(lái)了,可以開(kāi)始干活了。我這里是把聊天室相關(guān)的邏輯都放在了 chatroom 這個(gè)命名空間下。
圖片
of 的作用就是初始化并使用命名空間。
第二步是建立連接。首先引入依賴。
import io from "socket.io-client";
再進(jìn)行實(shí)例化,得到一個(gè) socket 實(shí)例。
this.socket = io(process.env.VUE_APP_SOCKET_SERVER + "/chatroom");
有了這個(gè) socket,我們就能監(jiān)聽(tīng)各種事件了。
圖片
當(dāng)一個(gè)客戶端連接上服務(wù)器時(shí),服務(wù)器會(huì)發(fā)送一條消息,“hello,歡迎您加入在線聊天室!”這是通過(guò)單播實(shí)現(xiàn)的,只要拿著socket對(duì)象,調(diào)用其emit方法就行。
圖片
除了對(duì)這個(gè)客戶端打招呼外,還需要告訴其他用戶,有新人加入了。這是通過(guò)socket.broadcast廣播實(shí)現(xiàn)的。
socket.broadcast.emit('broadcast', param);
當(dāng)有人退出聊天室,會(huì)觸發(fā) disconnect 事件,此時(shí)我們可以廣播通知其他人。
圖片
這就是我們?cè)谇岸隧?yè)面看到的效果:
圖片
由于我們做的是聊天室,相當(dāng)于一個(gè)群聊,就不涉及到單播聊天了,直接用廣播就行。
用戶在客戶端發(fā)聊天消息時(shí),是用到socket對(duì)象的emit進(jìn)行發(fā)送。
圖片
這個(gè)chat事件是在客戶端連接上服務(wù)端時(shí)開(kāi)始監(jiān)聽(tīng)的,在這個(gè)回調(diào)里,我們需要把內(nèi)容廣播給除發(fā)送者之外的其他用戶,子事件名是new_chat_content。
圖片
而其他用戶則會(huì)通過(guò)客戶端監(jiān)聽(tīng)廣播事件中的new_chat_content子事件拿到聊天數(shù)據(jù),最終呈現(xiàn)到界面上。
圖片
圖片
以上都是通訊上的設(shè)計(jì),了解了這個(gè)機(jī)制,UI 的展示就非常簡(jiǎn)單了,畢竟 UI = Render(data),不做更多介紹!
本文中,我首先分享了我對(duì) WebSocket 協(xié)議的一些理解,希望對(duì)還不太理解這塊的朋友起到一點(diǎn)幫助作用。面試?yán)铮琖ebSocket 是一個(gè)常問(wèn)的考點(diǎn),如果你回答的僅僅是“全雙工通信”,可能并不能起到一個(gè)很好的效果,把文中小知識(shí)甩面試官臉上吧,哈哈哈!
最終通過(guò)一個(gè)實(shí)際案例,帶大家理解一個(gè)聊天室功能的設(shè)計(jì)思路,在實(shí)際落地的過(guò)程中夯實(shí)對(duì) WebSocket 協(xié)議的理解。
本文鏈接:http://www.www897cc.com/showinfo-26-92103-0.html別背八股文了,WebSocket 是什么,我勸你花幾分鐘讓面試官驚艷!
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com