一文了解服務端推送(含JS程式碼示例)
阿新 • • 發佈:2020-03-20
常用的服務端推送技術,包括輪詢、長輪詢、websocket、server-sent-event(SSE)
傳統的HTTP請求是由客戶端傳送一個request,服務端返回對應response,所以當服務端想主動給客戶端傳送訊息時就遇到了問題。常見的業務場景如新訊息提醒。
## 1、輪詢(Polling)
最簡單的方法是輪詢,即客戶端不斷的傳送請求來獲取最新的訊息。優點是實現簡單。缺點是請求中有大半是無用,浪費頻寬和伺服器資源,同時,根據輪詢的時間間隔不同,獲取訊息會有對應的延遲。
例項,新浪微博新訊息提示。開啟控制檯可以發現 `https://rm.api.weibo.com/2/remind/push_count.json` 一個 `jsonp` 請求,這個請求每隔 30s 傳送一次,每次需求 100ms 左右。
![新浪微博新訊息請求](https://user-gold-cdn.xitu.io/2020/3/16/170e122161217206?w=2880&h=484&f=png&s=189040)
## 2、長輪詢(Long Polling)
長輪詢也比較容易理解,就是前端發起請求,並設定一個比較長的超時時間,後端接收到請求後,如果沒有相關資料,會hold住請求直到有結果了,或者等待一定時間超時才返回。返回後,客戶端會立即發起下一次請求。長輪詢的控制權的伺服器端,出現相關資料後會立即返回,實時性較高。
例項,QQ郵箱的新訊息提醒。可以看到 `https://wp.mail.qq.com/poll` 請求不斷髮送, 沒有新訊息時,請求每次都會需要 30s,上一次請求返回後立即傳送下一次請求,而當服務端有新訊息時會立即返回,實時性較高。
![QQ郵箱新訊息請求](https://user-gold-cdn.xitu.io/2020/3/16/170e12e6b3290559?w=2842&h=604&f=png&s=263956)
用程式碼簡單實現以上兩種輪詢
服務端程式碼
```js
const express = require('express');
const port = 2333;
const app = express();
app.get('/start', start);
app.get('/getCurrentResult', getCurrentResult);
app.get('/getFinalResult', getFinalResult);
app.listen(port, () => console.log(`Server listening on port ${port}`));
// 開始一個任務
function start(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*'); // 允許跨域
_startTask();
res.json({
code: 0,
data: '開始任務'
});
}
// 返回實時結果
function getCurrentResult(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.json({
code: 0,
data: result
});
}
// 任務執行結束之後再返回執行結果
async function getFinalResult(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
let result = await _startTask();
res.json({
code: 0,
data: result
});
}
// 模擬執行一個任務
let result = null;
function _startTask() {
result = null;
return new Promise((res, rej) => {
// 任務需要10s 10s後得到result
setTimeout(() => {
result = 'hello world';
res(result);
}, 10000);
});
}
```
客戶端程式碼
```html
輪詢&長輪詢
```
## 3、WebSocket
上面兩種方式,實際上還是客戶端單向傳送訊息,而 WebSocket 本質上解決了這個問題,WebSocket 是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。
WebSocket 握手階段採用 HTTP 協議,客戶端瀏覽器首先要向伺服器發起一個 HTTP 請求,其中附加頭資訊 `Upgrade: WebSocket` 表明這是一個申請協議升級的 HTTP 請求,伺服器端解析這些附加的頭資訊然後產生應答資訊返回給客戶端,客戶端和伺服器端的 WebSocket 連線就建立起來了,雙方就可以通過這個連線通道自由的傳遞資訊,並且這個連線會持續存在直到客戶端或者伺服器端的某一方主動的關閉連線。WebSocket 沒有同源限制。
例項,LeetCode-CN 的新訊息提醒,猜測是通過 WebSocket 實時返回是否有新訊息,再通過 XHR 請求具體資訊。
![](https://user-gold-cdn.xitu.io/2020/3/16/170e18783f56f58e?w=2490&h=600&f=png&s=150419)
簡單程式碼實現,使用了 ws 包
服務端程式碼
```js
const WebSocket = require('ws');
const http = require('http');
const port = 2333;
const server = http.createServer();
const wss = new WebSocket.Server({ server, path: '/ws' });
wss.on('connection', function(ws) {
console.log('WebSocket connection established');
let progress = 0;
ws.send(`任務進度 -- ${progress}%`);
let timer = setInterval(() => {
// 推送任務完成進度
if (++progress % 10 == 0) {
ws.send(`任務進度 -- ${progress}%`);
}
if (progress == 100) {
clearInterval(timer);
ws.close();
}
}, 200);
ws.on('close', () => {
console.log('WebSocket connection closed');
clearInterval(timer);
});
});
server.listen(port, function() {
console.log(`Server listening on port ${port}`);
});
```
客戶端程式碼
```html
WebSocket
```
## 4、Sever-Sent Event(SSE)
SSE 是一種能讓瀏覽器通過 HTTP 連線自動收到伺服器端推送的技術,EventSource 是 瀏覽器提供的對應 API。通過 EventSource 例項開啟與 HTTP 伺服器的持久連線,該伺服器以文字/事件流格式傳送事件,連線會保持開啟狀態,直到服務端或客戶端主動關閉。
與 WebSocket 區別,SSE 基於 HTTP 協議,使用簡單,SSE 預設支援斷線重連,但是 SSE 只能由服務端向客戶端推動訊息。
SSE 有四種欄位,其他的欄位會被忽略。欄位之間用`\n` 分隔,每條訊息要以 `\n\n` 結尾。
```
data // 資料項
event // 事件項 預設為 message 可設定任意值
id // 資料識別符號,用於斷線重連
retry // 斷線後重連時間
```
實際應用……最近幾天已經養成了進入一個網站就開啟控制檯看 network 的習慣……不過還是沒有找到 SSE 的實際應用……
簡單程式碼實現:
服務端程式碼
```js
const express = require('express');
const port = 2333;
const app = express();
app.get('/sse', respondSSE);
function respondSSE(req, res) {
let msg = 0;
let timer;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
res.write(sseMsg({
data: '===start===',
// 預設 event 是 'message'
}));
timer = setInterval(() => {
res.write(sseMsg({
id: Date.now(),
event: 'custom-event',
data: msg++,
retry: 2000
}));
}, 1000);
res.on('close', function () {
clearInterval(timer);
console.log('SSE connection closed');
});
}
const sseMsg = (sseObj) => {
let fields = ['id', 'event', 'data', 'retry'];
return fields
.filter(f => sseObj[f] != null)
.map(f => f + ':' + sseObj[f]).join('\n') + '\n\n';
}
app.listen(port, () => console.log(`Server listening on port ${port}`));
```
客戶端程式碼
```html
Document
```
## 5、HTTP/2 Server Push
文章比較少,而且和上面的推送並不一樣,看到這篇講得不錯~
[Node HTTP/2 Server Push 從瞭解到放棄](https://juejin.im/post/5ae3f17ef265da0ba062e7e3)
## 參考資料
- [Long Polling長輪詢詳解](https://www.jianshu.com/p/d3f66b1eb748)
- [websockets/ws](https://github.com/websockets/ws)
- [Server-Sent Events 教程](http://www.ruanyifeng.com/blog/2017/05/server-sent_event