webpack與browser-sync熱更新原理深度講解

路易斯發表於2019-03-04

本文首發於CSDN網站,下面的版本又經過進一步的修訂。
原文:webpack與browser-sync熱更新原理深度講解
本文包含如下內容:

  1. webpack-hot-middleware
  2. EventSource
    1. CORS
    2. nginx配置
  3. browser-sync
  4. WebSocket
    1. 支援性
    2. Frame
    3. 建立連線
    4. 服務端實現
    5. 傳送和監聽訊息
    6. 關閉連線
    7. 擁有的屬性
    8. 檔案上傳
    9. 心跳連線
    10. Socket.IO
  5. 小結

開發環境頁面熱更新早已是主流,我們不光要吃著火鍋唱著歌,享受熱更新高效率的快感,更要深入下去探求其原理。

要知道,觸類則旁通,常見的需求如賽事網頁推送比賽結果、網頁實時展示投票或點贊資料、線上評論或彈幕、線上聊天室等,都需要藉助熱更新功能,才能達到實時的端對端的極致體驗。

剛好,最近解決webpack-hot-middleware熱更新延遲問題的過程中,我深入接觸了EventSource技術。遂本文由此開篇,進一步講解webpack-hot-middlewarebrowser-sync背後的技術。

webpack-hot-middleware

webpack-hot-middleware中介軟體是webpack的一個plugin,通常結合webpack-dev-middleware一起使用。藉助它可以實現瀏覽器的無重新整理更新(熱更新),即webpack裡的HMR(Hot Module Replacement)。如何配置請參考 webpack-hot-middleware,如何理解其相關外掛請參考 手把手深入理解 webpack dev middleware 原理與相關 plugins

webpack加入webpack-hot-middleware後,記憶體中的頁面將包含HMR相關js,載入頁面後,Network欄可以看到如下請求:

webpack與browser-sync熱更新原理深度講解
__webpack_hmr

__webpack_hmr是一個type為EventSource的請求, 從Time欄可以看出:預設情況下,伺服器每十秒推送一條資訊到瀏覽器。

webpack與browser-sync熱更新原理深度講解
hmr每10秒推送一條資訊

如果此時關閉開發伺服器,瀏覽器由於重連機制,將持續丟擲類似GET http://www.test.com/__webpack_hmr 502 (Bad Gateway) 這樣的錯誤。重新啟動開發伺服器後,重連將會成功,此時便會重新整理頁面。

以上這些便是我們使用時感受到的最初的印象。當然,停留在使用層面不是我們的目標,接下來我們將跳出該中介軟體,講解其所使用到的EventSource技術。

EventSource

EventSource 不是一個新鮮的技術,它早就隨著H5規範提出了,正式一點應該叫Server-sent events,即SSE

鑑於傳統的通過ajax輪訓獲取伺服器資訊的技術方案已經過時,我們迫切需要一個高效的節省資源的方式去獲取伺服器資訊,一旦伺服器資源有更新,能夠及時地通知到客戶端,從而實時地反饋到使用者介面上。EventSource就是這樣的技術,它本質上還是HTTP,通過response流實時推送伺服器資訊到客戶端。

新建一個EventSource物件非常簡單。

const es = new EventSource('/message');// /message是服務端支援EventSource的介面複製程式碼

新建立的EventSource物件擁有如下屬性:

屬性 描述
url(只讀) es物件請求的伺服器url
readyState(只讀) es物件的狀態,初始為0,包含CONNECTING (0),OPEN (1),CLOSED (2)三種狀態
withCredentials 是否允許帶憑證等,預設為false,即不支援傳送cookie

服務端實現/message介面,需要返回型別為 text/event-stream的響應頭。

var http = require('http');
http.createServer(function(req,res){
  if(req.url === '/message'){
    res.writeHead(200,{
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    });
    setInterval(function(){
      res.write('data: ' + +new Date() + '\n\n');
    }, 1000);
  }
}).listen(8888);複製程式碼

