服務端事件EventSource揭祕

服務端推

服務端推,指的是由服務器主動的向客戶端發送消息(響應)。在應用層的HTTP協議實現中,「請求-響應」是一個round trip,它的起點來自客戶端,所以在應用層之上沒法實現簡易的服務端推功能。當前解決服務端推送的方案有這幾個:html

  1. 客戶端長輪訓
  2. websocket雙向鏈接
  3. iframe永久幀

長輪訓雖然能夠避免短輪訓形成的服務端過載,但在服務端返回數據後仍須要客戶端主動發起下一個長輪訓請求,等待服務端響應,這樣仍須要底層的鏈接創建並且服務端處理邏輯須要相應處理,不符合邏輯上的流程簡單的服務端推送;前端

websocket鏈接相對而言功能最強大,可是它對服務器的版本有要求,在可使用websocket協議的服務器上儘可能採用此種方式;node

iframe永久幀則是在在頁面嵌入一個專用來接受數據的iframe頁面,該頁面由服務器輸出相關信息,如<script>parent.utils.exec("response")</script>,服務器不停的向iframe中寫入相似的script標籤和數據,實現另外一種形式的服務端推送。不過永久幀的技術會致使主頁面的加載條始終處於「loading」狀態,體驗不好。web

HTML5規範中提供了服務端事件EventSource,瀏覽器在實現了該規範的前提下建立一個EventSource鏈接後,即可收到服務端的發送的消息,這些消息須要遵循必定的格式,對於前端開發人員而言,只需在瀏覽器中偵聽對應的事件皆可。json

相比較上文中提到的3中實現方式,EventSource流的實現方式對客戶端開發人員而言很是簡單,兼容性上出了IE系的瀏覽器(IE、Edge)外其餘都良好;對於服務端,它能夠兼容老的瀏覽器,無需upgrade爲其餘協議,在簡單的服務端推送的場景下能夠知足需求。在瀏覽器與服務端須要強交互的場景下,websocket還是不二的選擇。跨域

EventSource規範簡析

瀏覽器端

瀏覽器端,須要建立一個EventSource對象,而且傳入一個服務端的接口URI做爲參數。瀏覽器

var evtSource = new EventSource('http://localhost:9111/es');

其中,'http://localhost:9111/es'爲服務端吐出數據的接口。目前,EventSource在大多數瀏覽器端不支持
跨域,所以它不是一種跨域的解決方案。服務器

默認EventSource對象經過偵聽「message」事件獲取服務端傳來的消息,「open」事件則在http鏈接創建後觸發,」error「事件會在通訊錯誤(鏈接中斷、服務端返回數據失敗)的狀況下觸發。同時,EventSource規範容許服務端指定自定義事件,客戶端偵聽該事件便可。websocket

evtSource.addEventListener('message',function(e){
    console.log(e.data);
});
evtSource.addEventListener('error',function(e){
    console.log(e);
})

服務端

事件流的對應MIME格式爲text/event-stream,並且其基於HTTP長鏈接。針對HTTP1.1規範默認採用長鏈接,針對HTTP1.0的服務器須要特殊設置。app

服務端返回數據須要特殊的格式,它分爲四種消息類型:

event, data, id, retry

其中,event指定自定義消息的名稱,如event: customMessagen;

data指定具體的消息體,能夠是對象或者字符串,如data: JSON.stringify(jsonObj)\n\n,在消息體後面有兩個換行符n,表明當前消息體發送完畢,一個換行符標識當前消息並未結束,瀏覽器須要等待後面數據的到來後再觸發事件;

id爲當前消息的標識符,能夠不設置。一旦設置則在瀏覽器端的eventSource對象中就會有體現(假設服務端返回id: 369n),eventSource.lastEventId == 369。該字段使用場景不大;

retry設置當前http鏈接失敗後,從新鏈接的間隔。EventSource規範規定,客戶端在http鏈接失敗後默認進行從新鏈接,重連間隔爲3s,經過設置retry字段可指定重連間隔;

每一個字段都有名稱,緊接着有個」:「。當出現一個沒有名稱的字段而只有」:「時,這就會被服務端理解爲」註釋「,並不會被髮送至瀏覽器端,如: commision

因爲EventSource是基於HTTP鏈接之上的,所以在一段沒有數據的時期會出現超時問題。服務器默認HTTP超時時間爲2分鐘,在node端能夠經過response.connection.setTimeou(0)設置爲默認的2min超時, 所以須要服務端作心跳保活,不然客戶端在鏈接超時的狀況下出現net::ERR_INCOMPLETE_CHUNKED_ENCODING錯誤。經過閱讀相關規範,發現註釋行能夠用來防止鏈接超時,服務器能夠按期發送一條消息註釋行,以保持鏈接不斷。

下面提供koa的服務端代碼:

var fs = require('fs');
var path = require('path');
var PassThrough = require('stream').PassThrough;
var Readable = require('stream').Readable;
var koa = require('koa');
var Router = require('koa-router');
var app = new koa();
var router = new Router();

function RR(){
    Readable.call(this,arguments);
}
RR.prototype = new Readable();
RR.prototype._read = function(data){
}

router.get('/',function(ctx,next){
    ctx.set('content-type','text/html');
    ctx.body = fs.readFileSync(path.join(process.cwd(),'eventServer.html'));
});

const sse = (stream,event, data) => {
    return stream.push(`event:${ event }\ndata: ${ JSON.stringify(data) }\n\n`)
//    return stream.write(`event:${ event }\ndata: ${ JSON.stringify(data) }\n\n`);
}
router.get('/es',function(ctx,next){
    var stream = new RR()//PassThrough();
    ctx.set({
        'Content-Type':'text/event-stream',
        'Cache-Control':'no-cache',
        Connection: 'keep-alive'
    });
    sse(stream,'test',{a: "yango",b: "tango"});
    ctx.body = stream;
    setInterval(()=>{
        sse(stream,'test',{a: "yango",b: Date.now()});
    },3000); 
});

app.use(router.routes());
app.listen(9111,function(){
    console.log('listening port 9111');
});

此處須要注意的是koa-router的返回值必須是一個Stream(Readable),這是因爲koa的特殊性形成的。若是context.body不是Stream是一個字符串或者Buffer實例,會直接在node原生中調用res.end(buffer),結束了HTTP響應:

koa lib/application.js

// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);

所以形成了服務端事件流沒法正確響應。而返回Stream類型的方式有幾種,如經過擴展stream模塊的Readable可讀流返回或者直接採用PassThrough流返回,亦可經過through2模塊或者Transform對象實現,歸根到底保證能夠從該stream對象中pipe出數據至http.ServerResponse對象中。

附頁面代碼

<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div>
        hello world
    </div>
    <p id="info"></p>
    <script>
        var infoShow = document.querySelector('#info');
        var se = new EventSource('http://localhost:9111/es');
        se.addEventListener('test',function(e){
            infoShow.textContent += e.data+'\n';
        });
        se.addEventListener('error',function(e){
            console.log(e);
        })
    </script>
</body>
</html>

參考資料

使用服務器發送事件
EventSource超時

相關文章
相關標籤/搜索