我相信大家對 WebSocket 應該都不陌生,即使沒使用過,也能在網路上找到許多教學文章。因此今天想和大家分享的是,身為前端開發者,如果想要測試或深入了解 WebSocket 的運作方式,可以有哪些方法?其中一個方法就是:讓我們來親自架設 WebSocket Server 吧。
常用的是 Heroku,他可以方便部署 Node.js 應用程式,而且也是免費的,要拿來測試用也很方便。不過這次我想偷懶一下,改用 Glitch 這個有線上編輯器的部署平台,方便我快速建立 Node.js 應用程式,以下會用 Glitch 網站做範例介紹。
登入後選擇「New project」,他有幾個快速建立 app 的方案,我選的是 glitch-hello-node,基於 Node.js 和 Fastify 的應用程式,建立完後,就能開啟懶人模式,直接線上編輯啦 XXD。
首先打開 Terminal,安裝 ws 套件
npm install ws
在 server.js
設定 Fastify 伺服器,並整合 ws 庫,我還設了一個 Interval
讓他每一秒鐘發送訊息到前端頁面:
const fastify = require('fastify')({ logger: true });
const path = require('path');
// 配置 Fastify 靜態文件服務
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/',
});
// 設定 HTTP 路由(如果需要,可以顯示首頁)
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
// 啟動 HTTP 伺服器
const start = async () => {
try {
const server = await fastify.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
console.log(`Server listening on ${server}`);
// 啟動 WebSocket 伺服器
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: fastify.server });
wss.on('connection', (ws) => {
console.log('New client connected!');
const interval = setInterval(() => {
ws.send('Hello from the server!');
}, 1000);
ws.on('message', (message) => {
console.log(`Received: ${message}`);
ws.send(`You sent: ${message}`);
});
ws.on('close', () => {
clearInterval(interval);
console.log('Client has disconnected.');
});
});
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
然後在前端建立 wss 連線,這邊要注意的是我前端的連線是 HTTPS,所以 WebSocket 的協議要改用 wss://
而非 ws://
,避免產生 Mix Content 的問題。
const socket = new WebSocket('wss://xxx.glitch.me/');
socket.onopen = () => {
console.log('Connected to the WebSocket server');
};
socket.onmessage = (event) => {
const messagesDiv = document.getElementById('messages');
const newMessage = document.createElement('p');
newMessage.textContent = `Received: ${event.data}`;
messagesDiv.appendChild(newMessage);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
關於 glitch 的 WebSocket url 可以從右上角的「Share」按鈕取得 https 的對外連結,再將 https
改為 wss
就是你的 WebSocket Server Url。
打開網站後,可以從 console 面板看到 WebSocket 的連線狀況,建立連線後,WebSocket Server 就會使用 ws.send()
傳送訊息到前端,前端接受後會把訊息顯示在畫面上:
WebSocket 是一個單向通訊的服務,也就是當連線啟動時,我們前端只能被動的接收來自 WebSocket Server 的通知,但這也導致一個問題,就是我們可能不知道當沒收到通知時,究竟是網路問題?還是 WebSocket 本身的錯誤?這邊舉幾個 WebSocket 的特性來讓大家更了解發生的原因:
WebSocket 的連線狀態是隨時間動態變化的,我們只能檢查當下的連線狀態,例如我們在特定的時間點用 readyState
檢查連線狀態為 OPEN
時,我們也沒辦法保證檢查的下一分鐘、下一秒的連線狀態還是 OPEN
,這就會造成「無法即時掌握連線狀態」的問題。
此外,WebSocket 的連線是依賴於底層網路,而網路問題 (如封包遺失、延遲) 可能導致連線中斷或狀態異常。但瀏覽器或伺服器可能不會立即察覺到連線中斷,所以導致我們在檢查連線狀態時為 OPEN
,但實際上資料傳輸已經中斷了。
雖然前端只要不主動關閉連線,WebSocket 就會是持久連線,但 WebSocket 缺乏雙向的 ping 機制來主動檢查連線。伺服器雖然可以透過發送 ping
來檢查連線,但這依然是單向的,如果前端沒有實作類似的功能 (例如定期 ping
),那麼當連線中斷時,雙方可能都不會立即察覺,在這種情況下,就可能等到下一次嘗試發送訊息或進行操作時,才會發現連線已經中斷。
透過前面的段落,可以發現幾個解決方案,例如設置定時器,定時檢查 readyState
的連線狀態;在 onerror
和 onclose
事件中做一些處理邏輯,以便在連線異常時能立即應對;此外也能設定自動重連機制 ... 等等。這幾個解決方案,都是前端就能掌握的,相信大家有做過 WebSocket 都會做這些檢查,這邊就不贅述了。
而接下來要分享的 ping
機制,跟定時器很像,也是要定期去 ping
來檢查連線,ping
機制主要是寫在後端,用來確保前後端的 WebSocket 連線狀態。
ping
機制的實作原理為:Server 會定期發送 ping
給所有連線的 Client,如果 Client 有回應,就知道連線正常。如果在一定時間內沒有收到 ping
的回應,Server 就可以當作連線已經斷掉了,此時就能主動關閉連線。
我們來修改一下後端的程式碼,將這個 ws
套件的 ping()
方法加進去:
const fastify = require('fastify')({ logger: true });
const path = require('path');
fastify.register(require('@fastify/static'), {
root: path.join(__dirname, 'public'),
prefix: '/',
});
fastify.get('/', async (request, reply) => {
return { hello: 'world' };
});
const start = async () => {
try {
const server = await fastify.listen({ port: process.env.PORT || 3000, host: '0.0.0.0' });
console.log(`Server listening on ${server}`);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: fastify.server });
wss.on('connection', (ws) => {
console.log('New client connected!');
// 定期發送 ping
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
console.log('Ping sent');
}
}, 30000);
const interval = setInterval(() => {
ws.send('Hello from the server!');
}, 1000);
// 當收到 Client 端的 pong 時
ws.on('pong', () => {
console.log('Pong received');
});
ws.on('message', (message) => {
console.log(`Received: ${message}`);
ws.send(`You sent: ${message}`);
});
ws.on('close', () => {
clearInterval(interval);
// 停止 ping
clearInterval(pingInterval);
console.log('Client has disconnected.');
});
});
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();
ping()
和 pong()
是用來檢查底層網路層的連線,不是用來傳遞消息的,所以不會在 Client 端的 message
事件中接收到,而會自動處理。也就是說,Client 端會自動回應 Server 端的 ping
消息,並發送一個 pong
,但不會觸發 message
事件。
所以我們要看後端的 Log 來確認這個連線狀態,以 Glitch 為例,打開 Log 面板後,就能看到對應的訊息了
範例程式碼網址:https://mukiwu.github.io/web-api-demo/websocket.html
以上,有任何問題歡迎留言討論唷。
pint
和 pong
都是是自動處理的,那請問一段時間內沒收到 pong
後會斷開連線也是自動處理的嗎?🤔
是的,一段時間內沒收到 pong
之後自動斷開連線的處理,是由底層的 WebSocket 或其他網絡協議自動處理的。我們通常不需要手動去設定這些過程。