WebSocket 與 HTTP/2


個人筆記

一、WebSocket

WebSocket 是一個雙向通信協議,它在握手階段采用 HTTP/1.1 協議(暫時不支持 HTTP/2)。

握手過程如下:

  1. 首先客戶端向服務端發起一個特殊的 HTTP 請求,其消息頭如下:
GET /chat HTTP/1.1  // 請求行
Host: server.example.com
Upgrade: websocket  // required
Connection: Upgrade // required
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== // required,一個 16bits 編碼得到的 base64 串
Origin: http://example.com  // 用於防止未認證的跨域腳本使用瀏覽器 websocket api 與服務端進行通信
Sec-WebSocket-Protocol: chat, superchat  // optional, 子協議協商字段
Sec-WebSocket-Version: 13
  1. 如果服務端支持該版本的 WebSocket,會返回 101 響應,響應標頭如下:
HTTP/1.1 101 Switching Protocols  // 狀態行
Upgrade: websocket   // required
Connection: Upgrade  // required
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= // required,加密后的 Sec-WebSocket-Key
Sec-WebSocket-Protocol: chat // 表明選擇的子協議

握手完成后,接下來的 TCP 數據包就都是 WebSocket 協議的幀了。

可以看到,這里的握手不是 TCP 的握手,而是在 TCP 連接內部,從 HTTP/1.1 upgrade 到 WebSocket 的握手。

WebSocket 提供兩種協議:不加密的 ws:// 和 加密的 wss://. 因為是用 HTTP 握手,它和 HTTP 使用同樣的端口:ws 是 80(HTTP),wss 是 443(HTTPS)

在 Python 編程中,可使用 websockets 實現的異步 WebSocket 客戶端與服務端。此外 aiohttp 也提供了 WebSocket 支持。

Note:如果你搜索 Flask 的 WebScoket 插件,得到的第一個結果很可能是 Flask-SocketIO。但是 Flask-ScoektIO 使用的是它獨有的 SocketIO 協議,並不是標准的 WebSocket。只是它剛好提供與 WebSocket 相同的功能而已。

SocketIO 的優勢在於只要 Web 端使用了 SocketIO.js,就能支持該協議。而純 WS 協議,只有較新的瀏覽器才支持。對於客戶端非 Web 的情況,更好的選擇可能是使用 Flask-Sockets。

JS API

// WebSocket API
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.');
};

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

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

二、HTTP/2

HTTP/2 於 2015 年標准化,主要目的是優化性能。其特性如下:

  1. 二進制協議:HTTP/2 的消息頭使用二進制格式,而非文本格式。並且使用專門設計的 HPack 算法壓縮。
  2. 多路復用(Multiplexing):就是說 HTTP/2 可以重復使用同一個 TCP 連接,並且連接是多路的,多個請求或響應可以同時傳輸。
    • 對比之下,HTTP/1.1 的長連接也能復用 TCP 連接,但是只能串行,不能“多路”。
  3. 服務器推送:服務端能夠直接把資源推送給客戶端,當客戶端需要這些文件的時候,它已經在客戶端了。(該推送對 Web App 是隱藏的,由瀏覽器處理)
  4. HTTP/2 允許取消某個正在傳輸的數據流(通過發送 RST_STREAM 幀),而不關閉 TCP 連接。
    • 這正是二進制協議的好處之一,可以定義多種功能的數據幀。

它允許服務端將資源推送到客戶端緩存,我們訪問淘寶等網站時,經常會發現很多請求的請求頭部分會提示“provisional headers are shown”,這通常就是直接從緩存加載了資源,因此請求根本沒有被發送。觀察 Chrome Network 的 Size 列,這種請求的該字段一般都是 from disk cache 或者 from memroy cache.

Chrome 可以通過如下方式查看請求使用的協議:

