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

因爲 HTTP/1.1 自己不支持服務器主動向客戶端推送消息,在例如即時通信、消息提醒等應用場景中就會很不方便。解決的方法有不少,WebSocket 就很不錯,可是若是想要快速實現的話就不推薦使用,本文要介紹的是一種輕量的解決方案:SSE。javascript

SSE 是基於 HTTP 協議來完成服務器推送的。不要誤會,並非說 HTTP/2,而是一種取巧的方式:當服務器向客戶端聲明接下來要發送流信息時,客戶端就會保持鏈接打開,SSE 使用的就是這種原理。java

SSE 能作什麼

理論上,SSE 和 WebSocket 作的是同一件事情。當你須要用新數據局部更新網絡應用時,SSE 能夠作到不須要用戶執行任何操做,即可以完成。瀏覽器

舉例咱們要作一個統計系統的管理後臺,咱們想知道統計數據的實時狀況。相似這種更新頻繁、低延遲的場景,SSE 能夠徹底知足。服務器

其餘一些應用場景:例如郵箱服務的新郵件提醒,微博的新消息推送、管理後臺的一些操做實時同步等,SSE 都是不錯的選擇。網絡

SSE vs. WebSocket

SSE 是單向通道,只能服務器向客戶端發送消息,若是客戶端須要向服務器發送消息,則須要一個新的 HTTP 請求。這對比 WebSocket 的雙工通道來講,會有更大的開銷。這麼一來的話就會存在一個「何時才須要關心這個差別?」的問題,若是平均每秒會向服務器發送一次消息的話,那應該選擇 WebSocket。若是一分鐘僅 5 - 6 次的話,其實這個差別並不大。socket

在瀏覽器兼容方面,二者差很少。在較早以前,每當須要創建雙向 Socket 時就會使用 Flash,在移動瀏覽器不支持 Flash 的狀況下,WebSocket 的兼容是比較難作的。ide

SSE 我認爲最大的優點是便利:ui

  • 實現一個完整的服務僅須要少許的代碼;
  • 能夠在現有的服務中使用,不須要啓動一個新的服務;
  • 能夠用任何一種服務端語言中使用;
  • 基於 HTTP/HTTPS 協議,能夠直接運行於現有的代理服務器和認證技術。

有了這些優點,在選擇使用 SSE 時就已經爲本身的項目節約了很多成本。編碼

簡單示例

下面是一個簡單的示例,實現一個 SSE 服務。url

服務器

'use strict';

const http = require('http');

http.createServer((req, res) => {

  // 服務器聲明接下來發送的是事件流
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*',
  });

  // 發送消息
  setInterval(() => {
    res.write('event: slide\n'); // 事件類型
    res.write(`id: ${+new Date()}\n`); // 消息ID
    res.write('data: 7\n'); // 消息數據
    res.write('retry: 10000\n'); // 重連時間
    res.write('\n\n'); // 消息結束
  }, 3000);

  // 發送註釋保持長鏈接
  setInterval(() => {
    res.write(': \n\n');
  }, 12000);
}).listen(2000);

服務器首先向客戶端聲明接下來發送的是事件流(text/event-stream)類型的數據,而後就能夠向客戶端屢次發送消息。

事件流是一個簡單的文本流,僅支持 UTF-8 格式的編碼。每條消息以一個空行做爲分隔符。

在規範中爲消息定義了 4 個字段:

event 消息的事件類型。客戶端收到消息時,會在當前的 EventSource 對象上觸發一個事件,這個事件的名稱就是這個字段的值,若是消息沒有這個字段,客戶端的 EventSource 對象就會觸發默認的 message 事件。

id 這條消息的 ID。客戶端接收到消息後,會把這個 ID 做爲內部屬性 Last-Event-ID,在斷開重連成功後,會把 Last-Event-ID 發送給服務器。

data 消息的數據字段。客戶端會把這個字段解析爲字符串,若是一條消息有多個 data 字段,客戶端會自動用換行符鏈接成一個字符串。

retry 指定客戶端重連的時間。只接受整數,單位是毫秒。若是這個值不是整數則會被自動忽略。

一個頗有意思的地方是,規範中規定以冒號開頭的消息都會被看成註釋,一條普通的註釋(:\n\n)對於服務器來講只佔 5 個字符,可是發送到客戶端上的時候不會觸發任何事件,這對客戶端來講是很是友好的。因此註釋通常被用於維持服務器和客戶端的長鏈接。

客戶端

咱們建立了一個 EventSource 對象,傳入參數:url。而且根據服務器的狀態和發送的信息做出響應。

'use strict';

