一、概述
上一篇文章《淺析一次HTTP請求》我們分析了簡單的一次 HTTP 請求具體是怎麼樣完成的,分析了 HTTP 協議的資料結構,如何連線,如何斷開,又是如何多路複用的,那麼今天我們來聊聊另外一個協議,WebSocket。由於 WebSocket 的協議的內容非常多,本文只會取其冰山一角進行簡單闡述,不會鋪開詳細說。
二、什麼是 WebSocket
2.1 WebSocket 產生的背景
在 WebSocket 協議出現以前,建立一個和服務端進雙通道通訊的 web 應用,需要依賴HTTP協議,進行不停的輪詢,這會導致一些問題:
-
服務端被迫維持來自每個客戶端的大量不同的連線
-
大量的輪詢請求會造成高開銷,比如會帶上多餘的header,造成了無用的資料傳輸。
所以,為了解決這些問題,WebSocket 協議應運而生。
2.2 WebSocket 的定義
WebSocket 是一種在單個TCP連線上進行全雙工通訊的協議。 WebSocket 使得客戶端和伺服器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料。
在 WebSocket API 中,瀏覽器和伺服器只需要完成一次握手,兩者之間就直接可以建立永續性的連線, 並進行雙向資料傳輸。(維基百科)
三、WebSocket 的基礎幀結構分析
下圖是我參考 RFC6455 5.2章節畫的websocket 基礎幀的資料結構圖,接下里我們重點解析下資料結構圖。
FIN:佔用1 bit,表示這是訊息的最後一個片段。第一個片段也有可能是最後一個片段。
RSV1,RSV2,RSV3: 每個1 bit
必須設定為0,除非擴充套件了非0值含義的擴充套件。如果收到了一個非0值但是沒有擴充套件任何非0值的含義,接收終端必須斷開WebSocket連線。
Opcode: 4 bit,操作碼,如果收到一個未知的操作碼,接收終端必須斷開WebSocket連線。
%x0 表示一個持續幀
%x1 表示一個文字幀
%x2 表示一個二進位制幀
%x3-7 預留給以後的非控制幀
%x8 表示一個連線關閉包
%x9 表示一個ping包
%xA 表示一個pong包
%xB-F 預留給以後的控制幀
Mask: 1 bit,mask標誌位,定義“有效負載資料”是否新增掩碼。如果設定為1,那麼掩碼的鍵值存在於Masking-Key中。
Payload length: 7 bits, 7+16 bits, or 7+64 bits,以位元組為單位的“有效負載資料”長度。
Masking-Key: 0 or 4 bytes,
所有從客戶端發往服務端的資料幀都已經與一個包含在這一幀中的32 bit的掩碼進行過了運算。如果mask標誌位(1 bit)為1,那麼這個欄位存在,如果標誌位為0,那麼這個欄位不存在。 備註:載荷資料的長度,不包括mask key的長度。。
Payload data: 有效負載資料
為什麼需要掩碼?
為了安全,但並不是為了防止資料洩密,而是為了防止早期版本的協議中存在的代理快取汙染攻擊(proxy cache poisoning attacks)等問題。
四、 抓包分析
4.1 DEMO展示及分析
我寫了一個DMEMO用來抓包分析 websocket,原始碼會放在文章末尾的連結。DEMO效果如下:
頁面提供連線與斷開功能,輸入自己的名字傳送,服務端返回Hello,名字!功能很簡單,我們先看看頁面的請求和響應。
請求:
響應:
這裡的請求與響應就是反應了 WebSocket 的一次握手,我們根據上圖可以簡單抽象一下 WebSocket 的請求和響應格式: 客戶端握手請求格式:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
複製程式碼
服務端握手響應:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
複製程式碼
我們重點說明下結果請求欄位:
Upgrade:表示HTTP協議升級為webSocket
connection:Upgrade 請求升級。
Sec-WebSocket-Key: 用於服務端進行標識認證,生成全域性唯一id,GUID。
Sec-WebSocket-Version: 版本
Sec-WebSocket-Protocol: 請求服務端使用指定的子協議。如果指定了這個欄位,伺服器需要包含相同的欄位,並且從子協議的之中選擇一個值作為建立連線的響應。
Sec-WebSocket-Extensions: WebSocket的擴充套件。
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 生成的全域性唯一id,GUID。
GUID的生成演算法
演算法思想:通過 Sec-WebSocket-Key 傳入的 值,dGhlIHNhbXBsZSBub25jZQ==,連線服務端生成的字串,拼接格式如下
dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-
C5AB0DC85B11
複製程式碼
, 然後採用SHA-1雜湊演算法,然後用base64編碼生成最終的 Sec-WebSocket-Accept的值,生成的值就是
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
複製程式碼
(注意,這裡SHA1雜湊演算法生成的結果必須是二進位制的雜湊結果,比如
Python程式碼中的
h = hashlib.sha1("dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
.digest()
複製程式碼
,如果用線上處理工具生成,生成的Hash是16進位制的雜湊,用 Base64就會生成錯誤結果)。
4.2 抓包
我在DEMO中的操作流程如下:
- 連線WebSocket
- 傳送“LUOZHOU”
- 斷開連線
用 Wireshark 抓包如下:
我們結合瀏覽器截圖和抓包截圖,發現在真正開啟 websocket 之前,瀏覽器會有兩次http請求,分別是:A請求 GET /gs-guide-websocket/info?t=1551252237372 HTTP/1.1
B請求 GET /gs-guide-websocket/690/pdsz5x1q/websocket HTTP/1.1
複製程式碼
根據 RFC6455 協議規定 WebSocket 只需要一次握手就可以完成,所以我們只需要分析第二次的http 握手請求,A請求應該是使用的框架層面自己實現。
我們根據截圖可以知道,B請求對應的響應是序號 192 的資料,返回碼是101,根據 HTTP 返回碼我們可以知道,伺服器已經理解了客戶端的請求,並將通過Upgrade 訊息頭通知客戶端採用不同的協議來完成這個請求。在傳送完這個響應最後的空行後,伺服器將會切換到在 Upgrade 訊息頭中定義的那些協議,也就是升級為 WebSocket 協議。所以接著193的包已經變成了 WebSocket 協議了。到這裡,WebSocket 的握手連線就已經完成了。
接下來我們分析下傳送訊息的流程,這裡大家肯定會疑惑,就傳送了一條訊息,為啥會有這麼多 WebSocket 的包呢?其實這裡多餘的包是框架層面進行傳送的,比如要進行訂閱與釋出的註冊等等操作。所以真正使我們操作的包就只有斷開連線的相關包和傳送“LUOZHOU”的包
根據上圖我們發現 序號229的包是一個文字型別的包,opcode:1
,然後採用了掩碼處理,同時是最後一個處理包。我們仔細發現所有客戶端傳送服務端的包都會有[MASKED]標記,服務端返回的沒有,這就說明了從客戶端向服務端傳送資料時,需要對資料進行掩碼操作;從服務端向客戶端傳送資料時,不需要對資料進行掩碼操作。
五、總結
-
WebSocket 是為了在 web 應用上進行雙通道通訊而產生的協議,相比於輪詢HTTP請求的方式,WebSocket 有節省伺服器資源,效率高等優點。
-
WebSocket 中的掩碼是為了防止早期版本中存在中間快取汙染攻擊等問題而設定的,客戶端向服務端傳送資料需要掩碼,服務端向客戶端傳送資料不需要掩碼。
-
WebSocket 中 Sec-WebSocket-Key 的生成演算法是拼接服務端和客戶端生成的字串,進行SHA1雜湊演算法,再用base64編碼。
-
WebSocket 協議握手是依靠 HTTP 協議的,依靠於 HTTP 響應101進行協議升級轉換。
六、參考
[1]RFC6455