JavaScript工做原理(五):深刻了解WebSockets,HTTP/2和SSE,以及如何選擇

這一次,咱們將深刻到通訊協議的世界中,對比並討論它們的屬性並構建部件。咱們將提供WebSockets和HTTP / 2的快速比較。 最後,咱們分享一些關於如何選擇網絡協議。html

概述

現在,擁有豐富動態用戶界面的複雜網絡應用程序被視爲理所固然。這並不奇怪 - 互聯網自成立以來已經走過了很長的一段路。git

最初,互聯網並非爲支持這種動態和複雜的網絡應用程序而構建的。它被認爲是HTML頁面的集合,彼此連接以造成包含信息的「web」概念。一切都基本上圍繞HTTP的所謂的請求/響應範式而創建。客戶端加載一個頁面,而後什麼都不會發生,直到用戶點擊並導航到下一頁。github

大約在2005年,AJAX被引入,許多人開始探索在客戶端和服務器之間創建雙向鏈接的可能性。儘管如此,全部HTTP通訊都是由客戶端引導的,這須要用戶交互或按期輪詢從服務器加載新數據。web

使HTTP成爲「雙向」

使服務器「主動」向客戶端發送數據的技術已經存在了至關長的一段時間。例如「Push」和「Comet」等等。ajax

服務器向客戶端發送數據的最多見竅門之一稱爲長輪詢。經過長輪詢,客戶端打開一個HTTP鏈接到服務器,該服務器保持打開狀態直到發送響應。只要服務器有新的數據須要發送,它就會將其做爲響應發送出去。json

咱們來看看一個很是簡單的長輪詢片斷的樣子:api

(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多部分請求和所謂的htmlfiles。緩存

全部這些解決方案都有相同的問題:它們承載HTTP的開銷,這並不能使它們很是適合低延遲的應用程序。在瀏覽器或任何其餘具備實時組件的在線遊戲中考慮多人第一人稱射擊遊戲。安全

WebSockets的引入

WebSocket規範定義了在Web瀏覽器和服務器之間創建「套接字」鏈接的API。 簡而言之:客戶端和服務器之間存在持久鏈接,而且雙方能夠隨時開始發送數據。

客戶端經過稱爲WebSocket握手的過程創建WebSocket鏈接。該過程從客戶端向服務器發送常規HTTP請求開始。此請求中包含Upgrade頭信息,通知服務器客戶端但願創建WebSocket鏈接。

咱們來看看如何在客戶端打開WebSocket鏈接:

// 建立一個加密鏈接的WebSocket
var socket = new WebSocket('ws://websocket.example.com');

這個scheme只是開始打開websocket.example.com的WebSocket鏈接的過程。

如下是初始請求頭的簡化示例:

GET ws://websocket.example.com/ HTTP/1.1
Origin: http://example.com
Connection: Upgrade
Host: websocket.example.com
Upgrade: websocket

若是服務器支持WebSocket協議,它將贊成Upgrade,並將經過響應中的Upgrade頭進行通訊。

咱們來看看如何在Node.JS中實現這個功能:

// We'll be using the https://github.com/theturtle32/WebSocket-Node
// WebSocket implementation
var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
  // process HTTP request. 
});
server.listen(1337, function() { });

// create the server
wsServer = new WebSocketServer({
  httpServer: server
});

// WebSocket server
wsServer.on('request', function(request) {
  var connection = request.accept(null, request.origin);

  // This is the most important callback for us, we'll handle
  // all messages from users here.
  connection.on('message', function(message) {
      // Process WebSocket message
  });

  connection.on('close', function(connection) {
    // Connection closes
  });
});

鏈接創建後,服務器經過Upgrade進行回覆:

HTTP/1.1 101 Switching Protocols
Date: Wed, 25 Oct 2017 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket

鏈接創建後,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鏈接。此時,任何一方均可以開始發送數據。

藉助WebSocket,您能夠爲所欲爲地傳輸儘量多的數據,而不會產生與傳統HTTP請求相關的開銷。數據經過WebSocket做爲消息傳輸,每一個消息由一個或多個包含您要發送的數據(有效負載)的幀組成。 爲了確保消息在到達客戶端時可以被正確地重建,每一個幀都以4-12字節的有效負載數據做爲前綴。 使用這種基於幀的消息傳遞系統有助於減小傳輸的非有效載荷數據量,從而顯着減小延遲。

注意:值得注意的是,一旦接收到全部幀而且原始消息有效載荷已被重建,客戶端將僅被通知關於新消息。

WebSocket URLs

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

