JavaScript是如何工作:深入探索WebSocket和HTTP/2

  • 2019 年 12 月 30 日
  • 筆記

摘要: 對比WebSocket和HTTP/2。

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。

版權聲明

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

您的用戶遇到BUG了嗎?

體驗Demo 免費使用

.copyright *{box-sizing:border-box}