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

大家好,我是前端西瓜哥,今天我們用 WebSocket 來實現一個簡單的聊天室。
WebSocket 是一個應用層協議,有點類似 HTTP。但和 HTTP 不一樣的是,它支持真正的全雙工,即不僅客戶端可以主動發消息給服務端,服務端也可以主動發消息給客戶端。
尤其是后者,讓我們不用再基于 HTTP 長輪詢或短輪詢的低效方式來實現服務端通知。相比 HTTP,WebSocket 的服務端推送更輕量,并能減少服務端的壓力。
服務端
nodejs 并沒有提供原生的 WebSocket 模塊。如果要實現,需要基于 net 模塊,根據 WebSocket 標準去做實現。
因為實現很復雜,所以西瓜哥我選擇直接用第三庫 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 能夠做到:
- 如果瀏覽器不支持 WebSocket,回退為 HTTP 長輪詢方案來模擬 WebSocket( WebSocket 于 2011 年完成 RFC,已經很久了,目前來說主流瀏覽器都已經支持 WebSocket 了,還不支持 WebSocket 的瀏覽器是屑)。
- 使用心跳包機制實現了自動重連。
- 包緩存。斷連時發送數據,會將數據保存下來,等重新連接后再發送。
- 自定義事件支持。
- 廣播。
相比自己去一個個實現,使用流行的輪子可能是更好的選擇。
我們將前面的功能用 Socket.IO 實現一下。
服務端:
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 版本開始,默認不允許跨域,需要在配置顯式設置為允許跨域。
客戶端:
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 中有說明。
https://github.com/F-star/websocket-chat-demo
結尾
本文演示了 WebSocket 簡易的聊天室功能是如何實現的,希望對你有所幫助。