服務器推送 SSE 瞭解一下?

hello~親愛的看官老爺們你們好~過完年第一週已經結束,是時候開始制定新的工做計劃了。主要負責的項目是數據可視化平臺,而使用中若是服務器能有推送能力讓頁端獲得相關通知的話,就能實現不少功能上的優化。鑑於項目中 Node 端已經正式投入使用,前端擁有了本身的服務器,搞事情起來天然方便不少。前端

技術選型:SSE(Server-sent Events) or WebSocket

若干年前,服務器並無主動推送的能力,主要是經過輪詢的方式來達到近似於服務器推送的能力。如今不須要這麼麻煩,輪詢只做爲向下兼容的方案便可,當前主流的服務器推送是使用 SSE 或者 WebSocket 來實現的。二者對好比下:git

是否基於新協議 是否雙向通訊 是否支持跨域 接入成本
SSE 否(Http 否(服務器單向) 否(Firefox 支持跨域)
WebSocket 是(ws

須要稍微解釋一下的是接入成本。SSE 是相對輕量級的協議,(Node)代碼實現上比較簡單,而 WebSocket 是比較複雜的協議,雖然也有類庫能夠套用,也許頁端方面二者代碼量差很少,但服務器方面實現就複雜很多了。同時,要實現 WebSocket,是須要另起一個服務,而 SSE 並不須要。github

比較以後,對 SSE 與 WebSocket 有了大體的理解。項目對服務器推送的要求是發送通知,而將來可能須要接入實時同步的功能,結合項目實際狀況與接入成本後,選擇了 SSE。跨域

最後看一下瀏覽器支持狀況以做參考:瀏覽器

IElet it go吧,平常不支持~其餘瀏覽器仍是綠油油的,支持度仍是挺高的。服務器

示例

Node端

項目中使用 Egg 做爲框架,底層是 Koa2 的,於是使用 Koa2 做爲示例。Node 端關鍵代碼以下:app

app.use(async (ctx) => {
  const { res, request: { url } } = ctx;
  res.writeHead(200, {
    'Content-Type': 'text/event-stream', // 服務器聲明接下來發送的是事件流
  });
  let stream = new PassThrough();
  let i = 0;
  let timer = setInterval(() => {
    if (i === 5) {
      stream.write('event: pause\n'); // 事件類型
    } else {
      stream.write('event: test\n'); // 事件類型
    }
    stream.write(`id: ${+new Date()}\n`); // 消息ID
    stream.write(`data: ${i}\n`); // 消息數據
    stream.write('retry: 10000\n'); // 重連時間
    stream.write('\n\n'); // 消息結束
    i++;
  }, 1000);

  stream.on('close', function() {
    console.log('closed.')
    clearInterval(timer);
  })

  ctx.body = stream;
});
複製代碼

服務器告訴客戶端,返回的類型是事件流(text/event-stream),查閱 MDN 文檔可知:事件流僅僅是一個簡單的文本數據流,文本應該使用 UTF- 8 格式的編碼。每條消息後面都由一個空行做爲分隔符。以冒號開頭的行爲註釋行,會被忽略。框架

以後就是消息主體了,儘管例子使用 setInterval 模擬不斷髮送推送,但換成任意條件觸發推送也是能夠的。stream.write 調用了5次,對應規範中的各個字段,理解以下:async

  • event 爲消息的事件類型。客戶端在 EventSource 中能夠經過 addEventListener 收聽相關的消息。該字段可省略,省略後客戶端觸發 message 事件。
  • id 爲事件 ID。做爲客戶端內部的「最後一個事件 ID」的屬性值,用於重連,不可省略。
  • data 爲消息的數據字段,簡單說就是客戶端監聽時間後,經過e.data 拿到的數據。
  • retry 爲重連時間,可省略該參數。
  • 最後是結束該次通知的 \n\n,不可省略。除了上面規定的字段名,其餘全部的字段名都會被忽略。

更詳細的解釋能夠查閱 MDN 文檔。有一個小細節須要注意,在 SSE 的草案中提到,"text/event-stream" 的 MIME 類型傳輸應當在靜置 15 秒後自動斷開。但實測(僅用了 Chrome)後發現,即便靜置時間超過 15 秒,瀏覽器與客戶端均不會斷開鏈接。查閱了很多文章,均建議維護一套發送 \n\n 的心跳機制。我的認爲此舉有助於提升客戶端程序的健壯性,但不是必須的。測試

最後是監聽事件流的 close 事件,用於結束這次的連接。測試後發現,不管是讓客戶端調用 close 方法(下文有例子~),仍是異常結束,包括關閉窗口、關閉程序等,都能觸發服務器的 close 事件。

客戶端

客戶端代碼更簡單,示例以下:

const source = new EventSource('http://localhost:3000/test');

source.addEventListener('open', () => {
  console.log('Connected');
}, false);

source.addEventListener('message', e => {
  console.log(e.data);
}, false);

source.addEventListener('pause', e => {
  source.close();
}, false);
複製代碼

前端童鞋對於這樣的代碼應該挺熟悉的,一切都是事件觸發,根據不一樣的事件執行對應的代碼。稍微說明一下 EventSource 擁有的屬性和方法,相信你們就能夠愉快地使用了。

EventSource 有三個默認的事件,分別是:

  • open:在鏈接打開時被調用。
  • message:收到一個沒有 event 屬性的消息時被調用。
  • error:當發生錯誤時被調用。

兩個只讀屬性:

  • readyState:表明鏈接狀態。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
  • url:表明鏈接的 URL。

一個方法:

  • close:調用後關閉鏈接(也就是上文所說起的)。

更詳細的解釋能夠查閱 MDN 文檔

小結

關於服務器 SSE 的簡單介紹就到此爲止了,能夠看到,SSE 開發起來仍是比較簡單的,接入成本很是低。但並非說 WebSocket 就是很差的,拋開實際場景談業務就是耍流氓。此外上述代碼只是演示,還能進一步進行優化的。如爲了減輕服務器開銷,能夠創建一套機制有目的地斷開與重連等,你們能夠自行實現。

相關的代碼已經丟到 Github 上,歡迎查閱。

感謝各位看官大人看到這裏,知易行難,但願本文對你有所幫助~謝謝!

參考資料

20 行代碼寫一個數據推送服務

使用服務器發送事件

EventSource

相關文章
相關標籤/搜索