我們注意到,為了避免快取,Cache-Control 特別設定成了 no-cache,為了能夠傳送多個response, Connection被設定成了keep-alive.。傳送資料時,請務必保證伺服器推送的資料以 data:開始,以\n\n結束,否則推送將會失敗(原因就不說了,這是約定的)。

以上,伺服器每隔1s主動向客戶端傳送當前時間戳,為了接受這個資訊,客戶端需要監聽伺服器。如下:

es.onmessage = function(e){
  console.log(e.data); // 列印伺服器推送的資訊
}複製程式碼

如下是訊息推送的過程:

webpack與browser-sync熱更新原理深度講解
response size不斷增加

webpack與browser-sync熱更新原理深度講解
接收訊息

你以為es只能監聽message事件嗎?並不是,message只是預設的事件型別。實際上,它可以監聽任何指定型別的事件。

es.addEventListener("####", function(e) {// 事件型別可以隨你定義
  console.log('####:', e.data);
},false);複製程式碼

伺服器傳送不同型別的事件時,需要指定event欄位。

res.write('event: ####\n');
res.write('data: 這是一個自定義的####型別事件\n');
res.write('data: 多個data欄位將被解析成一個欄位\n\n');複製程式碼

如下所示:

webpack與browser-sync熱更新原理深度講解
####訊息

可以看到,服務端指定event事件名為"####"後,客戶端觸發了對應的事件回撥,同時服務端設定的多個data欄位,客戶端使用換行符連線成了一個字串。

不僅如此,事件流中還可以混合多種事件,請看我們是怎麼收到訊息的,如下:

webpack與browser-sync熱更新原理深度講解
混合訊息

除此之外,es物件還擁有另外3個方法: onopen()onerror()close(),請參考如下實現。

es.onopen = function(e){// 連結開啟時的回撥
  console.log('當前狀態readyState:', es.readyState);// open時readyState===1
}
es.onerror = function(e){// 出錯時的回撥(網路問題,或者服務下線等都有可能導致出錯)
  console.log(es.readyState);// 出錯時readyState===0
  es.close();// 出錯時,chrome瀏覽器會每隔3秒向伺服器重發原請求,直到成功. 因此出錯時,可主動斷開原連線.
}複製程式碼

使用EventSource技術實時更新網頁資訊十分高效。實際使用中,我們幾乎不用擔心相容性問題,主流瀏覽器都了支援EventSource,當然,除了掉隊的IE系。對於不支援的瀏覽器,其PolyFill方案請參考HTML5 Cross Browser Polyfills

CORS

另外,如果需要支援跨域呼叫,請設定響應頭Access-Control-Allow-Origin': '*'

如需支援傳送cookie,請設定響應頭Access-Control-Allow-Origin': req.headers.originAccess-Control-Allow-Credentials:true,並且建立es物件時,需要明確指定是否傳送憑證。如下:

var es = new EventSource('/message', {
  withCredentials: true
}); // 建立時指定配置才是有效的
es.withCredentials = true; // 與ajax不同,這樣設定是無效的複製程式碼

以下是主流瀏覽器對EventSource的CORS的支援:

Firefox Opera Chrome Safari iOS Android
10+ 12+ 26+ 7.0+ 7.0+ 4.4+

nginx配置

既然說到了EventSource,便有必要談談遇到的坑,接下來,就說說我遇到的webpack熱更新延遲問題。

如我們所知,webpack藉助webpack-hot-middleware外掛,實現了網頁熱更新機制,正常情況下,瀏覽器開啟 http://localhost:8080 這樣的網頁即可開始除錯。然而實際開發中,由於遠端伺服器需要種cookie登入態到特定的域名上等原因,因此本地往往會用nginx做一層反向代理。即把 www.test.com 的請求轉發到 http://localhost:8080 上(配置過程這裡不詳述,具體請參考Ajax知識體系大梳理-ajax除錯技巧)。轉發過後,發現熱更新便延遲了。

