首頁  >  文章  >  web前端  >  JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

coldplay.xixi
coldplay.xixi轉載
2020-12-21 17:33:452986瀏覽

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

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

相關免費學習推薦:javascript(影片)

#這次,我們將深入到通訊協定的領域,映射和探討它們的屬性,並在過程中建構部分元件。快速比較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。簡單地說:客戶機和伺服器之間存在長久連接,雙方可以隨時開始發送資料。

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

客戶端透過 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 連線的開始。

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

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

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

Node.js 的實作方式:

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

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

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

#一旦建立連接,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 版本中,每个包前面只有一个报头。然而,这是一个相当复杂的报头。以下是它的构建模块:

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

  • 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 连接中每个帧中的数据:

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

消息分片

有效载荷数据可以分成多个单独的帧。接收端应该对它们进行缓冲,直到设置好 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);
};

关闭连接

要关闭连接,客户机或服务器都应该发送包含操作码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 连接可以包含多个并发打开的流,任何一个端点都可以从多个流中交错帧。

JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!

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 為基礎構建,維護和配置的,這是大型/關鍵應用程式在彈性,安全性和可伸縮性方面更偏向的環境。

以上是JavaScript深入探索 websocket和HTTP/2與SSE +如何選擇正確的路徑!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除