瀏覽器與服務器長連接技術


瀏覽器和服務器保持持久連接的手段。

定時器

最簡單,使用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格式推送消息。
約束:

  1. 只能由服務器向瀏覽器推送數據,瀏覽器不能主動向服務器發送數據
  2. 推送的數據只能是文本

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:

  1. 它沒有同源策略的限制
  2. 瀏覽器對它支持很好,IE10開始也支持了
  3. 既可以發送文本也可以發送二進制
  4. 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);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM