實時消息推送整理

分不清輪詢、長輪詢?不知道何時該用websocket仍是SSE,看這篇就夠了。javascript

所謂的「實時推送」,從表面意思上來看是,客戶端訂閱的內容在發生改變時,服務器可以實時地通知客戶端,進而客戶端進行相應地反應。客戶端不須要主觀地發送請求去獲取本身關心的內容,而是由服務器端進行「推送」。html

注意上面的推送二字打了引號,這就意味着在現有的幾種實現方式中,並非服務器端主動地推送,而是經過必定的手段營造了一種實時的假象。就目前現有的幾種技術而言,主要有如下幾類:java

  • 客戶端輪詢:傳統意義上的輪詢(Short Polling)
  • 服務器端輪詢:長輪詢(Long Polling)
  • 全雙工通訊:Websocket
  • 單向服務器推送:Server-Sent Events(SSE)

文中會以一個簡易聊天室的例子來分別經過上述的四種方式實現,代碼地址mini-chatroom(存在些許bug,主要是爲了作演示用)git

overview

輪詢(Short Polling)

輪詢的實現原理:客戶端向服務器端發送一個請求,服務器返回數據,而後客戶端根據服務器端返回的數據進行處理;而後客戶端繼續向服務器端發送請求,繼續重複以上的步驟,若是不想給服務器端太大的壓力,通常狀況下會設置一個請求的時間間隔。github

shortPolling

使用輪詢明顯的優勢是基礎不須要額外的開發成本,請求數據,解析數據,做出響應,僅此而已,而後不斷重複。缺點也顯而易見:web

  • 不斷的發送和關閉請求,對服務器的壓力會比較大,由於自己開啓Http鏈接就是一件比較耗資源的事情
  • 輪詢的時間間隔很差控制。若是要求的實時性比較高,顯然使用短輪詢會有明顯的短板,若是設置interval的間隔過長,會致使消息延遲,而若是過短,會對服務器產生壓力

代碼實現

var ShortPollingNotification = {
  datasInterval: null,
  subscribe: function() {
    this.datasInterval = setInterval(function() {
      Request.getDatas().then(function(res) {
        window.ChatroomDOM.renderData(res);
      });
    }, TIMEOUT);
    return this.unsubscribe;
  },
  unsubscribe: function() {
    this.datasInterval && clearInterval(this.datasInterval);
  }
}
複製代碼

shortPolling

下面是對應的請求,注意左下角的請求數量一直在變化express

shortNetwork

在上圖中,每隔1s就會發送一個請求,看起來效果還不錯,可是若是將timeout的值設置成5s,效果將大打折扣,如圖:npm

shortPolling5s

長輪詢(Long Polling)

長輪詢的基本原理:客戶端發送一個請求,服務器會hold住這個請求,直到監聽的內容有改變,纔會返回數據,斷開鏈接,客戶端繼續發送請求,重複以上步驟。或者在必定的時間內,請求還得不到返回,就會由於超時自動斷開鏈接。json

longPolling

長輪詢是基於輪詢上的改進版本,主要是減小了客戶端發起Http鏈接的開銷,改爲了在服務器端主動地去判斷所關心的內容是否變化,因此其實輪詢的本質並無多大變化,變化的點在於:api

  • 對於內容變化的輪詢由客戶端改爲了服務器端(客戶端會在鏈接中斷以後,會再次發送請求,對比短輪詢來講,大大減小了發起鏈接的次數)
  • 客戶端只會在數據改變時去做相應的改變,對比短輪詢來講,並非全盤接收

代碼實現

// 客戶端
var LongPollingNotification = {
    // ....
    subscribe: function() {
      var that = this;

      // 設置超時時間
      Request.getV2Datas(this.getKey(),{ timeout: 10000 }).then(function(res) {
        var data = res.data;
        window.ChatroomDOM.renderData(res);
        // 成功獲取數據後會再次發送請求
        that.subscribe();
      }).catch(function (error) {
        // timeout 以後也會再次發送請求
        that.subscribe();
      });
      return this.unsubscribe;
    }

    // ....
}
複製代碼

