基於 HTTP 的實時 Web 通信
Web 應用的信息交互過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收和審覈完請求後進行處理並返回結果給客戶端,然後客戶端瀏覽器將信息呈現出來,這種機制對於信息變化不是特別頻繁的應用是能夠滿足的,但是對於那些實時要求比較高的應用來說,當客戶端瀏覽器準備呈現這些信息的時候,這些信息在服務器端可能已經過時了。所以保持客戶端和服務器端的信息同步是實時 Web 應用的關鍵要素,本文將簡要介紹三種基於 HTTP 的實時 Web 通信方案。
短輪詢 (Short Polling)
短輪詢是最早的一種實現實時 Web 應用的方案。客戶端以一定的時間間隔向服務端發出請求,以頻繁請求的方式來保持客戶端和服務器端的同步。這種同步方案的最大問題是,當客戶端以固定頻率向服務器發起請求的時候,服務器端的數據可能並沒有更新,這樣會帶來很多無用的網絡傳輸,所以這是一種非常低效的實時方案。
短輪詢的優點:
- 客戶端和服務器端實現都比較簡單
短輪詢的缺點:
- 請求間隔小,則頻繁請求,無用請求多,浪費網絡帶寬和服務器資源
- 請求間隔大,則信息時效性一般
客戶端短輪詢示例:
function poll() {
axios.get('/api/shortPolling')
.then(response => {
// do something ...
// 3秒後再發請求
setTimeout(() => {
poll();
}, 3000);
})
.catch(error => {
// error
});
}
長輪詢 (Long Polling)
長輪詢是對短輪詢的改進和提高,目地是爲了降低無效的網絡傳輸。當服務器端沒有數據更新的時候,連接會保持一段時間直到數據或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和服務器間的交互。當然,如果服務端的數據變更非常頻繁的話,這種機制和短輪詢比較起來沒有本質上的性能的提高。
長輪詢的優點:
- 有數據更新時,能比較及時的響應
- 沒有新數據更新時,服務端會保持連接直到超時,請求相對少,節省網絡帶寬
長輪詢的缺點:
- 服務器端需要保持 HTTP 連接,消耗一定的資源
- 服務器端數據變更頻繁時,請求也會變得頻繁
客戶端長輪詢示例:
function poll() {
axios.get('/api/longPolling')
.then(response => {
// do something ...
poll();
})
.catch(error => {
// error
});
}
服務器發送事件 (Server-Sent Events)
服務器發送事件(SSE)是客戶端通過 HTTP 連接自動從服務器接收更新的技術。
服務器端要按照規定的格式返回響應內容,響應的內容類型爲“text/event-stream”。響應文本的內容可以看成是一個事件流,由不同的事件所組成。每個事件由事件類型(event)和數據(data)兩部分組成,同時每個事件可以有一個可選的標識符(id)。不同事件的內容之間通過一個空行來分隔,每個事件的數據可能由多行組成。客戶端不再是使用瀏覽器提供的 XMLHttpRequest
對象來實現,而是使用 EventSource
對象實現,通過該對象建立連接和接收處理服務器推送的事件。
SSE 的優點:
- 實時性較好
- 除了超時的情況,客戶端一般只要發一次請求
SSE 的缺點:
- 服務器端需要保持 HTTP 連接,消耗一定的資源
- IE 和 Edge 瀏覽器不支持
客戶端創建一個連接到服務器接收事件,示例如下:
// 實例化 EventSource 對象,並指定一個 URL 地址
const eventSource = new EventSource('/api/serverSentEvents');
// 使用 addEventListener() 方法監聽事件
eventSource.addEventListener('message', (e) => {
// 消息數據
const data = e.data;
// do something ...
}, false);
事件流格式
事件流僅僅是一個簡單的文本數據流,每條消息後面都由一個空行作爲分隔符,以冒號開頭的行是註釋行。服務器可以定期發送一條註釋行消息,以保持連接防止連接超時。
: 這是註釋行
data: 一條沒有命名事件的消息數據
data: 一條多行
data: 消息數據
: 下面是事件名,瀏覽器在接收時,會產生對應事件名的事件
event: update
data: {"name": "harbor", "comment": "這是 JSON 格式的消息"}
除了上面看到的 event
和 data
字段,還有 id
和 retry
字段。
如果服務器設置了 id
,客戶端會保持最近的事件 ID。如果連接中斷了,重新連接發起新的請求時,會把最近接收到的事件 ID 放入 “Last-Event-ID” 請求的首部中,從而告知服務器最近的事件 ID。
: 下面是事件 ID
id: 8888
data: 一條消息數據
默認情況下,客戶端會在連接斷開後3秒嘗試進行重新連接。這個時間也可以由服務器來指定,以毫秒爲單位。
: 指定重新連接時間,單位毫秒
retry: 6000
Node.js 服務器端的例子
服務器端使用 Node.js 實現 SSE 的簡單示例:
const http = require('http');
http.createServer((request, response) => {
const filePath = request.url;
if (filePath === '/api/serverSentEvents') {
// Content-Type 首部指定爲"text/event-stream"
response.writeHead(200, {
'Content-Type': "text/event-stream",
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// 簡單模擬服務器檢查更新,每隔5秒發送一條消息
setInterval(() => {
const data = { timeStamp: Date.now() };
// 返回一條簡單的消息,JSON 格式
response.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);
} else {
response.writeHead(404);
response.end();
}
}).listen(8080);