個人筆記
一、WebSocket
WebSocket 是一個雙向通信協議,它在握手階段采用 HTTP/1.1 協議(暫時不支持 HTTP/2)。
握手過程如下:
- 首先客戶端向服務端發起一個特殊的 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
- 如果服務端支持該版本的 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 年標准化,主要目的是優化性能。其特性如下:
- 二進制協議:HTTP/2 的消息頭使用二進制格式,而非文本格式。並且使用專門設計的 HPack 算法壓縮。
- 多路復用(Multiplexing):就是說 HTTP/2 可以重復使用同一個 TCP 連接,並且連接是多路的,多個請求或響應可以同時傳輸。
- 對比之下,HTTP/1.1 的長連接也能復用 TCP 連接,但是只能串行,不能“多路”。
- 服務器推送:服務端能夠直接把資源推送給客戶端,當客戶端需要這些文件的時候,它已經在客戶端了。(該推送對 Web App 是隱藏的,由瀏覽器處理)
- 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 定義了四種信息:
data
:數據欄event
:自定義數據類型id
:數據 idretry
:最大間隔時間,超時則重新連接
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 的比較
-
加密與否:
- WebSocket 支持明文通信
ws://
和加密wss://
, - 而 HTTP/2 協議雖然沒有規定必須加密,但是主流瀏覽器都只支持 HTTP/2 over TLS.
- SSE 是使用的 HTTP 協議通信,支持 http/https
- WebSocket 支持明文通信
-
消息推送:
- 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:
- 使用二進制進行數據序列化,比 json 更節約流量、序列化與反序列化也更快。
- protobuf3 要求 api 被完全清晰的定義好,而 REST api 只能靠程序員自覺定義。
- gRPC 官方就支持從 api 定義生成代碼,而 REST api 需要借助 openapi-codegen 等第三方工具。
- 支持 4 種通信模式:一對一(unary)、客戶端流、服務端流、雙端流。更靈活
只是目前 gRPC 對 broswer 的支持仍然不是很好,如果你需要通過瀏覽器訪問 api,那 gRPC 可能不是你的菜。
如果你的產品只打算面向 App 等可控的客戶端,可以考慮上 gRPC。
對同時需要為瀏覽器和 APP 提供服務應用而言,也可以考慮 APP 使用 gRPC 協議,而瀏覽器使用 API 網關提供的 HTTP 接口,在 API 網關上進行 HTTP - gRPC 協議轉換。
gRPC over HTTP/2 定義
詳細的定義參見官方文檔 gRPC over HTTP/2.
這里是簡要說明幾點:
- gRPC 完全隱藏了 HTTP/2 本身的 method、headers、path 等語義,這些信息對用戶而言完全不可見了。
- 請求統一使用 POST,響應狀態統一為 200。只要響應是標准的 gRPC 格式,響應中的 HTTP 狀態碼將被完全忽略。
- gRPC 定義了自己的 status 狀態碼、格式固定的 path、還有它自己的 headers。
參考
- 深入探索 WebSockets 和 HTTP/2
- HTTP/2 特性與抓包分析
- SSE:服務器發送事件,使用長鏈接進行通訊
- Using server-sent events - MDN
- Can I Use HTTP/2 on Browsers
- Python 3.x how to get http version (using requests library)
- WebSocket 是什么原理?
- 原生模塊打造一個簡單的 WebSocket 服務器
- Google Cloud - API design: Understanding gRPC, OpenAPI and REST and when to use them