筆者採用的是express,默認不支持hold住請求,所以用了一個express-longpoll的庫來實現。

下面是一個原生不用庫的實現(這裏只是介紹原理),總體的思路是:若是服務器端支持hold住請求的話,那麼在必定的時間內會自輪詢,而後期間經過比較key值,判斷是否返回新數據

  • 客戶端第一次會帶一個空的key值,此次會當即返回,獲取新內容,服務器端將計算出的contentKey返回給客戶端
  • 而後客戶端發送第二次請求,帶上第一次返回的contentKey做爲key值,而後進行下一輪的比較
  • 若是兩次的key值相同,就會hold請求,進行內部輪詢,若是期間有新內容或者客戶端timeout,就會斷開鏈接
  • 重複以上步驟
// 服務器端

router.get('/v2/datas', function(req, res) {
  const key = _.get(req.query, 'key', '');
  let contentKey = chatRoom.getContentKey();

  while (key === contentKey) {
    sleep.sleep(5);
    contentKey = chatRoom.getContentKey();
  }

  const connectors = chatRoom.getConnectors();
  const messages = chatRoom.getMessages();
  res.json({
    code: 200,
    data: { connectors: connectors, messages: messages, key: contentKey },
  });
});
複製代碼

如下是用 express-longpoll 的實現片斷

// mini-chatroom/public/javascripts/server/longPolling.js

function pushDataToClient(key, longpoll) {
  var contentKey = chatRoom.getContentKey();

  if (key !== contentKey) {
    var connectors = chatRoom.getConnectors();
    var messages = chatRoom.getMessages();

    longpoll.publish(
      '/v2/datas',
      {
        code: 200,
        data: {connectors: connectors, messages: messages, key: contentKey},
      }
    );
  }
}

longpoll.create("/v2/datas", function(req, res, next) {
  key = _.get(req.query, 'key', '');
  pushDataToClient(key, longpoll);
  next();
});

intervalId = setInterval(function() {
  pushDataToClient(key, longpoll);
}, LONG_POLLING_TIMEOUT);
複製代碼

爲了方便演示,我將客戶端發起請求的timeout改爲了4s,注意觀察下面的截圖:

longPollingNetwork

能夠看到,斷開鏈接的兩種方式,要麼是超時,要麼是請求有數據返回。

基於iframe的長輪詢模式

這種模式的具體的原理爲:

  • 在頁面中嵌入一個iframe,地址指向輪詢的服務器地址,而後在父頁面中放置一個執行函數,好比execute(data)
  • 當服務器有內容改變時,會向iframe發送一個腳本<script>parent.execute(JSON.stringify(data))</script>
  • 經過發送的腳本,主動執行父頁面中的方法,達到推送的效果

具體能夠參看這裏

Websocket

The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code.

The protocol consists of an opening handshake followed by basic message framing, layered over TCP.

The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that does not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or iframe and long polling).

The WebSocket Protocol attempts to address the goals of existing bidirectional HTTP technologies in the context of the existing HTTP infrastructure; as such, it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries, even if this implies some complexity specific to the current environment.

特徵

  • websocket是雙向通訊的,設計的目的主要是爲了減小傳統輪詢時http鏈接數量的開銷
  • 創建在TCP協議之上,握手階段採用 HTTP 協議,所以握手時不容易屏蔽,能經過各類 HTTP 代理服務器
  • 與HTTP兼容性良好,一樣可使用80和443端口
  • 沒有同源限制,客戶端能夠與任意服務器通訊
  • 能夠發送文本,也能夠發送二進制數據。
  • 協議標識符是ws(若是加密,則爲wss),服務器網址就是 URL

websocket

關於Websocket API方面的知識,這裏再也不做講解,能夠本身查閱Websocket API MDN

兼容性

websocket兼容性良好,基本支持全部現代瀏覽器

websocket1

代碼實現

筆者這裏採用的是socket.io,是基於websocket的封裝,提供了客戶端以及服務器端的支持