原因是nginx預設開啟的buffer機制快取了伺服器推送的片段資訊,快取達到一定的量才會返回響應內容。只要關閉proxy_buffering即可。配置如下所示:

server {
    listen       80;
    server_name  www.test.company.com;
    location / {
        proxy_pass http://localhost:8080;
        proxy_buffering off;
    }
}複製程式碼

至此,EventSource部分便告一段落。學習講究由淺入深,循序漸進。後面我將重點講解的browser-sync熱更新機制,請耐心細讀。

browser-sync

開發中使用browser-sync外掛除錯,一個網頁裡的所有互動動作(包括滾動,輸入,點選等等),可以實時地同步到其他所有開啟該網頁的裝置,能夠節省大量的手工操作時間,從而帶來流暢的開發除錯體驗。目前browser-sync可以結合GulpGrunt一起使用,其API請參考:Browsersync API

通過上面的瞭解,我們知道EventSouce的使用是比較便捷的,那為什麼browser-sync不使用EventSource技術進行程式碼推送呢?這是因為browser-sync外掛共做了兩件事:

  • 開發更新了一段新的邏輯,伺服器實時推送程式碼改動資訊。資料流:伺服器 —> 瀏覽器,使用EventSource技術同樣能夠實現。
  • 使用者操作網頁,滾動、輸入或點選等,操作資訊實時傳送給伺服器,然後再由伺服器將操作同步給其他已開啟的網頁。資料流:瀏覽器 —> 伺服器 —> 瀏覽器,該部分功能EventSource技術已無能為力。

以上,browser-sync使用WebSocket技術達到實時推送程式碼改動和使用者操作兩個目的。至於它是如何計算推送內容,根據不同推送內容採取何種響應策略,不在本次討論範圍之內。下面我們將講解其核心的WebSocket技術。

WebSocket

WebSocket是基於TCP的全雙工通訊的協議,它與EventSource有著本質上的不同.(前者基於TCP,後者依然基於HTTP) 該協議於2011年被IETF定為標準RFC6455,後被RFC7936補充. WebSocket api也被W3C定為標準。

WebSocket使用和HTTP相同的TCP埠,預設為80, 統一資源標誌符為ws,執行在TLS之上時,預設使用443,統一資源標誌符為wss。它通過101 switch protocol進行一次TCP握手,即從HTTP協議切換成WebSocket通訊協議。

相對於HTTP協議,WebSocket擁有如下優點:

  • 全雙工,實時性更強。
  • 相對於http攜帶完整的頭部,WebSocket請求頭部明顯減少。
  • 保持連線狀態,不用再驗權了。
  • 二進位制支援更強,Websocket定義了二進位制幀,處理更輕鬆。
  • Websocket協議支援擴充套件,可以自定義的子協議,如 permessage-deflate 擴充套件。

支援性

優秀技術的落地,調研相容性是必不可少的環節。所幸的是,現代瀏覽器對WebSocket的支援比較友好,如下是PC端相容性:

IE/Edge Firefox Chrome Safari Opera
10+ 11+ 16+ 7+ 12.1+

如下是mobile端相容性:

iOS Safari Android Android Chrome Android UC QQ Browser Opera Mini
7.1+ 4.4+ 57+ 11.4+ 1.2+ -

Frame

根據RFC6455文件,WebSocket協議基於Frame而非Stream(EventSource是基於Stream的)。因此其傳輸的資料都是Frame(幀)。想要了解資料的往返,弄懂協議處理過程,Frame的解讀是必不可少。如下便是Frame的結構:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued,if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key,if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+複製程式碼

第一個位元組包含FIN、RSV、Opcode。

  • FIN:size為1bit,標示是否最後一幀。%x0表示還有後續幀,%x1表示這是最後一幀。

  • RSV1、2、3,每個size都是1bit,預設值都是0,如果沒有定義非零值的含義,卻出現了非零值,則WebSocket連結將失敗。

  • Opcode,size為4bits,表示『payload data』的型別。如果收到未知的opcode,連線將會斷開。已定義的opcode值如下:

    %x0:    代表連續的幀
    %x1:    文字幀
    %x2:    二進位制幀
    %x3~7:    預留的非控制幀
    %x8:    關閉握手幀
    %x9:    ping幀,後續心跳連線會講到
    %xA:    pong幀,後續心跳連線會講到
    %xB~F:    預留的非控制幀複製程式碼

第二個位元組包含Mask、Payload len。

  • Mask:size為1bit,標示『payload data』是否新增掩碼。所有從客戶端傳送到服務端的幀都會被置為1,如果置1,Masking-key便會賦值。

    //若server是一個WebSocket服務端例項
    //監聽客戶端訊息
    server.on('message', function(msg, flags) {
      console.log('client say: %s', msg);
      console.log('mask value:', flags.masked);// true,進一步佐證了客戶端傳送到服務端的Mask幀都會被置為1
    });
    //監聽客戶端pong幀響應
    server.on('pong', function(msg, flags) {
      console.log('pong data: %s', msg);
      console.log('mask value:', flags.masked);// true,進一步佐證了客戶端傳送到服務端的Mask幀都會被置為1
    });複製程式碼
  • Payload len:size為7bits,即使是當做無符號整型也只能表示0~127的值,所以它不能表示更大的值,因此規定"Payload data"長度小於或等於125的時候才用來描述資料長度。如果Payload len==126,則使用隨後的2bytes(16bits)來儲存資料長度。如果Payload len==127,則使用隨後的8bytes(64bits)來儲存資料長度。

以上,擴充套件的Payload len可能佔據第三至第四個或第三至第十個位元組。緊隨其後的是"Mask-key"。

  • Mask-key:size為0或4bytes(32bits),預設為0,與前面Mask呼應,從客戶端傳送到服務端的幀都包含4bytes(32bits)的掩碼,一旦掩碼被設定,所有接收到的"payload data"都必須與該值以一種演算法做異或運算來獲取真實值。
  • Payload data:size為"Extension data" 和 "Application data" 的總和,一般"Extension data"資料為空。
  • Extension data:預設為0,如果擴充套件被定義,擴充套件必須指定"Extension data"的長度。
  • Application data:佔據"Extension data"之後剩餘幀的空間。

關於Frame的更多理論介紹不妨讀讀 學習WebSocket協議—從頂層到底層的實現原理(修訂版)

關於Frame的資料幀解析不妨讀讀 WebSocket(貳) 解析資料幀 及其後續文章。

建立連線

瞭解了Frame的資料結構後,我們來實際練習下。瀏覽器上,新建一個ws物件十分簡單。如下:

let ws = new WebSocket('ws://127.0.0.1:10103/');// 本地使用10103埠進行測試複製程式碼

新建的WebSocket物件如下所示:

webpack與browser-sync熱更新原理深度講解
Websocket物件

這中間包含了一次Websocket握手的過程,我們分兩步來理解。

第一步,客戶端請求。

webpack與browser-sync熱更新原理深度講解
Websocket Request

這是一個GET請求,主要欄位如下:

Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key:61x6lFN92sJHgzXzCHfBJQ==
Sec-WebSocket-Version:13複製程式碼

Connection欄位指定為Upgrade,表示客戶端希望連線升級。

Upgrade欄位設定為websocket,表示希望升級至Websocket協議。

Sec-WebSocket-Key欄位是隨機字串,伺服器根據它來構造一個SHA-1的資訊摘要。

Sec-WebSocket-Version表示支援的Websocket版本。RFC6455要求使用的版本是13。

甚至我們可以從請求截圖裡看出,Origin是file://,而Host是127.0.0.1:10103,明顯不是同一個域下,但依然可以請求成功,說明Websocket協議是不受同源策略限制的(同源策略限制的是http協議)。

