【JavaScript】論一個低配版Web實時通訊庫是如何實現的之二( EventSource篇)

  • 2019 年 10 月 3 日
  • 筆記

前情提要

「 話說上回說到!那WebSocket大俠,巧借http之內力,破了敵陣的雙工鴛鴦鎖,終於突出重圍。

然而玄難未了,此時web森林中飛出一隻銀頭紅纓槍,劃破夜

“莫非!?” websocket大俠喃喃念道,”恐怖如斯,你莫不是就是那個手使單向追魂槍的。。。”

“正是在下!”,那人厲聲喝道。只見那胸前的紋章銘刻著幾個洋文——

讀作”EventSource”!」

 

上一篇文章請看這裡:論一個低配版Web實時通訊庫是如何實現的( WebSocket篇)

引論

simple-socket是我寫的一個”低配版”的Web實時通訊工具(相對於Socket.io),在參考了相關源碼和資料的基礎上,實現了前後端實時互通的基本功能,選用了WebSocket ->server-sent-event -> AJAX輪詢這三種方式做降級兼容,分為simple-socket-client和simple-socket-server兩套程式碼。

我的上一篇文章講了如何進行websocket的前後端編碼,所以今天來聊一聊event-source這塊的

論一個低配版Web實時通訊庫是如何實現的( WebSocket篇)

github倉庫地址

https://github.com/penghuwan/simple-socket

npm命令

npm i simple-socket-serve   (服務端npm包)  npm i simple-socket-client   (客戶端npm包)

 

EventSource的前端程式碼

