簡介 WebSocket 是雙工的,他支持在客戶端和伺服器之間互相發送文本或二進位消息流,除此功能以外,它還提供了更為複雜的附加擴展: 連接協商和同源策略實施 與現有HTTP基礎設施的互相操作性 面向消息的通信和高效的消息框架 這一點與Socket不同,Socket算是面向位元組,他沒有消息頭、消息尾 ...
簡介
WebSocket 是雙工的,他支持在客戶端和伺服器之間互相發送文本或二進位消息流,除此功能以外,它還提供了更為複雜的附加擴展:
-
連接協商和同源策略實施
-
與現有HTTP基礎設施的互相操作性
-
面向消息的通信和高效的消息框架
這一點與Socket不同,Socket算是面向位元組,他沒有消息頭、消息尾的概念。可以說Socket沒有那麼聰明
-
子協議協商和可擴展性
值得註意的一點是:WebSocket 不是 HTTP、XHR 或 SSE 的替代品,為了獲得最佳性能,利用每種傳輸的優勢至關重要。
WS 和 WSS URL 方案
這兩種方案都是WebSocket的自定義方案,WS用於純文本(即:明文)通訊,WSS用於加密管道通訊。
文本消息和二進位消息
WebSocket比較好的一點就是:無需擔心緩衝、解析和重構接收到的數據。
例:如果伺服器發送 1 MB 的有效負載,客戶端每次只能接收250 kb的有效荷載,那麼他會接收四次,當接收完畢後onmessage
才會在客戶端調用應用程式的回調。
有效負載:我有個長度為5 MB的位元組容器(即:一個int類型的數組),我要發送的消息轉為位元組數組後,長度只有1 MB,那麼這1 MB稱為有效負載,剩下的將是無效負載
有效載荷:客戶端每次只能接收250 kb(即:一個長度為250*1024的位元組數組),如果接收到了200 kb的數據,那麼200有效載荷 50無效載荷
性能
瀏覽器接收到一條新消息時,它會自動轉換為基於文本數據的 DOMString 對象或二進位數據的 Blob 對象,作為客戶端性能提升和優化的唯一方法就是,告訴瀏覽器將收到的二進位數據轉換為ArrayBuffer
而不是Blob
var ws = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer";
Blob:離線存在磁碟中或單獨存在記憶體中
Blob 對象表示不可變的原始數據的類文件對象,如果您不需要改動二進位數據(例如我只需要這麼一個Blob文件對象),那麼這是最好的選擇。
ArrayBuffer:可能更有效將數據保存在記憶體中
如果您需要對二進位數據進行額外的處理,那麼ArrayBuffer可能更合適。
ArrayBuffer 是一個結構化的,二進位數據的固定長度容器
關於發送文本和二進位數據
WebSocket 連接成功後,客戶端將是一個雙向通訊管道,它允許通過同一個 TCP 連接在兩個方向上傳遞消息,發送或接收 UTF-8 和二進位消息。
預設Send方法,接受一個 DOMString 對象,該對象線上路上被編碼為 UTF-8,當然也可以用於二進位傳輸的 ArrayBuffer、ArrayBufferView 或 Blob 對象之一。
但是請註意,後一種二進位的方式只是為了方便API,在網路層面中,WebSocket的數據幀(即:發送的數據包),會通過單個位元組位標記為二進位或文本,因此,如果應用程式想要使用其他類型的信息,那麼雙方必須約定一種新的機制來通訊該數據。
關於Send方法
Send方法是非同步的,將提供的數據由客戶端排隊發出,立即返回結果,這裡的立即返回結果不代表你的信息已經發送完了
,真正的發送完成
是要監控當前瀏覽器的排隊數據量
var ws = new WebSocket('wss://example.com/socket');
ws.onopen = function () {
// 當應用更新時觸發
subscribeToApplicationUpdates(function(evt) {
// 如果待發送的位元組數為0
if (ws.bufferedAmount == 0)
// 發送下一次請求
ws.send(evt.data);
});
};
所有 WebSocket 消息都按照它們在客戶端排隊的確切順序進行傳遞!!!
大量的排隊消息,或者單個大消息都將延遲其後面排隊的消息的傳遞——隊列頭阻塞!
解決方案:
- 大消息拆分成更小的塊
- 實現自己的優先順序隊列
應用程式應該密切關註每種類型的消息如何以及何時在套接字上排隊!
子協議協商
WebSocket 協議的預設消息格式只有兩種,文本數據和二進位數據,以便客戶端和服務端可以有效的對其進行編碼,如果不屬於這兩種,消息內容將是不透明的,服務端和客戶端將會不認識,導致無法解釋其內容。
WebSocket 與 HTTP 或 XHR請求不同,它們通過Header傳遞額外的信息,而WebSocket沒有這樣的協議,因此如果想要獲取額外的數據信息,那麼可以通過下麵的方式:
-
統一的JSON編碼或自定義的二進位數據來通訊
-
如果想要傳輸不同格式的數據,那麼可以通過約定消息頭
-
文本和二進位消息混合使用
原始的WebSocket提供了子協議協商
API來解決這個問題,最開始連接時,客戶端可以告訴伺服器他支持的協議,例:
var ws = new WebSocket('wss://example.com/socket',
['appProtocol', 'appProtocol-v2'])
如果子協議協商成功,則在 onopen
處觸發回調,應用程式可以查詢WebSocket實例上的protocol
屬性來確定伺服器選擇的協議,如果協商不成功,即伺服器不支持,則代表WebSocket協商是不完整的,將調用onerror
回調。
WebSocket的協議
WebSocket協議由兩個高級組件組成:
- 用於協商連接參數的開放 HTTP 握手
- 一種二進位消息框架機制,允許文本和二進位數據的低開銷、基於消息的交付
WebSocket 協議是一個功能齊全的獨立協議,可以在瀏覽器之外使用。話雖如此,它的主要應用程式是作為基於瀏覽器的應用程式的雙向傳輸。
二進位框架層
WebSocket 使用了一種自定義的二進位幀格式,它將每個應用程式消息拆分為一個或多個幀,將它們傳輸到目的地,重新組裝它們,最後在收到整個消息後通知接收者。
-
幀
通信的最小單元,每一單元包含一個變長幀報頭和一個可以承載全部或部分應用程式消息的有效載荷。
-
消息
映射到邏輯應用程式消息的一個完整的幀序列。
我們瞭解一下WebSocket的幀
是怎麼運行的?
我們先來看一下幀的內容:
幀是一個32位數據包(即:32-bit,一次處理4位元組,1位元組8位),我們來由淺入深一下:
比如a
在ASCII編碼中代表著97
,那麼97
也就是所謂的位元組,然後再通過toString(2)
得到01100001
,這是一個8 bit(即:8位)的位數據,所謂的幀
就是這樣的一堆二進位(也就是前面提到的8 bit)組成的。
現在我們來看一下伺服器發送給客戶端的原始數據,伺服器發送給客戶端的信息是兩個簡單的字元:aa
10000001 00000010 01100001 01100001
這是四位元組,你可以嘗試用後兩個位元組 01100001 01100001 去二進位轉換字元串(但是在轉換時您需要將它拼接起來,0110000101100001),你將得到信息aa
那麼前面兩個位元組中包含著什麼呢?
只是在目前的例子中他是占用了兩個位元組,其他情況下請參考數據內容
我們來看第一個位元組(10000001)
-
1
FIN:表示該數據包是不是消息的最後一個數據包(也就是說,如果他不是1,那麼表示數據包還沒有傳輸完成)
-
000,分別代表RSV1、RSV2、RSV3,
這三位必須是0,除非與伺服器協商了該擴展,如果這三個都不為0,那麼伺服器應該立即終止鏈接
-
0001
這四位代表操作碼
- 0000:代表連續的幀
- 0001:代表文本幀
- 0010:代表二進位幀
- 0011-0111:保留的幀,一般碰不到
- 1000:代表連接要關閉
- 1001:表示ping
- 1010:表示pong
- 1011-1111:保留的幀
我們接下來拆分第二個位元組(00000010)
-
0
掩碼,如果該位為1,那麼後面會有一個掩碼秘鑰,秘鑰占4位元組
-
0000010
這七位代表有效的載荷長度,轉換為數字為:2,也就是對應的
aa
的長度,在這裡如果轉換為的數字為0-125,那麼這就是應用數據的有效載荷長度。如果為126,那麼應該加上往後的擴展載荷長度,此情況下,擴展載荷長度為2個位元組。
如果為127,那麼擴展載荷長度為8個位元組。
如果有擴展的長度,那麼就需要將這些長度的位拼接起來,轉換為一個數字,該數字為真正的有效載荷的長度。
向後可能會出現的所屬於框架的位元組
-
0或4位元組
該項為掩碼秘鑰,用於屏蔽有效負載數據,即加密,此加密在本文下方有解釋。
-
有效載荷數據:X+Y
即擴展數據加應用數據
-
擴展數據
該數據應該是在創建連接(即:握手時)就應該協商好的,預設為0位元組,除非協商了擴展,以及定義好了擴展的長度,或長度應該如何計算。
-
應用數據
在擴展數據後的幀將是應用數據
掩碼、中介執行緩存中毒攻擊
掩碼是為了防止中介執行緩存中毒攻擊,該攻擊方式重點在於中介,此處的中介也就是一個代理伺服器,假設他是一個負載均衡代理,那麼他將負責轉發流量,我們知道WebSocket和HTTP並不相同,WebSocket發送到是原始位元組,這可以使其發送任何內容,比如模仿一個http請求:
soc.send(`
GET /script.js HTTP/1.1
HOST:惡意站點
`)
當這樣的一個消息,發送至中介伺服器時,中介伺服器並不知道這是一個WebSocket的數據封包,它將會被認為是HTTP請求,這將轉發到惡意站點,此時如果有相應的緩存設置,那麼接下來,訪問該安全站點 script.js 文件的無辜用戶,將會獲取到惡意站點的 script.js 文件。
而掩碼將會將 WebSocket 發送出去的數據進行屏蔽(加密),屏蔽完的數據他將不是明文的,而是看上去像是亂碼的數據。
至此,WebSocket的基本原理就是這樣,以上全文均為本人個人理解,如果有誤還請各位dai佬們指出。
接下來我會抽空將C#編寫的簡易WebSocketClient做成博客發出來。
第一次發博客,輕點噴,在此求求各位大佬了~