1. 程式人生 > >JavaScript是如何工作: 深入探索WebSocket和HTTP/2與SSE + 如何選擇正確的路徑!

JavaScript是如何工作: 深入探索WebSocket和HTTP/2與SSE + 如何選擇正確的路徑!

Fundebug經授權轉載,版權歸原作者所有。

文章底部分享給大家一套 react + socket 實戰教程

這是專門探索 JavaScript 及其所構建的元件的系列文章的第5篇。

如果你錯過了前面的章節,可以在這裡找到它們:

這一次,我們將深入到通訊協議的領域,對映和探討它們的屬性,並在此過程中構建部分元件。快速比較WebSockets和 HTTP/2。最後,我們分享一些關於如何選擇網路協議的方法。

簡介

如今,功能豐富、動態 ui 的複雜 web 應用程式被認為是理所當然。這並不奇怪——網際網路自誕生以來已經走過了漫長的道路。

最初,網際網路並不是為了支援這種動態和複雜的 web 應用程式而構建的。它被認為是HTML頁面的集合,相互連結形成一個包含資訊的 “web” 概念。一切都是圍繞 HTTP 的所謂 請求/響應 正規化構建的。客戶端載入一個頁面,然後在使用者單擊並導航到下一個頁面之前什麼都不會發生。

大約在2005年,AJAX被引入,很多人開始探索在客戶端和伺服器之間建立雙向連線的可能性。儘管如此,所有HTTP 通訊都由客戶端引導,客戶端需要使用者互動或定期輪詢以從伺服器載入新資料。

讓 HTTP 變成“雙向”互動

讓伺服器能夠“主動”向客戶機發送資料的技術已經出現了相當長的時間。例如“Push”和“Comet”。

最常見的一種黑客攻擊方法是讓伺服器產生一種需要向客戶端傳送資料的錯覺,這稱為長輪詢。通過長輪詢,客戶端開啟與伺服器的 HTTP 連線,使其保持開啟狀態,直到傳送響應為止。 每當伺服器有新資料時需要傳送時,就會作為響應傳送。

看看一個非常簡單的長輪詢程式碼片段是什麼樣的:

(function poll(){
   setTimeout(function(){
      $.ajax({ 
        url: 'https://api.example.com/endpoint', 
        success: function(data) {
          // Do something with `data`
          // ...

          //Setup the next poll recursively
          poll();
        }, 
        dataType: 'json'
      });
  }, 10000);
})();

這基本上是一個自執行函式,第一次立即執行時,它設定了 10 秒間隔,在對伺服器的每個非同步Ajax呼叫之後,回撥將再次呼叫Ajax。

其他技術涉及 Flash 或 XHR multipart request 和所謂的 htmlfiles 。

但是,所有這些工作區都有一個相同的問題:它們都帶有 HTTP 的開銷,這使得它們不適合於低延遲應用程式。想想瀏覽器中的多人第一人稱射擊遊戲或任何其他帶有實時元件的線上遊戲。

WebSockets 的引入

WebSocket 規範定義了在 web 瀏覽器和伺服器之間建立“套接字”連線的 API。簡單地說:客戶機和伺服器之間存在長久連線,雙方可以隨時開始傳送資料。

客戶端通過 WebSocket 握手 過程建立 WebSocket 連線。這個過程從客戶機向伺服器傳送一個常規 HTTP 請求開始,這個請求中包含一個升級頭,它通知伺服器客戶機希望建立一個 WebSocket 連線。

客戶端建立 WebSocket 連線方式如下:

// Create a new WebSocket with an encrypted connection.
var socket = new WebSocket('ws://websocket.example.com')

WebSocket url使用 ws 方案。還有 wss 用於安全的 WebSocket 連線,相當於HTTPS。

這個方案只是開啟 websocket.example.com 的 WebSocket 連線的開始。

下面是初始請求頭的一個簡化示例:

如果伺服器支援 WebSocke t協議,它將同意升級,並通過響應中的升級頭進行通訊。

Node.js 的實現方式:

建立連線後,伺服器通過升級頭部中內容時行響應:

一旦建立連線,open 事件將在客戶端 WebSocket 例項上被觸發:

