SSE技術詳解:使用 HTTP 作服務端數據推送應用的技術

  SSE ( Server-sent Events )是 WebSocket 的一種輕量代替方案,使用 HTTP 協議。瀏覽器

  嚴格地說,HTTP 協議是沒有辦法作服務器推送的,可是當服務器向客戶端聲明接下來要發送流信息時,客戶端就會保持鏈接打開,SSE 使用的就是這種原理。服務器

1、SSE 能作什麼?

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

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

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

2、SSE vs. WebSocket

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

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

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

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

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

3、SSE(Server-sent Events)在HTML 5中的技術規範和定義

  Server-sent Events 規範是 HTML 5 規範的一個組成部分,具體的規範文檔見參考資源。該規範比較簡單,主要由兩個部分組成:spa

  第一個部分是服務器端與瀏覽器端之間的通信協議,

  第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。

  通信協議是基於純文本的簡單協議。服務器端的響應的內容類型是「text/event-stream」。響應文本的內容能夠當作是一個事件流,由不一樣的事件所組成。

  每一個事件由類型數據兩部分組成,同時每一個事件能夠有一個可選的標識符。不一樣事件的內容之間經過僅包含回車符和換行符的空行(「\r\n」)來分隔。每一個事件的數據可能由多行組成。

  下面代碼給出了服務器端響應的示例:

data: first event data: second event id: 100 event: myevent data: third event id: 101 : this is a comment data: fourth event data: fourth event continue

  如上所示,每一個事件之間經過空行來分隔。對於每一行來講,冒號(「:」)前面表示的是該行的類型,冒號後面則是對應的值。可能的類型包括:

  • 類型爲空白,表示該行是註釋,會在處理時被忽略。
  • 類型爲 data,表示該行包含的是數據。以 data 開頭的行能夠出現屢次。全部這些行都是該事件的數據。
  • 類型爲 event,表示該行用來聲明事件的類型。瀏覽器在收到數據時,會產生對應類型的事件。
  • 類型爲 id,表示該行用來聲明事件的標識符。
  • 類型爲 retry,表示該行用來聲明瀏覽器在鏈接斷開以後進行再次鏈接以前的等待時間。

  在上述代碼中,第一個事件只包含數據「first event」,會產生默認的事件;第二個事件的標識符是 100,數據爲「second event」;第三個事件會產生類型爲「myevent」的事件;最後一個事件的數據爲「fourth event\nfourth event continue」。當有多行數據時,實際的數據由每行數據以換行符鏈接而成

  若是服務器端返回的數據中包含了事件的標識符,瀏覽器會記錄最近一次接收到的事件的標識符。若是與服務器端的鏈接中斷,當瀏覽器端再次進行鏈接時,會經過 HTTP 頭「Last-Event-ID」來聲明最後一次接收到的事件的標識符。服務器端能夠經過瀏覽器端發送的事件標識符來肯定從哪一個事件開始來繼續鏈接。

  對於服務器端返回的響應,瀏覽器端須要在 JavaScript 中使用 EventSource 對象來進行處理。EventSource 使用的是標準的事件監聽器方式,只須要在對象上添加相應的事件處理方法便可。EventSource 提供了三個標準事件

  如以前所述,服務器端能夠返回自定義類型的事件。對於這些事件,能夠使用 addEventListener 方法來添加相應的事件處理方法。以下代碼給出了 EventSource 對象的使用示例。

var es = new EventSource('events'); es.onmessage = function(e) { console.log(e.data); }; es.addEventListener('myevent', function(e) { console.log(e.data); });

  在指定 URL 建立出 EventSource 對象以後,能夠經過 onmessage 和 addEventListener 方法來添加事件處理方法。當服務器端有新的事件產生,相應的事件處理方法會被調用。EventSource 對象的 onmessage 屬性的做用相似於 addEventListener( ‘ message ’ ),不過 onmessage 屬性只支持一個事件處理方法。

4、簡單示例

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

  一、服務端

'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() 關閉鏈接

  效果:

5、SSE使用注意事項

一、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();

三、瀏覽器兼容

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

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

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

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

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

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

相關文章
相關標籤/搜索