前情提要
「 話說上回說到!那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主要有這么四個
-
創建es對象:var es = new EventSource(url)
-
es兩端連接事件打開的回調:es.onopen = function () { }
-
監聽服務端發送事件: es.addEventListener("XXX", function (e) { // e.data }
-
監聽服務端的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 () { }
握手代碼邏輯
-
創建stream實例,調用pipe方法輸送給Response, 同時stream我們保存在socket對象中,在向前端發送數據時候會使用
-
將Content-Type字段設置為'text/event-stream',同時Connection設置為'keep-alive'
-
將狀態碼設為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分開,而最后一個字段值需要用\n\n做結尾
例如:`event:message\n data: XXX \n\n`
話不多說,看代碼
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:message\ndata:${dataStr}\n\n`); } } }
Event-Source服務端接收前端消息
之前說了,event-source是單向的,所以前端到服務端的傳送是通過Ajax請求過來的,所以解析下body,觸發事件就OK了
故事到這里就結束了。
有詩為證
江河湖泊浪滔滔,WebSocket多逍遙
EventSource先來卻后到,Ajax輪詢熱血逞英豪!
欲知后事如何,且聽下回分解!
知乎專欄
最近也在知乎上寫文章,感覺破乎的體驗很差!沒有博客園好!感覺博客園的各位才個個都是人才,說話又好聽!我超喜歡在里面的。
所以說。。。大家好,給大家介紹一下這是我的知乎專欄
https://zhuanlan.zhihu.com/c_135367198
這位路過的大哥你有靈氣從鍵盤噴出,看來是百年一遇的代碼奇才,就施舍善心關注一下吧,以解小弟拖家帶口之憂,養兒奉母之愁(大霧)