var socket = new WebSocket('ws://websocket.example.com');

// Show a connected message when the WebSocket is opened.
socket.onopen = function(event) {
  console.log('WebSocket is connected.');
};

現在握手已經完成,初始 HTTP 連線被使用相同底層 TCP/IP 連線的 WebSocket 連線替換。此時,雙方都可以開始傳送資料。

使用 WebSockets,可以傳輸任意數量的資料,而不會產生與傳統 HTTP 請求相關的開銷。資料作為訊息通過 WebSocket 傳輸,每個訊息由一個或多個幀組成,其中包含正在傳送的資料(有效負載)。為了確保訊息在到達客戶端時能夠正確地進行重構,每一幀都以負載的4-12位元組資料為字首, 使用這種基於幀的訊息傳遞系統有助於減少傳輸的非有效負載資料量,從而大大的減少延遲。

注意:值得注意的是,只有在接收到所有幀並重構了原始訊息負載之後,客戶機才會收到關於新訊息的通知。

WebSocket URLs

之前簡要提到過 WebSockets 引入了一個新的URL方案。實際上,他們引入了兩個新的方案:ws:// 和wss://。

url 具有特定方案的語法。WebSocket url 的特殊之處在於它們不支援錨點(#sample_anchor)。

同樣的規則適用於 WebSocket 風格的url和 HTTP 風格的 url。ws 是未加密的,預設埠為80,而 wss 需要TLS加密,預設埠為 443。

幀協議

更深入地瞭解幀協議,這是 RFC 為我們提供的:

在RFC 指定的 WebSocket 版本中,每個包前面只有一個報頭。然而,這是一個相當複雜的報頭。以下是它的構建模組:

  • FIN :1bit ,表示是訊息的最後一幀,如果訊息只有一幀那麼第一幀也就是最後一幀,Firefox 在 32K 之後建立了第二個幀。

  • RSV1,RSV2,RSV3:每個1bit,必須是0,除非擴充套件定義為非零。如果接受到的是非零值但是擴充套件沒有定義,則需要關閉連線。

  • Opcode:4bit,解釋 Payload 資料,規定有以下不同的狀態,如果是未知的,接收方必須馬上關閉連線。狀態如下:

    • 0x00: 附加資料幀
    • 0x01:文字資料幀
    • 0x02:二進位制資料幀
    • 0x3-7:保留為之後非控制幀使用
    • 0x8:關閉連線幀
    • 0x9:ping
    • 0xA:pong
    • 0xB-F(保留為後面的控制幀使用)
  • Mask:1bit,掩碼,定義payload資料是否進行了掩碼處理,如果是1表示進行了掩碼處理。

  • Masking-key:域的資料即是掩碼金鑰,用於解碼PayloadData。客戶端發出的資料幀需要進行掩碼處理,所以此位是1。

  • Payload_len:7位,7 + 16位,7+64位,payload資料的長度,如果是0-125,就是真實的payload長度,如果是126,那麼接著後面的2個位元組對應的16位無符號整數就是payload資料長度;如果是127,那麼接著後面的8個位元組對應的64位無符號整數就是payload資料的長度。

  • Masking-key:0到4位元組,如果MASK位設為1則有4個位元組的掩碼解密金鑰,否則就沒有。

  • Payload data:任意長度資料。包含有擴充套件定義資料和應用資料,如果沒有定義擴充套件則沒有此項,僅含有應用資料。

為什麼 WebSocket 是基於幀而不是基於流?我不知道,就像你一樣,我很想了解更多,所以如果你有想法,請隨時在下面的回覆中新增評論和資源。另外,關於這個主題的討論可以在 HackerNews 上找到。

幀資料

如上所述,資料可以被分割成多個幀。 傳輸資料的第一幀有一個操作碼,表示正在傳輸什麼型別的資料。 這是必要的,因為 JavaScript 在開始規範時幾乎不存在對二進位制資料的支援。 0x01 表示 utf-8 編碼的文字資料,0x02 是二進位制資料。大多數人會發送 JSON ,在這種情況下,你可能要選擇文字操作碼。 當你傳送二進位制資料時,它將在瀏覽器特定的 Blob 中表示。

通過 WebSocket 傳送資料的API非常簡單:

var socket = new WebSocket('ws://websocket.example.com');
socket.onopen = function(event) {
  socket.send('Some message'); // Sends data to server.
};

當 WebSocket 接收資料時(在客戶端),會觸發一個訊息事件。此事件包括一個名為data的屬性,可用於訪問訊息的內容。

// Handle messages sent by the server.
socket.onmessage = function(event) {
  var message = event.data;
  console.log(message);
};

在Chrome開發工具:可以很容易地觀察 WebSocket 連線中每個幀中的資料:

訊息分片

有效載荷資料可以分成多個單獨的幀。接收端應該對它們進行緩衝,直到設定好 fin 位。因此,可以將字串“Hello World”傳送到11個包中,每個包的長度為6(報頭長度)+ 1位元組。控制元件包不允許分片。但是,規範希望能夠處理交錯的控制幀。這是TCP包以任意順序到達的情況。

連線幀的邏輯大致如下:

  • 接收第一幀
  • 記住操作碼
  • 將幀有效負載連線在一起,直到 fin 位被設定
  • 斷言每個包的操作碼是零

分片目的是傳送長度未知的訊息。如果不分片傳送,即一幀,就需要快取整個訊息,計算其長度,構建frame併發送;使用分片的話,可使用一個大小合適的buffer,用訊息內容填充buffer,填滿即傳送出去。

什麼是跳動檢測?

主要目的是保障客戶端 websocket 與服務端連線狀態,該程式有心跳檢測及自動重連機制,當網路斷開或者後端服務問題造成客戶端websocket斷開,程式會自動嘗試重新連線直到再次連線成功。

在使用原生websocket的時候,如果裝置網路斷開,不會觸發任何函式,前端程式無法得知當前連線已經斷開。這個時候如果呼叫 websocket.send 方法,瀏覽器就會發現訊息發不出去,便會立刻或者一定短時間後(不同瀏覽器或者瀏覽器版本可能表現不同)觸發 onclose 函式。

後端 websocket 服務也可能出現異常,連線斷開後前端也並沒有收到通知,因此需要前端定時傳送心跳訊息 ping,後端收到 ping 型別的訊息,立馬返回 pong 訊息,告知前端連線正常。如果一定時間沒收到pong訊息,就說明連線不正常,前端便會執行重連。

為了解決以上兩個問題,以前端作為主動方,定時傳送 ping 訊息,用於檢測網路和前後端連線問題。一旦發現異常,前端持續執行重連邏輯,直到重連成功。

錯誤處理

以通過監聽 error 事件來處理所有錯誤:

var socket = new WebSocket('ws://websocket.example.com');

// Handle any error that occurs.
socket.onerror = function(error) {
  console.log('WebSocket Error: ' + error);
};

歡迎試用Fundebug的BUG監控服務,支援自動捕獲WebSocket連線錯誤。

關閉連線

要關閉連線,客戶機或伺服器都應該傳送包含操作碼0x8的資料的控制幀。當接收到這樣一個幀時,另一個對等點發送一個關閉幀作為響應,然後第一個對等點關閉連線,關閉連線後接收到的任何其他資料都將被丟棄:

// Close if the connection is open.
if (socket.readyState === WebSocket.OPEN) {
    socket.close();
}

另外,為了在完成關閉之後執行其他清理,可以將事件偵聽器附加到關閉事件:

// Do necessary clean up.
socket.onclose = function(event) {
  console.log('Disconnected from WebSocket.');
};

伺服器必須監聽關閉事件以便在需要時處理它:

connection.on('close', function(reasonCode, description) {
    // The connection is getting closed.
});

WebSockets和HTTP/2 比較

雖然HTTP/2提供了很多功能,但它並沒有完全滿足對現有推送/流技術的需求。

關於 HTTP/2 的第一個重要的事情是它並不能替代所有的 HTTP 。verb、狀態碼和大部分頭資訊將保持與目前版本一致。HTTP/2 是意在提升資料線上路上傳輸的效率。

比較HTTP/2和WebSocket,可以看到很多相似之處:

正如我們在上面看到的,HTTP/2引入了 Server Push,它使伺服器能夠主動地將資源傳送到客戶機快取。但是,它不允許將資料下推到客戶機應用程式本身,伺服器推送只由瀏覽器處理,不會在應用程式程式碼中彈出,這意味著應用程式沒有API來獲取這些事件的通知。

這就是伺服器傳送事件(SSE)變得非常有用的地方。SSE 是一種機制,它允許伺服器在建立客戶機-伺服器連線之後非同步地將資料推送到客戶機。然後,只要有新的“資料塊”可用,伺服器就可以決定傳送資料。它可以看作是單向釋出-訂閱模式。它還提供了一個名為 EventSource API 的標準JavaScript,作為W3C HTML5標準的一部分,在大多數現代瀏覽器中實現。不支援 EventSource API 的瀏覽器可以輕鬆地使用 polyfilled 方案來解決。

由於 SSE 基於 HTTP ,因此它與 HTTP/2 非常合適,可以結合使用以實現最佳效果:HTTP/2 處理基於多路複用流的高效傳輸層,SSE 將 API 提供給應用以啟用資料推送。

為了理解 Streams 和 Multiplexing 是什麼,首先看一下``IETF`定義:“stream”是在HTTP/2 連線中客戶機和伺服器之間交換的獨立的、雙向的幀序列。它的一個主要特徵是,一個HTTP/2 連線可以包含多個併發開啟的流,任何一個端點都可以從多個流中交錯幀。

SSE 是基於 HTTP 的,這說明在 HTTP/2 中,不僅可以將多個 SSE 流交織到單個 TCP 連線上,而且還可以通過多個 SSE 流(伺服器到客戶端的推送)和多個客戶端請求(客戶端到伺服器)。因為有 HTTP/2 和 SSE 的存在,現在有一個純粹的 HTTP 雙向連線和一個簡單的 API 就可以讓應用程式程式碼註冊到伺服器推送服務上。在比較 SSE 和 WebSocket 時,缺乏雙向能力往往被認為是一個主要的缺陷。有了 HTTP/2,不再有這種情況。這樣就可以跳過 WebSocket ,而堅持使用基於 HTTP 的訊號機制。

如何選擇WebSocket和HTTP/2?

WebSockets 會在 HTTP/2 + SSE 的領域中生存下來,主要是因為它是一種已經被很好地應用的技術,並且在非常具體的使用情況下,它比 HTTP/2 更具優勢,因為它已經被構建用於具有較少開銷(如報頭)的雙向功能。

假設建立一個大型多人線上遊戲,需要來自連線兩端的大量訊息。在這種情況下,WebSockets 的效能會好很多。

一般情況下,只要需要客戶端和伺服器之間的真正低延遲,接近實時的連線,就使用 WebSocket ,這可能需要重新考慮如何構建伺服器端應用程式,以及將焦點轉移到佇列事件等技術上。

使用的方案需要顯示實時的市場訊息,市場資料,聊天應用程式等,依靠 HTTP/2 + SSE 將為你提供高效的雙向通訊渠道,同時獲得留在 HTTP 領域的各種好處:

  • 當考慮到與現有 Web 基礎設施的相容性時,WebSocket 通常會變成一個痛苦的源頭,因為它將 HTTP 連線升級到完全不同於 HTTP 的協議。
  • 規模和安全性:Web 元件(防火牆,入侵檢測,負載均衡)是以 HTTP 為基礎構建,維護和配置的,這是大型/關鍵應用程式在彈性,安全性和可伸縮性方面更偏向的環境。

原文:https://blog.sessionstack.com…

編輯中可能存在的bug沒法實時知道,事後為了解決這些bug,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具Fundebug。

老鐵福利

Redux+React+Express+Socket.io構建實時聊天應用教程

你的點贊是我持續分享好東西的動力,歡迎點贊!

一個笨笨的碼農,我的世界只能終身學習!

更多內容請關注公眾號《大遷世界》!

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!

版權宣告

轉載時請註明作者Fundebug以及本文地址:https://blog.fundebug.com/2018/12/20/how-does-javascript-websocket-and-http2-work/