if (window.EventSource) {

  // 建立 EventSource 對象鏈接服務器
  const source = new EventSource('http://localhost:2000');

  // 鏈接成功後會觸發open事件
  source.addEventListener('open', () => {
    console.log('Connected');
  }, false);

  // 服務器發送信息到客戶端時,若是沒有event字段,默認會觸發message事件
  source.addEventListener('message', e => {
    console.log(`data: ${e.data}`);
  }, false);

  // 自定義EventHandler,在收到event字段爲slide的消息時觸發
  source.addEventListener('slide', e => {
    console.log(`data: ${e.data}`); // => data: 7
  }, false);

  // 鏈接異常時會觸發error事件並自動重連
  source.addEventListener('error', e => {
    if (e.target.readyState === EventSource.CLOSED) {
      console.log('Disconnected');
    } else if (e.target.readyState === EventSource.CONNECTING) {
      console.log('Connecting...');
    }
  }, false);
} else {
  console.error('Your browser doesn\'t support SSE');
}

EventSource從父接口 EventTarget 中繼承了屬性和方法,其內置了 3EventHandler 屬性、2 個只讀屬性和 1 個方法:

EventHandler 屬性

EventSource.onopen 在鏈接打開時被調用。

EventSource.onmessage 在收到一個沒有 event 屬性的消息時被調用。

EventSource.onerror 在鏈接異常時被調用。

只讀屬性

EventSource.readyState 一個 unsigned short 值,表明鏈接狀態。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。

EventSource.url 鏈接的 URL。

方法

EventSource.close() 關閉鏈接。

SSE 如何保證數據完整性

客戶端在每次接收到消息時,會把消息的 id 字段做爲內部屬性 Last-Event-ID 儲存起來。

SSE 默認支持斷線重連機制,在鏈接斷開時會觸發EventSource 的 error 事件,同時自動重連。再次鏈接成功時 EventSource 會把 Last-Event-ID 屬性做爲請求頭髮送給服務器,這樣服務器就能夠根據這個 Last-Event-ID 做出相應的處理。

這裏須要注意的是,id 字段不是必須的,服務器有可能不會在消息中帶上 id 字段,這樣子客戶端就不會存在 Last-Event-ID 這個屬性。因此爲了保證數據可靠,咱們須要在每條消息上帶上 id 字段。

減小開銷

在 SSE 的草案中提到,"text/event-stream" 的 MIME 類型傳輸應當在靜置 15 秒後自動斷開。在實際的項目中也會有這個機制,可是斷開的時間沒有被列入標準中。

爲了減小服務器的開銷,咱們也能夠有目的的斷開和重連。

簡單的辦法是服務器發送一個關閉消息並指定一個重連的時間戳,客戶端在觸發關閉事件時關閉當前鏈接並建立一個計時器,在重連時把計時器銷燬。

'use strict';

function connectSSE() {
  if (window.EventSource) {
    const source = new EventSource('http://localhost:2000');
    let reconnectTimeout;

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

    source.addEventListener('pause', e => {
      source.close();
      const reconnectTime = +e.data;
      const currentTime = +new Date();
      reconnectTimeout = setTimeout(() => {
        connectSSE();
      }, reconnectTime - currentTime);
    }, false);
  } else {
    console.error('Your browser doesn\'t support SSE');
  }
}

connectSSE();

瀏覽器兼容

EventSource 瀏覽器兼容
Broswer support of EventSource from Can I Use...

向下兼容

早些時候,爲了實現數據實時更新最多見的方法就是輪詢。

輪詢是以一個固定頻率向服務器發送請求,服務器在有數據更新時返回新的數據,以此來管理數據的更新。這種輪詢的方式不但開銷大,並且更新的效率和頻率有關,也不能達到及時更新的目的。

接着便出現了長輪詢的方式:客戶端向服務器發送請求以後,服務器會暫時把請求掛起,等到有數據更新時再返回最新的數據給客戶端,客戶端在接收到新的消息後再向服務器發送請求。與常規輪詢的不一樣之處是:數據能夠作到實時更新,能夠減小沒必要要的開銷。

這裏有一個「選擇長輪詢仍是常規輪詢?」的命題,長輪詢是否是總比常規輪詢佔有優點?咱們能夠從帶寬佔用的角度分析,若是一個程序數據更新太過頻繁,假設每秒 2 次更新,若是使用長輪詢的話每分鐘要發送 120 次 HTTP 請求。若是使用常規輪詢,每 5 秒發送一次請求的話,一分鐘才 20 次,從這裏看,常規輪詢更佔有優點。

長輪詢和 SSE 最關鍵的區別在於,每一次數據更新都須要一次 HTTP 請求。和 WebSocket 還有 SSE 同樣,長輪詢也會佔用一個 socket。在數據更新效率上和 SSE 差很少,一有數據更新就能檢測到。加上全部瀏覽器都支持,是一個不錯的 SSE 替代方案。

結尾

文章介紹了 SSE 的用法及使用過程當中的一些技巧。對比 WebSocket,SSE 在開發時間和成本上佔有較大的優點。作數據推送服務,除了 WebSocket,SSE 也是一個不錯的選擇,但願對你們有所幫助。

參考

Server-Sent Events
EventSource - Web APIs | MDN
Using server-sent events - Web APIs | MDN

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證) 轉載請註明出處

原文地址:20 行代碼寫一個數據推送服務
文章做者:何啓邦
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證) 轉載請註明出處

相關文章
相關標籤/搜索