2019-02-10: 使用 Chrome 查看,目前主流網站基本都已經部分使用了 HTTP/2,知乎、bilibili、GIthub 使用了 wss 協議,也有很多網站使用了 SSE(格式如 data:image/png;base64,<base64 string>
而且很多網站都有使用 HTTP/2 + QUIC,該協議的新名稱是 HTTP/3,它是基於 UDP 的 HTTP 協議。

SSE

服務端推送事件,是通過 HTTP 長連接進行信息推送的一個功能。
它首先由瀏覽器向服務端建立一個 HTTP 長連接,然后服務端不斷地通過這個長連接將消息推送給瀏覽器。JS API 如下:

// create SSE connection
var source = new EventSource('/dates');

// 連接建立時,這些 API 和 WebSocket 的很相似
source.onopen = function(event) {
  // handle open event
};

// 收到消息時(它只捕獲未命名 event)
source.onmessage = function(event) {
  var data = event.data;  // 發送過來的實際數據(string)
  var origin = event.origin;  // 服務器端URL的域名部分,即協議、域名和端口。
  var lastEventId = event.lastEventId;  // 數據的編號,由服務器端發送。如果沒有編號,這個屬性為空。
  // handle message
};

source.onerror = function(event) {
  // handle error event
};

具體的實現

在收到客戶端的 SSE 請求(HTTP 協議)時,服務端返回的響應首部如下:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

而 body 部分,SSE 定義了四種信息:

  1. data:數據欄
  2. event:自定義數據類型
  3. id :數據 id
  4. retry:最大間隔時間,超時則重新連接

body 舉例說明:

: 這種格式的消息是注釋,會被忽略\n\n
: 通常服務器每隔一段時間就會發送一個注釋,防止超時 retry\n\n

: 下面這個是一個單行數據\n\n
data: some text\n\n

: 下面這個是多行數據,在客戶端會重組成一個 data\n\n
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n

: 這是一個命名 event,只會被事件名與之相同的 listener 捕獲\n\n
event: foo\n
data: a foo event\n\n

: 未命名事件,會被 onmessage 捕獲\n\n
data: an unnamed event\n\n

event: bar\n
data: a bar event\n\n

: 這個 id 對應 event.lastEventId\n\n
id: msg1\n
data: message\n\n

WebSocket、HTTP/2 與 SSE 的比較

  1. 加密與否:

  2. 消息推送:

    • WebSocket是全雙工通道,可以雙向通信。而且消息是直接推送給 Web App.
    • SSE 只能單向串行地從服務端將數據推送給 Web App.
    • HTTP/2 雖然也支持 Server Push,但是服務器只能主動將資源推送到客戶端緩存!並不允許將數據推送到客戶端里跑的 Web App 本身。服務器推送只能由瀏覽器處理,不會在應用程序代碼中彈出服務器數據,這意味着應用程序沒有 API 來獲取這些事件的通知。
      • 為了接近實時地將數據推送給 Web App, HTTP/2 可以結合 SSE(Server-Sent Event)使用。

WebSocket 在需要接近實時雙向通信的領域,很有用武之地。而 HTTP/2 + SSE 適合用於展示實時數據。

另外在客戶端非瀏覽器的情況下,使用不加密的 HTTP/2 也是可能的。

requests 查看 HTTP 協議版本號

可以通過 resp.raw.version 得到響應的 HTTP 版本號:

>>> import requests
>>> resp = requests.get("https://zhihu.com")      
>>> resp.raw.version
11

但是 requests 默認使用 HTTP/1.1,並且不支持 HTTP/2.(不過這也不是什么大問題,HTTP/2 只是做了性能優化,用 HTTP/1.1 也就是慢一點而已。)

三、gRPC 協議

gRPC 是一個遠程過程調用框架,默認使用 protobuf3 進行數據的高效序列化與 service 定義,使用 HTTP/2 進行數據傳輸。
這里討論的是 gRPC over HTTP/2 協議。

目前 gRPC 主要被用在微服務通信中,但是因為其優越的性能,很適合游戲、loT 等需要高性能低延遲的場景下。

其實光從協議先進程度上講,gRPC 基本全面超越 REST:

  1. 使用二進制進行數據序列化,比 json 更節約流量、序列化與反序列化也更快。
  2. protobuf3 要求 api 被完全清晰的定義好,而 REST api 只能靠程序員自覺定義。
  3. gRPC 官方就支持從 api 定義生成代碼,而 REST api 需要借助 openapi-codegen 等第三方工具。
  4. 支持 4 種通信模式:一對一(unary)、客戶端流、服務端流、雙端流。更靈活

只是目前 gRPC 對 broswer 的支持仍然不是很好,如果你需要通過瀏覽器訪問 api,那 gRPC 可能不是你的菜。
如果你的產品只打算面向 App 等可控的客戶端,可以考慮上 gRPC。

對同時需要為瀏覽器和 APP 提供服務應用而言,也可以考慮 APP 使用 gRPC 協議,而瀏覽器使用 API 網關提供的 HTTP 接口,在 API 網關上進行 HTTP - gRPC 協議轉換。

gRPC over HTTP/2 定義

詳細的定義參見官方文檔 gRPC over HTTP/2.

這里是簡要說明幾點:

  1. gRPC 完全隱藏了 HTTP/2 本身的 method、headers、path 等語義,這些信息對用戶而言完全不可見了。
    1. 請求統一使用 POST,響應狀態統一為 200。只要響應是標准的 gRPC 格式,響應中的 HTTP 狀態碼將被完全忽略。
  2. gRPC 定義了自己的 status 狀態碼、格式固定的 path、還有它自己的 headers。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM