輪詢、SSE、Web Socket

在閱讀組內編碼規範時,遇到了一個陌生的概念:SSE 。因而去查了下,它的全稱是 Server-sent events 。這是一種實現服務端向客戶端(Browser)「主動推送」 的技術,相似的技術還有輪詢和 Web Socket。固然 SSE 和輪詢同樣它們實質上並非主動推送,只是使用一種很糙的方式實現了 「等價」 的效果。本文將幾種技術做簡單介紹和總結,重在介紹思想,API的細節仍是參考相關文檔比較好。javascript

輪詢

短輪詢

短輪詢的實現思路很簡單,即每隔一段時間就向服務器端發送 Http 請求,我看到網上的文章基本都會給短輪詢加上一條特性:服務端無論數據有沒有更新都會當即返回響應。對於這條特性,我我的認爲是不太準確的。我覺得短輪詢的誕生與 Ajax 技術是強相關的,它是遠古時代人們對網站實時性開始有了愈來愈高的要求後開始出現的一種經過犧牲服務端和客戶端性能來提高實時性的方式。簡單來講就是重複發請求,至於服務端要不要先判斷數據有沒有更新,那是服務端的事情了,固然,也有多是我對短輪詢的理解有誤差。php

短輪詢的客戶端簡單實現以下:html

var xhr = new XMLHttpRequest();
    setInterval(function(){
        xhr.open('GET','/user');
        xhr.onreadystatechange = function(){

        };
        xhr.send();
    },1000)
複製代碼

短輪詢的實現很簡單,也好理解,可缺點也是顯而易見的,對於一個基於短輪詢的應用,若是同時有較大數量級的人在線,每一個用戶的客戶端都會不斷的向服務器發送 http 請求,會給服務器帶來巨大併發壓力。java

所以,短輪詢是難以控制的。它的實時性是由發送請求的間隔時間來控制的,這致使咱們很難在實時性與良好性能間達到能夠接受的平衡,通常狀況下只能將其用於對實時性要求不高,同時鏈接數較少的應用。node

長輪詢

至於長輪詢,和短輪詢放在一塊兒就很好理解,短輪詢收到請求後返回響應、關閉鏈接。而咱們知道 TCP 協議是支持長鏈接的,在此之上的 HTTP1.1 支持持久鏈接。所以咱們能夠在收到請求後 hold 住鏈接,等到服務端有消息推送時再返回響應關閉鏈接,這就是長輪詢。react

也就是說當服務器收到客戶端發來的請求後,服務器端不會直接進行響應,而是先將這個請求掛起,而後判斷服務器端數據是否有更新。若是有更新,則進行響應,若是一直沒有數據,則到達必定的時間限制才返回。 客戶端腳本中的響應處理函數會在處理完服務器返回的信息後,再次發出請求,從新創建鏈接。android

長輪詢和短輪詢比起來,明顯減小了不少沒必要要的http請求次數,相比之下節約了資源。長輪詢的缺點在於,鏈接掛起也會致使資源的浪費。git

客戶端示例:github

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // 狀態 502 是鏈接超時錯誤,
    // 鏈接掛起時間過長時可能會發生,
    // 遠程服務器或代理會關閉它
    // 讓咱們從新鏈接
    await subscribe();
  } else if (response.status != 200) {
    // 一個 error —— 讓咱們顯示它
    showMessage(response.statusText);
    // 一秒後從新鏈接
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // 獲取並顯示消息
    let message = await response.text();
    showMessage(message);
    // 再次調用 subscribe() 以獲取下一條消息
    await subscribe();
  }
}

subscribe();
複製代碼

服務端示例:ajax

const Koa = require('koa');
const app = new Koa();

// response
app.use(async ctx => {
    let rel = await Promise.race([delay(1000 * 10), getRel(1000 * 5)]);
    ctx.body = rel;
});

function delay(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('delayed');
        }, ms);
    });
}

function getRel(ms) {
    return new Promise(resolve => {
        let time = new Date();
        let it = setInterval(() => {
            if (Date.now() - time > ms) {
                clearInterval(it);
                resolve('gotRel');
            }
        }, 10);
    });
}

const port = 3000;

app.listen(port, err => {
    if (err) {
        console.error(`err: ${err}`);
    }
    console.log(`server start listening ${port}`);
});
複製代碼

SSE

前面咱們已經知道 Http 協議沒法作到服務端主動推送消息,可是有一種取巧的辦法,就是讓服務端向客戶端發送的是流信息(Streaming)。即:

Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no
複製代碼

使用 Cache-Control:no-transform 這一行是由於若是你用了create-react-app等工具來轉發你的請求,那麼你的數據流極可能被壓縮,形成你怎麼也收不到響應。

而加上 X-Accel-Buffering: no 這一行是由於若是網站使用Nginx 方向代理,默認會對應用的響應作緩衝(buffering),致使應用返回的消息不會立馬發出去。這點在Ngnix 官網中也是有說明的:

Sets the proxy buffering for this connection. Setting this to 「no」 will allow unbuffered responses suitable for Comet and HTTP streaming applications. Setting this to 「yes」 will allow the response to be cached

