WebSocket 入門:簡易聊天室

語言: CN / TW / HK

本文首發於我的公眾號:前端西瓜哥

大家好,我是前端西瓜哥,今天我們用 WebSocket 來實現一個簡單的聊天室。

WebSocket 是一個應用層協議,有點類似 HTTP。但和 HTTP 不一樣的是,它支援真正的全雙工,即不僅客戶端可以主動發訊息給服務端,服務端也可以主動發訊息給客戶端。

尤其是後者,讓我們不用再基於 HTTP 長輪詢或短輪詢的低效方式來實現服務端通知。相比 HTTP,WebSocket 的服務端推送更輕量,並能減少服務端的壓力。

服務端

nodejs 並沒有提供原生的 WebSocket 模組。如果要實現,需要基於 net 模組,根據 WebSocket 標準去做實現。

因為實現很複雜,所以西瓜哥我選擇直接用第三庫 ws。

yarn add ws

類似 nodejs 原生的 http 等模組,ws 庫支援 WebSocket 的服務端或客戶端, 提供偏底層的 API。

我們先實現服務端程式碼:

``` import { WebSocketServer } from "ws";

// 建立一個 ws 服務 const wsSever = new WebSocketServer({   port: 6060, });

// 每當一個客戶端進行了 ws 連線,就會建立一個 ws 物件 wsSever.on("connection", (ws) => {   // 新客戶端連線時,廣播   wsSever.clients.forEach((client) => {     client.send(有人進入聊天室,當前聊天室人數:${wsSever.clients.size});   });

// 廣播任何客戶端傳送的訊息   ws.on("message", (data) => {     const msg = data.toString();     wsSever.clients.forEach((client) => {       client.send(msg);     });   });

// 當有客戶端退出時,廣播   ws.on("close", () => {     wsSever.clients.forEach((client) => {       client.send(有人退出了聊天室,當前聊天室人數:${wsSever.clients.size});     });   }); }); ```

每當一個客戶端進行了 websocket 連線,都會觸發 wsServer 的 connection 事件,然後拿到一個 ws 物件。

這個 ws 物件代表了某個客戶端和服務端的連線,我們可以通過它來接收對應客戶端的訊息,並讓服務端對指定客戶端進行主動訊息推送。

新建立的 ws 物件會在建立連線時儲存到 wsServer.clients 集合下,並在關閉連線後移除。所以我們可以利用這個 wsServer.clients 來進行廣播,實現聊天室功能。

客戶端

客戶端使用原生的 WebSocket 物件,來和服務端進行 WebSocket 連線。

``` const ws = new WebSocket('ws://localhost:6060');

ws.addEventListener('message', (event) => {   const div = document.createElement('div');   div.innerText = event.data;   document.body.append(div); })

// 點擊發送按鈕,將輸入框中的內容傳送給伺服器 const input = document.querySelector('input'); const btn = document.querySelector('button'); btn.onclick = () => {   ws.send(input.value);   input.value = ''; } ```

效果

圖片

簡易聊天室

改為使用 Socket.IO

ws 庫是偏底層的實現,比較簡單。

另一個庫 Socket.IO 的底層使用了 ws,並做功能上的增強,提供更多的能力。

相比 ws,Socket.IO 能夠做到:

  1. 如果瀏覽器不支援 WebSocket,回退為 HTTP 長輪詢方案來模擬 WebSocket( WebSocket 於 2011 年完成 RFC,已經很久了,目前來說主流瀏覽器都已經支援 WebSocket 了,還不支援 WebSocket 的瀏覽器是屑);

  2. 使用心跳包機制實現了自動重連。

  3. 包快取。斷連時傳送資料,會將資料儲存下來,等重新連線後再發送;

  4. 自定義事件支援;

  5. 廣播;

相比自己去一個個實現,使用流行的輪子可能是更好的選擇。

我們將前面的功能用 Socket.IO 實現一下。

服務端:

```js import { Server } from "socket.io";

// socket.io v3.x 開始預設不允許跨域,需要在配置顯式設定為允許跨域 const io = new Server(6060, { cors: { origin: "*" } });

io.on("connection", (socket) => {   // 新客戶端連線時,廣播   io.emit("chat", 有人進入聊天室,當前聊天室人數:${io.engine.clientsCount});

// 廣播任何客戶端傳送的訊息   socket.on("chat", (data) => {     io.emit("chat", data);   });

// 當有客戶端退出時,廣播   socket.on("disconnect", () => {     io.emit("chat", 有人退出了聊天室,當前聊天室人數:${io.engine.clientsCount});   }); }); ```

需要特別注意的是,Socket.IO 的 v3.x 版本開始,預設不允許跨域,需要在配置顯式設定為允許跨域。

客戶端:

```js const socket = io('ws://localhost:6060');

socket.on('chat', (data) => {   const div = document.createElement('div');   div.innerText = data;   document.body.append(div); })

// 點擊發送按鈕,將輸入框中的內容傳送給伺服器 const input = document.querySelector('input'); const btn = document.querySelector('button'); btn.onclick = () => {   console.log('傳送');   socket.emit('chat', input.value);   input.value = ''; } ```

Socket.IO 優點是實現了生產環境需要的底層非業務能力,讓我們能更心無旁騖地去編寫業務程式碼。

缺點是丟失了靈活性。因為做了定製化,所以需要配套使用 Socket.IO 的客戶端和服務端庫的包,某種意義脫離了網路協議標準。在出現跨語言(比如前端是 JS,後端是 Java)的場景時,需要提供對應的語言的 Socket.IO 實現。

demo

demo 已經放到 github 上了,使用方法在 README.md 中有說明。

http://github.com/F-star/websocket-chat-demo

結尾

本文演示了 WebSocket  簡易的聊天室功能是如何實現的,希望對你有所幫助。

我是前端西瓜哥,歡迎關注我,學習更多前端知識。