第二步,服務端響應。

webpack與browser-sync熱更新原理深度講解
Websocket Response

Status Code: 101 Switching Protocols 表示Websocket協議通過101狀態碼進行握手。

Sec-WebSocket-Accept欄位是由Sec-WebSocket-Key欄位加上特定字串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",計算SHA-1摘要,然後再base64編碼之後生成的. 該操作可避免普通http請求,被誤認為Websocket協議。

Sec-WebSocket-Extensions欄位表示服務端對Websocket協議的擴充套件。

以上,WebSocket構造器不止可以傳入url,還能傳入一個可選的協議名稱字串或陣列。

ws = new WebSocket('ws://127.0.0.1:10103/', ['abc','son_protocols']);複製程式碼

服務端實現

等等,我們慢一點,上面好像漏掉了一步,似乎沒有提到服務端是怎麼實現的。請繼續往下看:

先做一些準備。ws是一個nodejs版的WebSocketServer實現。使用 npm install ws 即可安裝。

var WebSocketServer = require('ws').Server,
    server = new WebSocketServer({port: 10103});
server.on('connection', function(s) {
  s.on('message', function(msg) { //監聽客戶端訊息
    console.log('client say: %s', msg);
  });
  s.send('server ready!');// 連線建立好後,向客戶端傳送一條訊息
});複製程式碼

以上,new WebSocketServer()建立伺服器時如需許可權驗證,請指定verifyClient為驗權的函式。

server = new WebSocketServer({
  port: 10103,
  verifyClient: verify
});
function verify(info){
  console.log(Object.keys(info));// [ 'origin', 'secure', 'req' ]
  console.log(info.orgin);// "file://"
  return true;// 返回true時表示驗權通過,否則客戶端將丟擲"HTTP Authentication failed"錯誤
}複製程式碼

以上,verifyClient指定的函式只有一個形參,若為它顯式指定兩個形參,那麼第一個引數同上info,第二個引數將是一個cb回撥函式。該函式用於顯式指定拒絕時的HTTP狀態碼等,它預設擁有3個形參,依次為:

  • result,布林值型別,表示是否通過許可權驗證。
  • code,數值型別,若result值為false時,表示HTTP的錯誤狀態碼。
  • name,字串型別,若result值為false時,表示HTTP狀態碼的錯誤資訊。
// 若verify定義如下
function verify(info, cb){
  //一旦擁有第二個形參,如果不呼叫,預設將通過驗權
  cb(false, 401, '許可權不夠');// 此時表示驗權失敗,HTTP狀態碼為401,錯誤資訊為"許可權不夠"
  return true;// 一旦擁有第二個形參,響應就被cb接管了,返回什麼值都不會影響前面的處理結果
}複製程式碼

除了portverifyClient設定外,其它設定項及更多API,請參考文件 ws-doc

傳送和監聽訊息

接下來,我們來實現訊息收發。如下是客戶端傳送訊息。

