網頁即時通訊實作 X Long-Polling, Server Sent Events, WebSockets 從把妹角度理解前後端如何和平相處

me
林彥成
2022-09-23 | 4 min.
文章目錄
  1. 1. 即時通訊原理
  2. 2. Long-Polling
  3. 3. Server Sent Events 實作
  4. 4. WebSocket
    1. 4.1. Socket.IO 簡介
    2. 4.2. MQTT

在另外一篇文章小編介紹了怎麼透過非同步 AJAX 的方式跟伺服器進行溝通,那需要即時的同步溝通怎麼辦?

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

  • Long-Polling: 長時間輪詢
  • Server Sent Events: 伺服器傳送事件
  • WebSocket: 全雙工通訊
  • Forever Frame: IE only,嵌入一個 IFrame,連向 SignalR 提供的內容

API 在系統設計上是為了溝通而產生的,而非同步溝通技術較為簡單且容易實作,即時通訊則需要較多層面的情境與技術考量。

以男女之間來說,非同步的溝通比較容易造就產生時間管理大師,即時通訊則相對較為困難。

即時通訊原理

在 2010 年 Chrome 開始支援了新的即時通訊 API 後 Web App 開始走向全新的時代,概念上從雙方通訊的方式可以分成下面三種:

  • 單工:訊號只在一個方向上進行傳遞,像是寫情書給喜歡的女生
  • 雙工: 允許雙向資料傳輸,像是曖昧期的相處
    • 半雙工:可切換方向的單工通訊,像是只有一方有意思的時候,訊息通常是單向的
    • 全雙工:現在即時通訊,即將熱戀中的男女,雙方同時接收或是傳送訊息

非同步溝通: 屬於單工或半雙工,注重的會是資訊的 “傳入” 以及 “傳出”
即時通訊: 全雙工,注重的會是 “監聽事件” 以及 “發出事件”

Long-Polling

從翻譯來看就是比較長的 Polling,用途其實是以舊的 AJAX 技術模擬即時通訊的效果。

  • Polling: 前端向後端發出請求,如果沒拿到想要的資料就重發,伺服器附載較重
  • Long-Polling:
    1. Client 發 Request 給 Server
    2. Server 送 Response 給 Client 後才斷開連線 (降低伺服器負擔但占用連線數)
    3. Client 收到後再發 Request 給 Sevrer

Polling 像是奪命連環 Call 會一直 Call 到有反應為止,就像正妹的 Line 打開永遠都是 999+ 未讀未接一樣,負擔其實很大。

Long-Polling 會是優化版本的,正妹雖然有好幾個通訊軟體,雖然打 Line 過去後被 Mute (因為切去用 Messenger),但至少可以確定之後會回。

以下為 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 事件, 伺服器在任何時候都可以向客戶端推送資料,推送進來的訊息可以在客戶端上做事件與資料的處理。

另外一種方式是使用Service Worker 可透過 PushManager 一起搭配實作離線推播,不過為獨立 Thread 無法操作 dom。

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
41
42
43
44
// 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);
};

events.addEventListener("ping", function (event) {
if (event.data) console.log(event.data);
});

WebSocket

WebSocket 這個 API 在不必 polling 伺服器的情況下,讓用戶傳送訊息至伺服器並接受事件驅動回應,達到即時通訊的效果。

接近真的談戀愛的溝通,雙方各自在沒什麼負擔的情況下進行訊息的交流。

Socket.IO 簡介

Socket.IO 屬於 node.js 解決方案,封裝了 Long-Polling 及 WebSocket,是一個 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 = "";
}
});

MQTT

MQTT (Message Queuing Telemetry Transport) 適合輕量級物聯網使用,封包較小可以支援大量的 client。

主要是基於 subscribe 跟 publish 兩個概念的協定,為了硬體效能低下的遠端裝置以及網路狀況糟糕的情況下而設計。

主要是以 TCP/IP 協定上去優化且取代 HTTP 這種較肥的資料傳輸協定,因此會需要一個訊息中介軟體 (MQTT Broker) 來提供輕量化的解決方案。

底下官方範例會是使用官方提供的 MQTT Broker mqtt://test.mosquitto.org,若為自己的服務需要自行架設。

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
<html>
<head>
<title>test Ws mqtt.js</title>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
</head>
<body>
<script>
const mqtt = require("mqtt");
const client = mqtt.connect("mqtt://test.mosquitto.org");

client.on("connect", function () {
client.subscribe("presence", function (err) {
if (!err) {
client.publish("presence", "Hello mqtt");
}
});
});

client.on("message", function (topic, message) {
// message is Buffer
console.log(message.toString());
client.end();
});
</script>
</body>
</html>

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


share