// 客戶端
var WebsocketNotification = {
  // ...
  subscribe: function(args) {
    var connector = args[1];
    this.socket = io();

    this.socket.emit('register', connector);

    this.socket.on('register done', function() {
      window.ChatroomDOM.renderAfterRegister();
    });

    this.socket.on('data', function(res) {
      window.ChatroomDOM.renderData(res);
    });

    this.socket.on('disconnect', function() {
      window.ChatroomDOM.renderAfterLogout();
    });
  }
  // ...
}

// 服務器端
var io = socketIo(httpServer);

io.on('connection', (socket) => {
  socket.on('register', function(connector) {
    chatRoom.onConnect(connector);

    io.emit('register done');

    var data = chatRoom.getDatas();
    io.emit('data', { data });
  });

  socket.on('chat', function(message) {
    chatRoom.receive(message);

    var data = chatRoom.getDatas();
    io.emit('data', { data });
  });
});
複製代碼

響應格式以下:

websocket-request-response

Server-Sent Events(SSE)

傳統意義上服務器端不會主動推送給客戶端消息,通常都是客戶端主動去請求服務器端獲取最新的數據。SSE就是一種能夠主動從服務端推送消息的技術。

SSE的本質其實就是一個HTTP的長鏈接,只不過它給客戶端發送的不是一次性的數據包,而是一個stream流,格式爲text/event-stream,因此客戶端不會關閉鏈接,會一直等着服務器發過來的新的數據流,視頻播放就是這樣的例子。

  • SSE 使用 HTTP 協議,現有的服務器軟件都支持。WebSocket 是一個獨立協議。
  • SSE 屬於輕量級,使用簡單;WebSocket 協議相對複雜。
  • SSE 默認支持斷線重連,WebSocket 須要本身實現。
  • SSE 通常只用來傳送文本,二進制數據須要編碼後傳送,WebSocket 默認支持傳送二進制數據。
  • SSE 支持自定義發送的消息類型。

基本的使用方法,參看SSE API

sse

兼容性

目前除了IE以及低版本的瀏覽器不支持,基本支持絕大多數的現代瀏覽器。

sse2

代碼實現

// 客戶端
var SSENotification = {
  source: null,
  subscribe: function() {
    if ('EventSource' in window) {
      this.source = new EventSource('/sse');

      this.source.addEventListener('message', function(res) {
        const d = res.data;
        window.ChatroomDOM.renderData(JSON.parse(d));
      });
    }
    return this.unsubscribe;
  },
  unsubscribe: function () {
    this.source && this.source.close();
  }
}

// 服務器端
router.get('/sse', function(req, res) {
  const connectors = chatRoom.getConnectors();
  const messages = chatRoom.getMessages();
  const response = { code: 200, data: { connectors: connectors, messages: messages } };

  res.writeHead(200, {
    "Content-Type":"text/event-stream",
    "Cache-Control":"no-cache",
    "Connection":"keep-alive",
    "Access-Control-Allow-Origin": '*',
  });

  res.write("retry: 10000\n");
  res.write("data: " + JSON.stringify(response) + "\n\n");

  var unsubscribe = Event.subscribe(function() {
    const connectors = chatRoom.getConnectors();
    const messages = chatRoom.getMessages();
    const response = { code: 200, data: { connectors: connectors, messages: messages } };
    res.write("data: " + JSON.stringify(response) + "\n\n");
  });

  req.connection.addListener("close", function () {
    unsubscribe();
  }, false);
});
複製代碼

下面是控制檯的狀況,注意觀察響應類型

sse-type

詳情中注意查看請求類型,以及EventStream消息類型

sse

總結

  • 短輪詢、長輪詢實現成本相對比較簡單,適用於一些實時性要求不高的消息推送,在實時性要求高的場景下,會存在延遲以及會給服務器帶來更大的壓力
  • websocket目前而言實現成本相對較低,適合於雙工通訊,對於多人在線,要求實時性較高的項目比較實用
  • SSE只能是服務器端推送消息,所以對於不須要雙向通訊的項目比較適用

參考鏈接

相關文章
相關標籤/搜索