網頁即時通訊實作 Polling, Long-Polling, Server Sent Events, WebSocket

me
林彥成
2019-07-04 | 2 min.
文章目錄
  1. 1. 前端解決方案
  2. 2. Long-Polling 實作
  3. 3. Server Sent Events 實作
  4. 4. Socket.IO 簡介

這篇文章會介紹以下幾種可以做到即時網頁通訊服務的技術:

  • Polling: 前端向 Server 發出 Request,如果沒拿到想要的資料,就再重發,伺服器附載較重。
  • Long-Polling: Client 發 Request 給 Server,Server 送 Response 給 Client 後才斷開連線,Client 收到,再發 Request 給 Sevrer,占用連線數。
  • Server Sent Events: 由瀏覽器幫我們處理,前端實作上只要監聽相關的事件就可以了。
  • WebSocket: 解決了單向請求的問題,但很多情況不一定需要用到雙向通訊,從上面的特性看起來,伺服器也因為使用的情境不同,最佳化時會有差異。
  • Forever Frame: IE only,嵌入一個 IFrame,連向 SignalR 提供的內容。

概念上從雙方通訊的方式可以分成下面三種:

  • 單工:訊號只在一個方向上進行傳遞,像是郵差寄信。
  • 半雙工:可以切換方向的單工通訊,像是小時候玩的對講機。
  • 全雙工:現在的即時通訊,雙方都可以同時接收或是傳送訊息。

前端解決方案

在網頁端提供支援較也常用的解決方案有以下四種:

  • SignalR: 微軟的解決方案,封裝了所有的即時網頁技術,包含支援 IE。
  • Socket.IO: node.js 解決方案,封裝了 polling 及 websocket。
  • MQTT: 適合輕量級物聯網使用,封包較小可以支援大量的 client。
  • Service Worker: 離線推播,獨立 Thread 無法操作 dom,透過 PushManager 可以使用推播。

Long-Polling 實作

以下為 Long-Polling 實作範例程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let timeout;

function valueChanged(value) {
return (dispatch) => {
dispatch(loadSuggestionsInProgress());
dispatch({
type: "VALUE_CHANGED",
payload: {
value,
},
});

// 一秒內有值改變就清除且在重設 timeout
timeout && clearTimeout(timeout);

// 一秒後再打一次
timeout = setTimeout(
() => {
axios
.get(`/suggestions?q=${value}`)
.then((response) =>
dispatch(loadSuggestionsSuccess(response.data.suggestions))
)
.catch(() => dispatch(loadSuggestionsFailed()));
},
1000,
value
);
};
}

Server Sent Events 實作

以下為 Server Sent Events 實作範例程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Server
let clients = [];
const server = express();

function sendEventsToClients() {
clients.forEach((c) => {
c.res.write(`change`);
});
}

async function modify(req, res) {
res.json({ status: 1 });
return sendEventsToClients();
}

server.get("/modify", modify);
server.get("/listen", (req, res) => {
const { clientId } = req.query;
const headers = {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"Cache-Control": "no-cache",
};
res.writeHead(200, headers);
res.write("");
const newClient = {
id: clientId,
res,
};
clients.push(newClient);
req.on("close", () => {
clients = clients.filter((c) => c.id !== clientId);
});
});

// client
const events = new EventSource("/listen?clientId=test");
events.onmessage = (event) => {
if (event.data) console.log(event.data);
};

Socket.IO 簡介

是一個 event-based 全雙工的通訊函式庫,事件驅動這個部分是最容易出包的地方,當我們在和 react 專案整合在一起的時候,就需要去注意事件是不是有和畫面的渲染綁在一起,意思就是說不可以每渲染一次,我就重新建立連結、重新監聽事件、重新發出訊息,這樣一來記憶體很快就會用完,處理器也根本來不及處理。

一個後端的基本範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const { Server } = require("socket.io");
const io = new Server(server);

app.get("/", (req, res) => {
res.sendFile(__dirname + "/index.html");
});

io.on("connection", (socket) => {
console.log("a user connected");
socket.on("message", (msg) => {
console.log("message: " + msg);
});
});

server.listen(3000, () => {
console.log("listening on *:3000");
});

前端

1
2
3
4
5
6
7
8
9
10
11
12
var socket = io();

var form = document.getElementById("form");
var input = document.getElementById("input");

form.addEventListener("submit", function (e) {
e.preventDefault();
if (input.value) {
socket.emit("message", input.value);
input.value = "";
}
});

喜歡這篇文章,請幫忙拍拍手喔 🤣

share