伺服器推送 SSE 瞭解一下?

sea_ljf發表於2019-03-03

hello~親愛的看官老爺們大家好~過完年第一週已經結束,是時候開始制定新的工作計劃了。主要負責的專案是資料視覺化平臺,而使用中如果伺服器能有推送能力讓頁端得到相關通知的話,就能實現很多功能上的優化。鑑於專案中 Node 端已經正式投入使用,前端擁有了自己的伺服器,搞事情起來自然方便很多。

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

若干年前,伺服器並沒有主動推送的能力,主要是通過輪詢的方式來達到近似於伺服器推送的能力。現在不需要這麼麻煩,輪詢只作為向下相容的方案即可,當前主流的伺服器推送是使用 SSE 或者 WebSocket 來實現的。兩者對比如下:

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

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

比較之後,對 SSE 與 WebSocket 有了大致的理解。專案對伺服器推送的要求是傳送通知,而未來可能需要接入實時同步的功能,結合專案實際情況與接入成本後,選擇了 SSE。

最後看一下瀏覽器支援情況以作參考:

伺服器推送 SSE 瞭解一下?

IElet it go吧,日常不支援~其他瀏覽器還是綠油油的,支援度還是挺高的。

示例

Node端

專案中使用 Egg 作為框架,底層是 Koa2 的,因而使用 Koa2 作為示例。Node 端關鍵程式碼如下:

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次,對應規範中的各個欄位,理解如下:

  • 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

相關文章