一樣,咱們將SSE 和 前面說的長輪詢進行對比,SSE 的實現和長輪詢是比較類似的,不一樣之處在於 SSE 中 每一個鏈接不僅發送一個消息。客戶端發送一個請求後,服務端會保持這個鏈接,即便有新消息發送回客戶端,咱們仍然能夠保持着鏈接,這樣鏈接就能夠消息的再次發送,由服務器單向發送給客戶端。由於發送的不是一次性的數據包,而是一個數據流,會接二連三地發送過來,就像視頻播放的數據流同樣。

基本用法:

var evtSource = new EventSource(url);
複製代碼

若是發送事件的腳本不一樣源,應該建立一個新的包含URL和options參數的EventSource對象。例如,假設客戶端腳本在example.com上:

const evtSource = new EventSource("//api.example.com/ssedemo.php", { withCredentials: true } );
複製代碼

ps:在我實際測試中,瀏覽器對 EventSource 的跨域支持彷佛不是太好,不過也沒繼續深究了。

成功初始化一個事件源後,咱們就能夠經過 addeventlistener 爲不一樣類型的事件添加監聽函數或者將**事件分發(attach)**到對應屬性上來監遵從服務器發出的消息。

客戶端示例:

<body>
    <button id="btn">創建鏈接</button>
    <button id="btn2">關閉鏈接</button>
    <div id="result"></div>
    <script>
        var btn=document.querySelector("#btn");
        var btn2=document.querySelector("#btn2");
        var result=document.querySelector("#result");
        var source;
        btn.onclick=function () {
            source=new EventSource("http://localhost:8088/sse");
            source.addEventListener("open",function () {
                result.innerHTML+="創建鏈接<br/>";
            },false);
            source.addEventListener("connecttime",function (e) {
                result.innerHTML+="鏈接已創建:"+e.data+"<br/>";
            },false);
            source.addEventListener("message",function (e) {
                result.innerHTML+="接受更新時間:"+e.data+"<br/>";
            },false)
        };
        btn2.onclick=function () {
            if(source){
                source.close();
                result.innerHTML+="關閉鏈接<br/>";
            }
        }
    </script>
</body>
複製代碼

服務端示例:

const onEvent = function(data) {
    res.write(`event: message\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
};

emitter.on('message', onEvent);
複製代碼

咱們用\n來分隔每一行數據,用\n\n來分隔每個事件。每個事件中包含事件的type和事件的data,分別用兩行來描述。好比上面是返回來一個message事件(若不指定事件類型,則默認message)。

而Koa 官網給出的示例是這樣的:

var Transform = require('stream').Transform;
var inherits = require('util').inherits;

module.exports = SSE;

inherits(SSE, Transform);

function SSE(options) {
  if (!(this instanceof SSE)) return new SSE(options);

  options = options || {};
  Transform.call(this, options);
}

SSE.prototype._transform = function(data, enc, cb) {
  this.push('data: ' + data.toString('utf8') + '\n\n');
  cb();
};
複製代碼

而後咱們將 SSE() 賦給 ctx.body 便可,要注意的一點是:

全部轉換流的實現都必須提供 _transform() 方法來接收輸入並生產輸出。 transform._transform() 的實現會處理寫入的字節,進行一些計算操做,而後使用 transform.push() 輸出到可讀流。

Web Socket

不一樣與上面幾種,WebSocket是一種在單個 TCP 鏈接上進行全雙工通訊的協議,是 Http 協議的一個補充(借用 Http 協議來完成一部分握手)。Web Socket 通訊協議於2011年被IETF定爲標準RFC 6455,並由 RFC7936 補充規範。WebSocket API也被 W3C 定爲標準。

WebSocket使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在WebSocket API中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。

客戶端示例:

const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});
複製代碼

Web Socket 服務端的實現就沒 SSE 那麼好搞了。。。不過仍是有相關框架幫咱們作了些封裝:Socket.IO ,這是一個基於 Web Socket 的 Node.js 實時應用程序框架,而且對不支持 Web Socket 的瀏覽器降級成 comet / ajax 輪詢。

另外 Web Socket 是二進制協議,通用性更好,而 SSE 是文本協議(一般使用UTF-8編碼),固然了,你也能夠經過轉碼使其能傳輸二進制數據。

其餘

iframe 永久幀

iframe永久幀也是一種實現服務端推送的方式,很是 hack,也很是不實用。其作法就是在頁面嵌入一個專用來接受數據的 iframe 頁面,該頁面由服務器注入相關信息,如 <script>parent.utils.exec("response")</script>

服務器不停的向iframe中寫入相似的 script 標籤和數據,實現另外一種形式的服務端推送。不過永久幀的技術會致使主頁面的加載條始終處於加載狀態,體驗不好。

主動推送的困境

固然,實際的主動推送場景也許要複雜的多,好比如下兩個問題:

  • 移動端如何維持穩定的長鏈接?
  • 多端場景如何保證推送同步?

實在知識盲區,不過看到了一些解決方案。好比這個:百度雲推送

參考

相關文章
相關標籤/搜索