網址具備特定schema語法。WebSocket URL特別之處在於它們不支持錨(#sample_anchor)。

對於HTTP風格的URL,相同的規則適用於WebSocket風格的URL。ws未加密,默認端口爲80,而wss須要TLS加密而且端口443爲默認端口。

成幀協議

讓咱們更深刻地瞭解組幀協議。 這是RFC爲咱們提供的內容:

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 ...                |
     +---------------------------------------------------------------+

從RFC指定的WebSocket版本開始,每一個數據包前只有一個標頭。不過,這是一個至關複雜的標題。如下是其解釋的構建塊:

  • fin(1比特):指示該幀是否構成消息的最終幀。大多數狀況下,消息適合於一個幀,而且該位老是被設置。實驗代表,Firefox在32K以後建立了第二個幀。
  • rsv1,rsv2,rsv3(每一個1位):除非擴展協商定義了非零值的含義,不然必須爲0。若是接收到非零值而且沒有任何協商的擴展定義這種非零值的含義,則接收端點必須失敗鏈接。
  • opcode(4位):說明幀表明什麼。如下值目前正在使用中:

0x00:表明繼續幀。
0x01:表明文本幀。
0x02:表明二進制幀。
0x08:該幀終止鏈接。
0x09:這個幀是一個ping。
0x0a:這個幀是一個pong。
(正如您所看到的,有足夠的值未被使用;它們已被保留供未來使用)。

  • 掩碼(1位):指示鏈接是否被屏蔽。就目前而言,從客戶端到服務器的每條消息都必須被屏蔽,而且規範會在未被屏蔽的狀況下終止鏈接。
  • payload_len(7比特):有效載荷的長度。 WebSocket幀包含如下長度括號:
    0-125表示有效載荷的長度。 126表示如下兩個字節表示長度,127表示接下來的8個字節表示長度。因此有效負載的長度在〜7位,16位和64位括號內。
  • 屏蔽鍵(32位):從客戶端發送到服務器的全部幀都被幀中包含的32位值屏蔽。
  • 有效載荷:最可能被掩蓋的實際數據。它的長度是payload_len的長度。

爲何WebSocket基於框架而不是基於流?我不知道,只是和你同樣,我很想了解更多,因此若是你有一個想法,請隨時在下面的回覆中添加評論和資源。另外,關於這個主題的討論能夠在HackerNews上找到。

關於幀的數據

如上所述,數據能夠分紅多個幀。傳輸數據的第一幀有一個操做碼,表示正在傳輸什麼類型的數據。 這是必要的,由於在規範開始時JavaScript幾乎不存在對二進制數據的支持。0x01表示utf-8編碼的文本數據,0x02是二進制數據。大多數人會傳輸JSON,在這種狀況下,您可能想要選擇文本操做碼。當你發射二進制數據時,它將在瀏覽器特定的Blob中表示。

經過WebSocket發送數據的API很是簡單:

socket.onopen = function(event) {
  socket.send('Some message'); // Sends data to server.
};

當WebSocket接收數據時(在客戶端),message事件被觸發。此事件包含一個稱爲data的屬性,可用於訪問消息的內容。

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

您可使用Chrome DevTools中的Network選項卡輕鬆瀏覽WebSocket鏈接中每一個幀中的數據:

碎片

有效載荷數據能夠分紅多個獨立的幀。接收端應該緩衝它們直到fin位置位。因此你能夠經過11個6(頭部長度)包+每一個1字節的字符串傳輸字符串「Hello World」。控制包不容許使用碎片。可是,規範要求您可以處理交錯控制幀。這是在TCP包以任意順序到達的狀況下。

合併幀的邏輯大體以下:

  • 接收第一幀
  • 記得操做碼
  • 將幀有效載荷鏈接在一塊兒,直到fin位被設置
  • 斷言每一個包的操做碼都是零

分段的主要目的是在消息啓動時容許發送未知大小的消息。經過分段,服務器能夠選擇合理大小的緩衝區,而且當緩衝區滿時,將一個片斷寫入網絡。分片的次要用例是多路複用,其中一個邏輯信道上的大消息接管整個輸出信道是不可取的,所以多路複用須要自由將消息分紅更小的片斷以更好地共享輸出渠道。

什麼是心跳?

在握手以後的任什麼時候候,客戶端或服務器均可以選擇向對方發送ping命令。當收到ping時,收件人必須儘快發回pong。這是一個心跳。您可使用它來確保客戶端仍處於鏈接狀態。

ping或pong只是一個常規幀,但它是一個控制幀。 ping具備0x9的操做碼,而且pongs具備0xA的操做碼。當你獲得一個ping以後,發回一個與ping徹底相同的Payload Data的pong(對於ping和pongs,最大有效載荷長度是125)。你也可能在沒有發送ping的狀況下獲得一個pong。若是發生,請忽略它。

心跳很是有用。有些服務(如負載均衡器)會終止空閒鏈接。另外,接收方不可能看到遠端是否已經終止。只有在下一次發送時你纔會意識到出了問題。

處理錯誤

您能夠經過監聽錯誤事件來處理髮生的任何錯誤。

它看起來像這樣:

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

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

關閉鏈接

要關閉鏈接,客戶端或服務器應發送包含操做碼0x8的數據的控制幀。一旦接收到這樣的幀,對方發送一個關閉幀做爲響應。第一個同伴而後關閉鏈接。 而後放棄關閉鏈接後收到的任何其餘數據。

這是您如何啓動從客戶端關閉WebSocket鏈接的方式:

// 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。 動詞,狀態代碼和大部分標題將保持與今天相同。HTTP/2是關於提升數據在線路上傳輸方式的效率。

如今,若是咱們比較HTTP / 2和WebSocket,咱們能夠看到不少類似之處:
http2_websocket

正如咱們上面看到的那樣,HTTP/2引入了服務器推送,它使服務器可以主動發送資源到客戶端緩存。可是,它並不容許將數據推送到客戶端應用程序自己。服務器推送只能由瀏覽器處理,而且不會在應用程序代碼中彈出,這意味着應用程序沒有API來獲取這些事件的通知。

這是Server-Sent Events(SSE)變得很是有用的地方。SSE是一種容許服務器在創建客戶端 - 服務器鏈接後將數據異步推送到客戶端的機制。只要有新的「大塊」數據可用,服務器就能夠決定發送數據。它能夠被認爲是一種單向發佈 - 訂閱模式。它還提供了一個標準的JavaScript客戶端API,名爲EventSource,在大多數現代瀏覽器中實現,做爲W3C的HTML5標準的一部分。請注意,不支持EventSource API的瀏覽器能夠很容易地被polyfilled。

因爲SSE基於HTTP,所以它很是適合HTTP/2,而且能夠結合使用以實現最佳效果:HTTP/2基於多路複用流處理高效傳輸層,SSE將API提供給應用程序以啓用推。

爲了充分理解Streams和Multiplexing是什麼,咱們首先看看IETF的定義:「stream」是在HTTP / 2鏈接中在客戶端和服務器之間交換的一個獨立的雙向幀序列。其主要特徵之一是單個HTTP/2鏈接能夠包含多個同時打開的流,其中來自多個流的端點交織幀。
multiplexing

咱們必須記住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具備優點,由於它已經以較少的開銷構建用於雙向能力(例如頭)。

假設你想構建一個Massive Multiplayer在線遊戲,須要來自鏈接兩端的大量消息。在這種狀況下,WebSockets的性能會好不少。

一般,只要須要客戶端和服務器之間的真正低延遲,近實時的鏈接,就使用WebSocket。請記住,這可能須要從新考慮如何構建服務器端應用程序,以及將焦點轉移到事件隊列等技術上。

若是您的使用案例須要顯示實時市場新聞,市場數據,聊天應用程序等,依靠HTTP/2 + SSE將爲您提供高效的雙向溝通​​渠道,同時得到留在HTTP世界的好處:

  • 當考慮到與現有Web基礎架構的兼容性時,WebSocket一般會成爲痛苦的源頭,由於它將HTTP鏈接升級到與HTTP無關的徹底不一樣的協議。
  • 規模和安全性:Web組件(防火牆,入侵檢測,負載平衡器)是以HTTP爲基礎構建,維護和配置的,這是大型/關鍵應用程序在彈性,安全性和可伸縮性方面更喜歡的環境。

另外,您必須考慮瀏覽器支持。看看WebSocket:
圖片描述

實際上至關不錯,不是嗎?

然而,HTTP/2的狀況並不相同:
圖片描述

  • 僅TLS(不是很糟糕)
  • 部分支持IE 11,但僅限於Windows 10
  • 僅在Safari中支持OSX 10.11+
  • 若是您能夠經過ALPN進行協商,則僅支持HTTP/2(您的服務器須要明確支持)

SSE支持的更好:
圖片描述

只有IE / Edge不提供支持。(好吧,Opera Mini既不支持SSE也不支持WebSocket,因此咱們能夠將其徹底排除在外)。 在IE / Edge中有一些體面的polyfills用於SSE支持。

相關文章
相關標籤/搜索