ws.onopen = function(e){
  // 可傳送字串,ArrayBuffer 或者 Blob資料
  ws.send('client ready!);
};複製程式碼

客戶端監聽資訊。

ws.onmessage = function(e){
  console.log('server say:', e.data);
};複製程式碼

如下是瀏覽器的執行截圖。

webpack與browser-sync熱更新原理深度講解
message

訊息的內容都在Frames欄,第一條彩色背景的資訊是客戶端傳送的,第二條是服務端傳送的。兩條訊息的長度都是13。

如下是Timing欄,不止是WebSocket,包括EventSource,都有這樣的黃色高亮警告。

webpack與browser-sync熱更新原理深度講解
Websocket Request

該警告說明:請求還沒完成。實際上,直到一方連線close掉,請求才會完成。

關閉連線

說到close,ws的close方法比es的略複雜。

語法:close(short code,string reason);

close預設可傳入兩個引數。code是數字,表示關閉連線的狀態號,預設是1000,即正常關閉。(code取值範圍從0到4999,其中有些是保留狀態號,正常關閉時只能指定為1000或者3000~4999之間的值,具體請參考CloseEvent - Web APIs)。reason是UTF-8文字,表示關閉的原因(文字長度需小於或等於123位元組)。

由於code 和 reason都有限制,因此該方法可能丟擲異常,建議catch下.

try{
  ws.close(1001, 'CLOSE_GOING_AWAY');
}catch(e){
  console.log(e);
}複製程式碼

ws物件還擁有onclose和onerror監聽器,分別監聽關閉和錯誤事件。(注:EventSource沒有onclose監聽)

擁有的屬性

ws的readyState屬性擁有4個值,比es的readyState的多一個CLOSING的狀態。

常量 描述 EventSource(值) WebSocket(值)
CONNECTING 連線未初始化 0 0
OPEN 連線已就緒 1 1
CLOSING 連線正在關閉 - 2
CLOSED 連線已關閉 2 3

另外,除了兩種都有的url屬性外,WebSocket物件還擁有更多的屬性。

屬性 描述
binaryType 被傳輸二進位制內容的型別,有blob,arraybuffer兩種
bufferedAmount 待傳輸的資料的長度
extensions 表示伺服器選用的擴充套件
protocol 指的是構造器第二個引數傳入的子協議名稱

檔案上傳

以前一直是使用ajax做檔案上傳,實際上,Websocket上傳檔案也是一把好刀. 其send方法可以傳送String,ArrayBuffer,Blob共三種資料型別,傳送二進位制檔案完全不在話下。

由於各個瀏覽器對Websocket單次傳送的資料有限制,所以我們需要將待上傳檔案切成片段去傳送。如下是實現。

1) html。

<input type="file" id="file"/>複製程式碼

2) js。

const ws = new WebSocket('ws://127.0.0.1:10103/');// 連線伺服器
const fileSelect = document.getElementById('file');
const size = 1024 * 128;// 分段傳送的檔案大小(位元組)
let curSize, total, file, fileReader;

fileSelect.onchange = function(){
  file = this.files[0];// 選中的待上傳檔案
  curSize = 0;// 當前已傳送的檔案大小
  total = file.size;// 檔案大小
  ws.send(file.name);// 先傳送待上傳檔案的名稱
  fileReader = new FileReader();// 準備讀取檔案
  fileReader.onload = loadAndSend;
  readFragment();// 讀取檔案片段
};

function loadAndSend(){
  if(ws.bufferedAmount > size * 5){// 若傳送佇列中的資料太多,先等一等
    setTimeout(loadAndSend,4);
    return;
  }
  ws.send(fileReader.result);// 傳送本次讀取的片段內容
  curSize += size;// 更新已傳送檔案大小
  curSize < total ? readFragment() : console.log('upload successed!');// 下一步操作
}

function readFragment(){
  const blob = file.slice(curSize, curSize + size);// 獲取檔案指定片段
  fileReader.readAsArrayBuffer(blob);// 讀取檔案為ArrayBuffer物件
}複製程式碼

3) server(node)。

var WebSocketServer = require('ws').Server,
    server = new WebSocketServer({port: 10103}),// 啟動伺服器
    fs = require('fs');
server.on('connection', function(wsServer){
  var fileName, i = 0;// 變數定義不可放在全域性,因每個連線都不一樣,這裡才是私有作用域
  server.on('message', function(data, flags){// 監聽客戶端訊息
    if(flags.binary){// 判斷是否二進位制資料
      var method = i++ ? 'appendFileSync' : 'writeFileSync';
      // 當前目錄下寫入或者追加寫入檔案(建議加上try語句捕獲可能的錯誤)
      fs[method]('./' + fileName, data,'utf-8');
    }else{// 非二進位制資料則認為是檔名稱
      fileName = data;
    }
  });
  wsServer.send('server ready!');// 告知客戶端伺服器已就緒
});複製程式碼