EventSource的前端API主要有這麼四個

  1. 創建es對象:var es = new EventSource(url)

  2. es兩端連接事件打開的回調:es.onopen = function () { }

  3. 監聽服務端發送事件: es.addEventListener(“XXX”, function (e) { // e.data }

  4. 監聽服務端的message事件es.onmessage = function; 相當於es.addEventListener(“message”,function);

業務程式碼如下

(1)前端從服務端接收消息

前端通過監聽服務端message事件,接收消息,並解析event和data,然後通過emitter.emit(event, data)觸發事件,從而調用socket.on設置的監聽回調

 

function Client() {    this.ws = null    this.es = null;   // EventSource對象    init.call(this);  // 設置this.type並初始化相關對象例如es或ws    listen.call(this);    // ...  }    function listen() {    // 保存this    var self  = this;    switch (this.type) {      // 當type為eventsource時,執行以下程式碼,this.type根據能力檢測設置      case 'eventsource':        // 監聽觸發connect事件,把client對象自身傳入當作socket        this.es.onopen = function () {          emitter.emit('connect', self);        };        // 監聽服務端傳來的message事件        this.es.addEventListener("message", function (e) {          var payload = JSON.parse(e.data);;          var event = payload.event;          var data = payload.data;          emitter.emit(event, data);        }, false);        break;       // ...    }  }

 

(2)前端發送消息給服務端

由於event-source是單向的,只能從服務端從前端發送消息,而不能從前端發送消息給服務端。這和websocket顯著不同

不過別擔心,因為我們不是還有AJAX嘛!

對於前端發送消息的情況 我們可以發一個post請求過去,同時藉助/eventsource這個路徑,告訴服務端這是一個SSE請求

 

$.ajax({,    type: 'POST',    url: `http://${url}/eventsource`,    data: { event, data },    success: function () {    }  });

 

EventSource的服務端程式碼

好像這波就沒了吧,OK,我們接下來走下路。

server-sent-event的服務端握手流程

server-sent-event(或event-source),需要藉助流(stream)的方式去實現通訊。

Stream 是一個抽象介面,Node 中有很多對象實現了這個介面。例如,對http 伺服器的request/response 對象就是一個 Stream。

它可以分為四種類型:

  • Readable – 可讀操作。

  • Writable – 可寫操作。

  • Duplex – 可讀可寫操作.

  • Transform – 操作被寫入數據,然後讀出結果。

伺服器每次接收的Response是一個Writable,它可以被寫入數據,將一個流寫入另一個流可以通過調用pipe方法。

所以我們需要創建一個stream的實例,然後通過調用stream.pipe(Response)將流寫入響應中,這樣就可以被前端es.addEventListener添加的回調給接收到了。

但問題在於 。。。Stream是個抽象介面,Node.js沒有給Stream提供構造函數

 

不過沒關係,我們可以這樣做:

    • 使用call方法繼承stream父函數

    • 使用util.inherits繼承stream的原型

    • 重寫_read和_write方法(否則會報錯)

 

// 因為我們的流需要寫和讀,所以使用雙工的stream.Duplex構造  function EventStream() {  stream.Duplex.call(this);  // 構造函數繼承  }  util.inherits(EventStream, stream.Duplex); // 原型繼承  // 重寫_read和_write方法  EventStream.prototype._read = function () { }  EventStream.prototype._write = function () { }

握手程式碼邏輯

  1. 創建stream實例,調用pipe方法輸送給Response, 同時stream我們保存在socket對象中,在向前端發送數據時候會使用

  2. 將Content-Type欄位設置為’text/event-stream’,同時Connection設置為’keep-alive’

  3. 將狀態碼設為200(否則前端onopen方法不會觸發)

 

_handleEShandShake(ctx, socket) {    // 前面定義好的類似stream的類    const eventStream = new EventStream();    // 設置eventStream    socket.setEventStream(eventStream);    // 握手成功後觸發onConnection方法,TODO    // 設置符合Event-Source要求的首部    ctx.set({      'Content-Type': 'text/event-stream',      'Cache-Control': 'no-cache',      'Connection': 'keep-alive',    });    // 將Stream賦給body,Koa底層會判斷Stream類型並調用pipe方法流入response    ctx.body = eventStream;    // 設置表示請求成功,否則前端onopen方法不會觸發      ctx.status = 200;    // 觸發connect方法,傳遞socket對象    this.emit('connect', socket);  }

 

Event-Source服務端向前端發送消息。

這裡要先說下event-source的報文結構了,由四種欄位組成

  • event:事件名,對應前端es.addEventLisener設置的事件名

  • data:數據,為字元串

  • id: 消息標識符,可以預設

  • retry:表示重新連接的時間間隔

這四個欄位兩兩之間用n分開,而最後一個欄位值需要用nn做結尾

例如:`event:messagen data: XXX nn`

 

話不多說,看程式碼

class Socket extends events.EventEmitter {    constructor(socketId) {      super();    }    // 設置    setEventStream(eventStream) {      this.eventStream = eventStream;    }      // 自定義的emit,觸發的是前端的on    emit(event, data) {      const dataStr = JSON.stringify({event,data})      if (this.transport === 'eventsource') {        if (!this.eventStream) { throw new Error('eventStream不存在,無法emit') };        // 向stream中寫入數據,只要stream尚未關閉        // 數據就會傳給前端的onmessage方法或addEventListener('message',fuc)方法        this.eventStream.push(`event:messagendata:${dataStr}nn`);      }    }  }

Event-Source服務端接收前端消息

之前說了,event-source是單向的,所以前端到服務端的傳送是通過Ajax請求過來的,所以解析下body,觸發事件就OK了

 

故事到這裡就結束了。

有詩為證

 

江河湖泊浪滔滔,WebSocket多逍遙

EventSource先來卻後到,Ajax輪詢熱血逞英豪!

 

欲知後事如何,且聽下回分解!

知乎專欄

最近也在知乎上寫文章,感覺破乎的體驗很差!沒有部落格園好!感覺部落格園的各位才個個都是人才,說話又好聽!我超喜歡在裡面的。

所以說。。。大家好,給大家介紹一下這是我的知乎專欄

https://zhuanlan.zhihu.com/c_135367198

這位路過的大哥你有靈氣從鍵盤噴出,看來是百年一遇的程式碼奇才,就施捨善心關注一下吧,以解小弟拖家帶口之憂,養兒奉母之愁(大霧)