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