執行效果如下:

webpack與browser-sync熱更新原理深度講解
Websocket upload

上述測試程式碼中沒有過多涉及伺服器的儲存過程。通常,伺服器也會有快取區上限,如果客戶端單次傳送的資料量超過服務端快取區上限,那麼服務端也需要多次讀取。

心跳連線

生產環境下上傳一個檔案遠比本地測試來得複雜。實際上,從客戶端到服務端,中間存在著大量的網路鏈路,如路由器,防火牆等等。一份檔案的上傳要經過中間的層層路由轉發,過濾。這些中間鏈路可能會認為一段時間沒有資料傳送,就自發切斷兩端的連線。這個時候,由於TCP並不定時檢測連線是否中斷,而通訊的雙方又相互沒有資料傳送,客戶端和服務端依然會一廂情願的信任之前的連線,長此以往,將使得大量的服務端資源被WebSocket連線佔用。

正常情況下,TCP的四次揮手完全可以通知兩端去釋放連線。但是上述這種普遍存在的異常場景,將使得連線的釋放成為夢幻。

為此,早在websocket協議實現時,設計者們便提供了一種 Ping/Pong Frame的心跳機制。一端傳送Ping Frame,另一端以 Pong Frame響應。這種Frame是一種特殊的資料包,它只包含一些後設資料,能夠在不影響原通訊的情況下維持住連線。

根據規範RFC 6455,Ping Frame包含一個值為9的opcode,它可能攜帶資料。收到Ping Frame後,Pong Frame必須被作為響應發出。Pong Frame包含一個值為10的opcode,它將包含與Ping Frame中相同的資料。

藉助ws包,服務端可以這麼來傳送Ping Frame。

wsServer.ping();複製程式碼

同時,需要監聽客戶端響應的pong Frame.

wsServer.on('pong', function(data, flags) {
  console.log(data);// ""
  console.log(flags);// { masked: true,binary: true }
});複製程式碼

以上,由於Ping Frame 不帶資料,因此作為響應的Pong Frame的data值為空串。遺憾的是,目前瀏覽器只能被動傳送Pong Frame作為響應(Sending websocket ping/pong frame from browser),無法通過JS API主動向服務端傳送Ping Frame。因此對於web服務,可以採取服務端主動ping的方式,來保持住連結。實際應用中,服務端還需要設定心跳的週期,以保證心跳連線可以一直持續。同時,還應該有重發機制,若連續幾次沒有收到心跳連線的回覆,則認為連線已經斷開,此時便可以關閉Websocket連線了。

Socket.IO

WebSocket出世已久,很多優秀的大神基於此開發出了各式各樣的庫。其中Socket.IO是一個非常不錯的開源WebSocke庫,旨在抹平瀏覽器之間的相容性問題。它基於Node.js,支援以下方式優雅降級:

  • Websocket
  • Adobe® Flash® Socket
  • AJAX long polling
  • AJAX multipart streaming
  • Forever Iframe
  • JSONP Polling

如何在專案中使用Socket.IO,請參考 第一章 socket.io 簡介及使用

小結

EventSource,本質依然是HTTP,它僅提供服務端到客戶端的單向文字資料傳輸,不需要心跳連線,連線斷開會持續觸發重連。

WebSocket協議,基於TCP協議,它提供雙向資料傳輸,支援二進位制,需要心跳連線,連線斷開不會重連。

EventSource更輕量和簡單,WebSocket支援性更好(因其支援IE10+)。通常來說,使用EventSource能夠完成的功能,使用WebSocket一樣能夠做到,反之卻不行,使用時若遇到連線斷開或拋錯,請及時呼叫各自的close方法主動釋放資源。


本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論。

本文作者: louis

本文連結: louiszhai.github.io/2017/04/19/…

參考文章

相關文章