本教程展示如何在Node.js中使用WebSocket实现浏览器与服务器之间的双向交互通信。这一技术对于快速、实时的应用程序至关重要,例如仪表板、聊天应用和多人游戏。
網絡基於請求-響應的HTTP消息運作。瀏覽器發出URL請求,伺服器則回應數據。這可能導致瀏覽器對圖像、CSS、JavaScript等發出進一步請求,以及伺服器的相應回應,但伺服器不能隨意向瀏覽器發送數據。
長輪詢Ajax技術能使網絡應用看似實時更新,但對於真正的實時應用來說,這一過程過於局限。每秒輪詢在某些時候效率低下,在其他時候又太慢。
在瀏覽器建立初始連接後,伺服器發送事件是一種標準(流式)HTTP響應,能夠在任何時候從伺服器發送消息。然而,通道是單向的,瀏覽器無法發送消息回伺服器。對於真正快速的雙向通信,你需要WebSocket。
WebSocket概覽
術語WebSocket指的是一種TCP通信協議,使用ws://
或安全加密的wss://
。它與HTTP不同,儘管它可以運行在80或443端口上,以確保在阻止非網絡流量的地方也能工作。自2012年以來發布的大多數瀏覽器支持WebSocket協議。
在典型的實時網絡應用中,你至少需要一個網絡伺服器來提供網絡內容(HTML、CSS、JavaScript、圖像等)和一個WebSocket伺服器來處理雙向通信。
瀏覽器仍會向伺服器發出初始的WebSocket請求,開啟一個通訊管道。此後,無論是瀏覽器或伺服器均可透過該管道發送訊息,而另一端裝置將因此觸發事件。
與其他連接的瀏覽器通訊
在初始請求後,瀏覽器能夠向WebSocket伺服器發送及接收訊息。WebSocket伺服器亦能向其任一連線的客戶端瀏覽器發送及接收訊息。
點對點通訊並可行。即使BrowserA與BrowserB運行於同一裝置且連接至同一WebSocket伺服器,BrowserA仍無法直接向BrowserB發送訊息!BrowserA僅能將訊息傳送至伺服器,並期望伺服器依需求轉發給其他瀏覽器。
WebSocket伺服器支援
Node.js目前尚未內建WebSocket支援,儘管有傳言即將推出!本文中,我使用第三方ws模組,但還有數十種其他選擇。
內建WebSocket支援已包含於Deno與BunJavaScript執行環境中。
WebSocket 函式庫適用於多種執行環境,包括 PHP、Python 和 Ruby。此外,第三方 SaaS 解決方案如 Pusher 和 PubNub 也提供託管的 WebSocket 服務。
WebSocket 示範快速入門
聊天應用程式是 WebSocket 示範的 Hello, World!
,因此我為以下事項道歉:
-
缺乏原創性。儘管如此,聊天應用程式是解釋這些概念的絕佳方式。
-
無法提供完全託管的在線解決方案。我寧願不必監控和審查一連串匿名訊息!
複製或下載 GitHub 上的 node-wschat 儲存庫:
git clone https://github.com/craigbuckler/node-wschat
安裝 Node.js 相依性:
cd node-wschat
npm install
啟動聊天應用程式:
npm start
在多個瀏覽器或分頁中打開http://localhost:3000/(您也可以在查詢字串中定義您的聊天名稱,例如http://localhost:3000/?Craig)。在一個視窗中輸入內容並按下SEND或敲擊Enter,您會看到它在所有連接的瀏覽器中出現。
Node.js 程式碼概覽
Node.js 應用程式的index.js
入口檔案啟動了兩個伺服器:
-
一個在http://localhost:3000/運行的Express應用,使用EJS模板提供一個包含客戶端HTML、CSS和JavaScript的單頁面。瀏覽器JavaScript使用WebSocket API進行初始連接,然後發送和接收消息。
-
A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:
// WebSocket 伺服器 import WebSocket, { WebSocketServer } from 'ws'; const ws = new WebSocketServer({ port: cfg.wsPort }); // 客戶端連接 ws.on('connection', (socket, req) => { console.log(`connection from ${ req.socket.remoteAddress }`); // 接收訊息 socket.on('message', (msg, binary) => { // 廣播至所有客戶端 ws.clients.forEach(client => { client.readyState === WebSocket.OPEN && client.send(msg, { binary }); }); }); // 已關閉 socket.on('close', () => { console.log(`disconnection from ${ req.socket.remoteAddress }`); }); });
使用Node.js ws 庫:
-
當瀏覽器想要連接時,會觸發
"connection"
事件。處理函數接收一個socket
物件,用於與該個別裝置通訊。此物件必須在連接期間持續保留。 -
當瀏覽器發送訊息時,會觸發
socket "message"
事件。處理函數將訊息廣播回每個已連接的瀏覽器(包括發送訊息的瀏覽器)。 -
當瀏覽器斷開連接時,通常是在關閉或刷新標籤頁時,會觸發
socket "close"
事件。
客戶端JavaScript程式碼概覽
應用程式的static/main.js
檔案執行wsInit()
函數,並傳遞WebSocket伺服器的地址(頁面的域名加上HTML頁面模板中定義的埠值):
wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);
// 處理WebSocket通訊
function wsInit(wsServer) {
const ws = new WebSocket(wsServer);
// 連接到伺服器
ws.addEventListener('open', () => {
sendMessage('entered the chat room');
});
當瀏覽器連接到WebSocket伺服器時,open
事件觸發。處理函數通過調用sendMessage()
發送一條已進入聊天室
消息:
// 發送消息
function sendMessage(setMsg) {
let
name = dom.name.value.trim(),
msg = setMsg || dom.message.value.trim();
name && msg && ws.send( JSON.stringify({ name, msg }) );
}
sendMessage()
函數從HTML表單中獲取用戶名和消息,儘管消息可以被任何傳遞的setMsg
參數覆蓋。這些值被轉換為JSON對象,並使用ws.send()
方法發送到WebSocket伺服器。
WebSocket伺服器接收傳入的消息,觸發"message"
處理程序(如上所示)並將其廣播回所有瀏覽器。這會在每個客戶端觸發"message"
事件:
// 接收消息
ws.addEventListener('message', e => {
try {
const
chat = JSON.parse(e.data),
name = document.createElement('div'),
msg = document.createElement('div');
name.className = 'name';
name.textContent = (chat.name || 'unknown');
dom.chat.appendChild(name);
msg.className = 'msg';
msg.textContent = (chat.msg || 'said nothing');
dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });
}
catch(err) {
console.log('invalid JSON', err);
}
});
處理程序在事件對象的.data
屬性上接收傳輸的JSON數據。該函數將其解析為JavaScript對象並更新聊天窗口。
最後,每當表單的"submit"
處理程序觸發時,就會使用sendMessage()
函數發送新消息。
// 表單提交
dom.form.addEventListener('submit', e => {
e.preventDefault();
sendMessage();
dom.message.value = '';
dom.message.focus();
}, false);
錯誤處理
當WebSocket通信失敗時,會觸發"error"
事件。這可以在服務器上處理:
// 在服務器上處理WebSocket錯誤
socket.on('error', e => {
console.log('WebSocket error:', e);
});
和/或在客戶端:
// 在客戶端處理WebSocket錯誤
ws.addEventListener('error', e => {
console.log('WebSocket error:', e);
})
只有客戶端能夠通過再次運行new WebSocket()
構造函數來重新建立連接。
關閉連接
任何設備都可以隨時使用連接的.close()
方法關閉WebSocket。您可以選擇性地提供code
整數和reason
字符串(最多123字節)參數,這些參數在斷開連接之前傳輸到另一個設備。
高級WebSocket
在Node.js中管理WebSocket很容易:一個設備使用.send()
方法發送消息,這會在另一個設備上觸發"message"
事件。每個設備如何創建和響應這些消息可能更具挑戰性。以下部分描述了您可能需要考慮的問題。
WebSocket安全性
WebSocket協議不處理授權或驗證。您無法保證傳入的通信請求來自瀏覽器或登錄到您的Web應用程序的用戶——特別是當Web和WebSocket服務器可能在不同的設備上時。初始連接接收包含Cookie和服務器Origin
的HTTP標頭,但可以偽造這些值。
以下技巧確保您僅對授權用戶限制WebSocket通訊:
-
在發起初始WebSocket請求之前,瀏覽器會聯繫HTTP網頁伺服器(可能使用Ajax)。
-
伺服器檢查用戶的憑證並返回一個新的授權票證。該票證通常會引用包含用戶ID、IP地址、請求時間、會話過期時間及其他所需數據的資料庫記錄。
-
瀏覽器在初始握手時將票證傳遞給WebSocket伺服器。
-
WebSocket伺服器驗證票證並在允許連接前檢查諸如IP地址、過期時間等因素。當票證無效時,它執行WebSocket
.close()
方法。 -
WebSocket伺服器可能需要定期重新檢查資料庫記錄以確保用戶會話保持有效。
重要的是,始終驗證傳入數據:
-
與HTTP類似,WebSocket伺服器容易受到SQL注入及其他攻擊。
-
客戶端不應將原始值注入DOM或評估JavaScript代碼。
單一與多個WebSocket伺服器實例
考慮一個在線多人遊戲。遊戲中有許多宇宙運行著各自的遊戲實例:universeA
、universeB
和universeC
。玩家連接到單一宇宙:
universeA
:由player1
、player2
和player3
加入universeB
:由player99
加入
您可以實現以下方案:
-
A separate WebSocket server for each universe.
A player action in
universeA
would never be seen by those inuniverseB
. However, launching and managing separate server instances could be difficult. Would you stopuniverseC
because it has no players, or continue to manage that resource? -
為所有遊戲宇宙使用單一WebSocket伺服器。
這樣做可以節省資源且更易於管理,但WebSocket伺服器必須記錄每位玩家加入的宇宙。當
player1
執行一個動作時,必須向player2
和player3
廣播,但不包括player99
。
多個WebSocket伺服器
示例聊天應用能處理數百名同時在線用戶,但一旦受歡迎程度和記憶體使用量超過關鍵閾值,應用將崩潰。最終需透過增加更多伺服器來實現水平擴展。
每個WebSocket伺服器僅能管理其連接的客戶端。來自瀏覽器發往serverX
的消息無法廣播給連接至serverY
的用戶。可能需要實施後端發布者-訂閱者(pub-sub)消息系統。例如:
-
WebSocket
serverX
欲向所有客戶端發送消息。它在pub-sub系統上發布該消息。 -
所有訂閱pub-sub系統的WebSocket伺服器接收到新消息事件(包括
serverX
)。每個伺服器均可處理消息並將其廣播給連接的客戶端。
WebSocket消息效率
WebSocket 通訊速度快,但伺服器需管理所有連接的客戶端。在開發多人動作遊戲時,必須考慮訊息的機制與效率:
-
如何將玩家的動作同步至所有客戶端設備?
-
若
玩家1
與玩家2
位置不同,是否需要向玩家2
傳送他們看不到的動作資訊? -
如何應對網路延遲—或通訊延遲?擁有快速機器與連線的玩家是否會有不公平的優勢?
快速遊戲必須做出妥協。想像在本地設備上玩遊戲,但某些物件受到其他玩家的活動影響。與其不斷發送每個物件的精確位置,遊戲通常發送更簡單、頻率較低的訊息。例如:
物件X
出現在點X物件Y
有新的方向與速度物件Z
已被摧毀
每個客戶端遊戲填補了空白。當objectZ
爆炸時,無論每個設備上的爆炸效果是否不同,都不會有影響。
結論
Node.js使得處理WebSockets變得容易。它不一定使實時應用程序的設計或編碼變得更容易,但這項技術不會成為你的阻礙!
主要缺點:
-
WebSockets需要自己的獨立服務器實例。Ajax
Fetch()
請求和服務器推送事件可以由您已經運行的網絡服務器處理。 -
WebSocket服務器需要自己的安全和授權檢查。
-
丟失的WebSocket連接必須手動重新建立。
但不要因此而氣餒!
關於使用WebSockets和服務器推送事件的實時應用程序的常見問題(FAQs)
WebSockets在性能和功能方面與HTTP有何不同?
WebSockets 提供了一個透過單一 TCP 連接進行全雙工通信的通道,這意味著數據可以同時被發送和接收。這相較於 HTTP 是一個顯著的改進,HTTP 中每個請求都需要一個新的連接。WebSockets 還允許即時數據傳輸,使其非常適合需要即時更新的應用程序,例如聊天應用或實時體育更新。另一方面,HTTP 是無狀態的,每個請求-響應對都是獨立的,這可能更適合不需要即時更新的應用程序。
您能解釋一下 WebSocket 連接的生命週期嗎?
WebSocket 連接的生命週期始於握手,這將 HTTP 連接升級為 WebSocket 連接。一旦連接建立,數據可以在客戶端和服務器之間來回發送,直到任何一方決定關閉連接。連接可以由客戶端或服務器發送一個關閉幀來關閉,隨後另一方確認關閉幀。
如何在 Android 應用程序中實現 WebSockets?
在 Android 應用程序中實現 WebSockets 涉及創建一個可以連接到 WebSocket 服務器的 WebSocket 客戶端。這可以使用 OkHttp 或 Scarlet 等庫來完成。一旦客戶端設置好,您可以打開到服務器的連接,發送和接收消息,並處理不同的事件,如連接打開、消息接收和連接關閉。
什麼是 Server-Sent Events,它們與 WebSockets 相比如何?
Server-Sent Events (SSE) 是一項標準,允許伺服器透過 HTTP 向客戶端推送更新。與 WebSockets 不同,SSE 是單向的,意味著它們僅允許數據從伺服器傳送到客戶端。這使得它們不太適合需要雙向通訊的應用程序,但對於僅需伺服器更新通知的應用程序來說,它們可以是一個更簡單且更高效的解決方案。
WebSockets 和 Server-Sent Events 的一些常見使用案例有哪些?
WebSockets 常用於需要即時雙向通訊的應用程序,例如聊天應用、多人遊戲和協作工具。而 Server-Sent Events 則常用於需要即時接收伺服器更新的應用程序,如即時新聞更新、股票價格更新或長時間運行任務的進度報告。
在 Spring Boot 應用程序中,我如何處理 WebSocket 連接?
Spring Boot 透過 Spring WebSocket 模塊提供對 WebSocket 通訊的支持。您可以使用 @EnableWebSocket 註解來啟用 WebSocket 支持,並定義一個 WebSocketHandler 來處理連接生命週期和消息處理。您還可以使用 SimpMessagingTemplate 向已連接的客戶端發送消息。
使用 WebSockets 時有哪些安全考量?
如同其他網路技術,WebSocket也可能面臨多種安全威脅,例如跨站WebSocket劫持(CSWSH)和拒絕服務(DoS)攻擊。為降低這些風險,應始終使用安全的WebSocket連接(wss://),並驗證及清理所有傳入數據。同時,考慮使用認證和授權機制來控制對WebSocket伺服器的存取。
我可以在REST API中使用WebSocket嗎?
可以,您可以在REST API中結合使用WebSocket。雖然REST API適用於無狀態的請求-回應通信,但WebSocket可用於即時的雙向通信。這對於需要即時更新的應用程式特別有用,如聊天應用或即時體育更新。
如何測試WebSocket伺服器?
有多種工具可用於測試WebSocket伺服器,例如WebSocket.org的Echo Test或Postman。這些工具允許您打開與伺服器的WebSocket連接,發送消息並接收回應。您也可以使用Jest或Mocha等庫為您的WebSocket伺服器編寫自動化測試。
WebSocket和伺服器推送事件有哪些限制?
儘管WebSocket和伺服器推送事件為即時通信提供了強大的功能,但它們也有其限制。例如,並非所有瀏覽器和網絡都支持這些技術,且若管理不當,它們可能會消耗大量資源。此外,與傳統HTTP通信相比,它們的實施和管理可能更為複雜。
Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/