如何在 Node.js 中使用 WebSockets 創建即時應用

本教程展示如何在Node.js中使用WebSocket实现浏览器与服务器之间的双向交互通信。这一技术对于快速、实时的应用程序至关重要,例如仪表板、聊天应用和多人游戏。

Table of Contents

網絡基於請求-響應的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支援已包含於DenoBunJavaScript執行環境中。

WebSocket 函式庫適用於多種執行環境,包括 PHPPythonRuby。此外,第三方 SaaS 解決方案如 PusherPubNub 也提供託管的 WebSocket 服務。

WebSocket 示範快速入門

聊天應用程式是 WebSocket 示範的 Hello, World!,因此我為以下事項道歉:

  1. 缺乏原創性。儘管如此,聊天應用程式是解釋這些概念的絕佳方式。

  2. 無法提供完全託管的在線解決方案。我寧願不必監控和審查一連串匿名訊息!

複製或下載 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入口檔案啟動了兩個伺服器:

  1. 一個在http://localhost:3000/運行的Express應用,使用EJS模板提供一個包含客戶端HTML、CSS和JavaScript的單頁面。瀏覽器JavaScript使用WebSocket API進行初始連接,然後發送和接收消息。

  2. 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通訊:

  1. 在發起初始WebSocket請求之前,瀏覽器會聯繫HTTP網頁伺服器(可能使用Ajax)。

  2. 伺服器檢查用戶的憑證並返回一個新的授權票證。該票證通常會引用包含用戶ID、IP地址、請求時間、會話過期時間及其他所需數據的資料庫記錄。

  3. 瀏覽器在初始握手時將票證傳遞給WebSocket伺服器。

  4. WebSocket伺服器驗證票證並在允許連接前檢查諸如IP地址、過期時間等因素。當票證無效時,它執行WebSocket .close()方法。

  5. WebSocket伺服器可能需要定期重新檢查資料庫記錄以確保用戶會話保持有效。

重要的是,始終驗證傳入數據:

  • 與HTTP類似,WebSocket伺服器容易受到SQL注入及其他攻擊。

  • 客戶端不應將原始值注入DOM或評估JavaScript代碼。

單一與多個WebSocket伺服器實例

考慮一個在線多人遊戲。遊戲中有許多宇宙運行著各自的遊戲實例:universeAuniverseBuniverseC。玩家連接到單一宇宙:

  • universeA:由player1player2player3加入
  • universeB:由player99加入

您可以實現以下方案:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. 為所有遊戲宇宙使用單一WebSocket伺服器。

    這樣做可以節省資源且更易於管理,但WebSocket伺服器必須記錄每位玩家加入的宇宙。當player1執行一個動作時,必須向player2player3廣播,但不包括player99

多個WebSocket伺服器

示例聊天應用能處理數百名同時在線用戶,但一旦受歡迎程度和記憶體使用量超過關鍵閾值,應用將崩潰。最終需透過增加更多伺服器來實現水平擴展

每個WebSocket伺服器僅能管理其連接的客戶端。來自瀏覽器發往serverX的消息無法廣播給連接至serverY的用戶。可能需要實施後端發布者-訂閱者(pub-sub)消息系統。例如:

  1. WebSocket serverX欲向所有客戶端發送消息。它在pub-sub系統上發布該消息。

  2. 所有訂閱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/