瀏覽器和服務器保持持久連接的手段。
定時器
最簡單,使用setTimeout、setInterval或其他計時手段定期向服務器發送請求,此方法優點就是簡單,缺點就是不靈活,容易造成大量沒有意義的請求。
長輪詢
瀏覽器向服務器發出一個請求,服務器收到請求並將這個請求掛起(pending),當服務器需要向瀏覽器發送數據了,就響應掛起的這個請求,瀏覽器收到響應之后立刻再發送一個請求,服務器再把它掛起,如此反復,即實現了最簡單的長輪詢機制,它不需要任何新的協議。
適合B/S不頻繁的通信,因為即便是很小的數據量,也要重新發送一個完整的http請求。
瀏覽器端代碼:
function validHttpStatus(){
return arguments[0] > 199 && arguments[0] < 300;
}
async function longPolling(){
let response = await fetch("http://localhost:3000/getdata");
if (!validHttpStatus(response.status)) {
// 發生了錯誤,打印一下錯誤
console.error(`${response.url}: ${response.statusText}`);
setTimeout(() => { // 過一會再試
longPolling();
}, 1e3);
}else{
// 打印出服務器返回的數據
let data = await response.text();
console.info(data);
// 立刻再次調用,保持連接一直處於打開狀態
longPolling();
}
}
longPolling(); // 開始長輪詢
服務器端代碼:
// 使用了Koa
function delay(seconds){
return new Promise(ok=>setTimeout(ok, 1e3*seconds));
}
router.get('/getdata', async(ctx, next)=>{
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Content-Type', 'text/plain; charset=utf-8');
ctx.set("Cache-Control", "no-store"); // 禁用緩存
await delay(Math.floor(Math.random()*10) + 1); // 模擬服務器突然向瀏覽器響應數據
ctx.body = 'hi ' + (new Date);
await next();
});
Server Sent Event
規范文檔: https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface
瀏覽器內建的EventSource構造函數能創建一個對應的實例,只有IE全系列不支持。
支持SSE的服務器使用text/event-stream
格式推送消息。
約束:
- 只能由服務器向瀏覽器推送數據,瀏覽器不能主動向服務器發送數據
- 推送的數據只能是文本
SSE使用的也是http協議,它可以自動重連,而websocket需要我們手動處理重連,對於單向的且數據量不多的情景可以使用SSE,沒必要強行使用websocket。
瀏覽器端代碼:
function start(){
var eventSource = new EventSource('http://localhost:3000/getdata');
eventSource.onmessage = function(e){ // 或addEventListener
console.log('a new msg here:', e.data);
};
eventSource.addEventListener('goodbye', function(e){
// 對於自定義事件,不能使用onxxxx,必須是addEventListener
console.log('finial message:', e.data);
});
setTimeout(() => { // 一小時后自動關閉
// 一旦一個EventSource實例被關閉,就無法再復用它了,必須再新建一個實例
eventSource.close();
}, 1e3*60*60);
}
// start(); // 啟動
服務器端代碼:
var http = require('http');
var count = 0;
http.createServer(function(req, res){
res.setHeader('Access-Control-Allow-Origin', '*');
if (req.url.includes('getdata')){
if (count++ == 2){ // 2次之后不讓瀏覽器繼續連接了
count = 0; // 重置
res.statusCode = 204; // 規范約定了204是告訴瀏覽器不要重試了,服務器關閉連接了,204狀態碼本身表示無內容,No-Content
res.end();
return;
}
res.statusCode = 200;
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
let id = setInterval(() => {
// 每條消息以雙LF分隔,每條消息還有event、retry和id字段
// event: 表示本消息的事件名,瀏覽器需要對它進行addEventListener
// retry: 告訴瀏覽器重試等待事件,單位毫秒,默認3000
// id:本消息的ID,重試時瀏覽器會發送最后一個接收到的ID以告訴服務器從哪繼續開始重傳,就像TCP的ack確認號
res.write(`data: hi ${new Date}\n\n`);
}, (Math.floor(Math.random()*2) + 1)*1e3);
setTimeout(() => {
clearInterval(id);
// 本次消息周期完成,然后瀏覽器將嘗試自動重連
res.end(`event: goodbye\ndata: see next time\n\n`);
}, 1e3*4);
}else{
res.end();
}
}).listen(3000);
WebSocket
是瀏覽器和服務器全雙工通信的解決方案,通信不基於http(websocket握手還是采用http),而是使用自己的ws協議,以及TSL加密的wss協議。
當瀏覽器請求建立websocket連接時,發送的http請求有2個重要字段:(不能使用XHR或fetch來模擬websocket的握手,因為JavaScript無法設置這些請求頭)
GET /getdata
Connection: Upgrade // 表示瀏覽器需要改變(升級)協議
Upgrade: websocket // 改變為websocket
如果服務器支持websocket,就判斷來源並同意是否升級,如果同意返回如下響應:
101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
握手完成了,之后就是用ws的數據幀開始通信了。
強大的websocket:
- 它沒有同源策略的限制
- 瀏覽器對它支持很好,IE10開始也支持了
- 既可以發送文本也可以發送二進制
- 3個方法4個事件
方法:
socket.send(data)
socket.close([code], [reason])
事件:open
message
error
close
瀏覽器端代碼:
function start(){
var id;
let socket = new WebSocket('ws://localhost:3000/getdata'); // 注意是ws://
socket.binaryType = 'arraybuffer'; // 默認是'blob',即把接收到的二進制當作blob,blob是有類型的二進制數據塊,作為高層的二進制數據存在,可以直接供<a>、<img>等標簽使用,而arraybuffer提供了細顆粒的二進制操作
socket.onopen = function(e){
console.log("opened");
socket.send('hi'); // 發送文本
// id = setInterval(() => {
// socket.send(new Uint8Array([1,2,3,4])); // 發送二進制,可以是ArrayBuffer或Blob
// }, 2000);
};
socket.onmessage = function(e){
console.log('a msg here:', e.data);
};
socket.onclose = function(e){
console.log(`closed, code=${e.code}, reason=${e.reason}`);
// clearInterval(id);
// 如果返回的code是1006,表示對方被異常關閉,比如進程被殺死了,而這個狀態碼是無法通過代碼設置的
};
}
// start(); // 啟動
服務器端代碼:
const http = require('http');
const ws = require('ws');
const wsinstance = new ws.Server({noServer: true});
http.createServer(function(req, res){
// 只接受websocket
if (!req.headers.upgrade || req.headers.upgrade.toLowerCase() != 'websocket') {
res.end();
return;
}
// Connection: keep-alive, Upgrade
if (!/upgrade/i.test(req.headers.connection)) {
res.end();
return;
}
// 進行協議升級
wsinstance.handleUpgrade(req, req.socket, Buffer.allocUnsafe(0), function(ws){
ws.on('message', function(data){
console.log('receive data from browser:', data);
// ws.send(`now ${new Date}!`); // 發送文本
ws.send(new Uint8Array([5,6,7,8])); // 發送二進制數據
setTimeout(() => ws.close(1000, "Bye!"), 5000);
})
});
}).listen(3000);