1 述
- WebSocket是一種網絡通信協議
- WebSocket 協議在2008年誕生,2011年成為國際標準。
- HTML5開始提供的一種瀏覽器與服務器進行全雙工通訊的網絡技術,屬于應用層協議。它基于TCP傳輸協議,并復用HTTP的握手通道,可以傳輸基于消息的文本和二進制數據。
- 兩端都可以隨時向另一端發送數據。
- 任何事物都不是完美的,設計限制和性能權衡始終會有,利用WebSocket 也不例外,在提供自定義數據交換協議同時,也不再享有在一些本由瀏覽器提供的服務和優化,如狀態管理、壓縮、緩存等。
2websocket對比http
2.1 雙向通信
Http 有 Keep-Alive
- HTTP1.1 默認使用持久連接(persistent connection),在一個 TCP 連接上也可以傳輸多個 Request/Response 消息對,但是 HTTP 的基本模型還是一個 Request 對應一個 Response。
- 在雙向通信(客戶端要向服務器傳送數據,同時服務器也需要實時的向客戶端傳送信息,一個聊天系統就是典型的雙向通信)時一般會使用這樣幾種解決方案:
- 1.輪詢(polling),輪詢就會造成對網絡和通信雙方的資源的浪費,且非實時。
- 2.長輪詢,?長輪詢主要是發出一個HTTP請求到服務器,然后保持連接打開以允許服務器在稍后的時間響應(由服務器確定)。
- 3.長連接HTTP 的長連接,本質上還是 Request/Response 消息對,仍然會造成資源的浪費、實時性不強等問題
2.2相同點
- 都是基于 TCP 的應用層協議
- 都使用 Request/Response 模型進行連接的建立
- 在連接的建立過程中對錯誤的處理方式相同,在這個階段 WS 可能返回和 HTTP 相同的返回碼
- 都可以在網絡中傳輸數據
2.3 不同點
- WS 使用 HTTP 來建立連接,但是定義了一系列新的 header 域,這些域在 HTTP 中并不會使用
- WS 的連接不能通過中間人來轉發,它必須是一個直接連接
- WS 連接建立之后,通信雙方都可以在任何時刻向另一方發送數據
- WS 連接建立之后,數據的傳輸使用幀來傳遞,不再需要 Request 消息
- WS 的數據幀有序
2 Socket 與 WebSocket 的關系
Socket 其實并不是一個協議,它工作在 OSI 模型會話層(第5層),是為了方便大家直接使用更底層協議(一般是 TCP或 UDP)而存在的一個抽象層。
2 和 TCP 以及 HTTP 之間的關系
WebSocket 是一個獨立的基于 TCP 的協議,它與 HTTP 之間的唯一關系就是它的握手請求可以作為一個升級請求(Upgrade request)經由 HTTP 服務器解釋(也就是可以使用 Nginx 反向代理一個 WebSocket)。
默認情況下,WebSocket 協議使用 80 端口作為一般請求的端口,端口 443 作為基于傳輸加密層連(TLS)RFC2818 接的端口
3 優點
說到優點,這里的對比參照物是HTTP協議,概括地說就是:支持雙向通信,更靈活,更高效,可擴展性更好。
具體優化如下:
- 1)支持雙向通信,實時性更強;
- 2)更好的二進制支持;
- 3)較少的控制開銷:
- 連接創建后,ws客戶端、服務端進行數據交換時,協議控制的數據包頭部較小。在不包含頭部的情況下,服務端到客戶端的包頭只有2~10字節(取決于數據包長度),客戶端到服務端的的話,需要加上額外的4字節的掩碼。而HTTP協議每次通信都需要攜帶完整的頭部;
- 4)支持擴展:
- ws協議定義了擴展,用戶可以擴展協議,或者實現自定義的子協議(比如支持自定義壓縮算法等)。
子協議
在使用 WebSocket 協議連接到一個 WebSocket 服務器時,客戶端可以指定其 Sec-WebSocket-Protocol 為其所期望采用的子協議集合,而服務端則可以在此集合中選取一個并返回給客戶端。
作為服務端,必須確保選的是客戶端握手請求中的幾個子協議中的一個:
Sec-WebSocket-Protocol: chat
4 建立連接
- WebSocket復用了HTTP的握手通道。具體指的是,客戶端通過HTTP請求與WebSocket服務端協商升級協議。協議升級完成后,后續的數據交換則遵照WebSocket的協議。
4.1 客戶端:申請協議升級
首先,客戶端發起協議升級請求。可以看到,采用的是標準的HTTP報文格式,且只支持GET方法:
GET / HTTP/1.1
Host: localhost:8080
Origin: [url=http://127.0.0.1:3000]http://127.0.0.1:3000[/url]
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
重點請求首部意義如下:
Connection: Upgrade:表示要升級協議
Upgrade: websocket:表示要升級到websocket協議。
Sec-WebSocket-Version: 13:表示websocket的版本。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,里面包含服務端支持的版本號。
Sec-WebSocket-Key:與后面服務端響應首部的Sec-WebSocket-Accept是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。
注意:上面請求省略了部分非重點請求首部。由于是標準的HTTP請求,類似Host、Origin、Cookie等請求首部會照常發送。在握手階段,可以通過相關請求首部進行 安全限制、權限校驗等。
4.2 服務端:響應協議升級
任何其他的非 101 表示 WebSocket 握手還沒有結束,客戶端需要使用原有的 HTTP 的方式去響應那些狀態碼。
服務端返回內容如下,狀態代碼101表示協議切換:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
到此完成協議升級,后續的數據交互都按照新的協議來。
備注:每個header都以\r\n結尾,并且最后一行加上一個額外的空行\r\n。此外,服務端回應的HTTP狀態碼只能在握手階段使用。過了握手階段后,就只能采用特定的錯誤碼。
4.3 Sec-WebSocket-Accept的計算
- Sec-WebSocket-Accept根據客戶端請求首部的Sec-WebSocket-Key計算出來。
計算公式為:- 1)將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接;
- 2)通過SHA1計算出摘要,并轉成base64字符串。
4.4 關閉握手
- 任意一端都可以選擇關閉握手過程。
需要關閉握手的一方通過發送一個特定的控制序列去開始一個關閉握手的過程。- 一端一旦接受到了來自另一端的請求關閉控制幀后,接收到關閉請求的一端如果還沒有返回一個作為響應的關閉幀的話,那么它需要先發送一個關閉幀。
- 在接受到了對方響應的關閉幀之后,發起關閉請求的那一端就可以關閉連接了。
- 在發送了請求關閉控制序列之后,發送請求的一端將不可以再發送其他的數據內容;
- 同樣的,一但接收到了一端的請求關閉控制序列之后,來自那一端的其他數據內容將被忽略。
- 注意這里的說的是數據內容,控制幀還是可以響應的。
- 兩邊同時發起關閉請求也是可以的。
作者:mconintet
鏈接:http://www.lxweimin.com/p/867274a5e054
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
5 數據幀格式
- 客戶端、服務端數據的交換,離不開數據幀格式的定義。因此,在實際講解數據交換之前,我們先來看下WebSocket的數據幀格式。
- WebSocket客戶端、服務端通信的最小單位是幀(frame),由1個或多個幀組成一條完整的消息(message)。
詳情如下:- 發送端:將消息切割成多個幀,并發送給服務端;
- 接收端:接收消息幀,并將關聯的幀重新組裝成完整的消息。
5.1 數據幀格式概覽
- 從左到右,單位是比特。比如FIN、RSV1各占據1比特,opcode占據4比特;
- 內容包括了標識、操作代碼、掩碼、數據、數據長度等。
5.2 數據幀格式詳解
- FIN:1個比特
如果是1,表示這是消息(message)的最后一個分片(fragment),如果是0,表示不是是消息(message)的最后一個分片(fragment)。
- RSV1, RSV2, RSV3:各占1個比特
一般情況下全為0。當客戶端、服務端協商采用WebSocket擴展時,這三個標志位可以非0,且值的含義由擴展進行定義。如果出現非零的值,且并沒有采用WebSocket擴展,連接出錯。
- Opcode: 4個比特
- 操作代碼,Opcode的值決定了應該如何解析后續的數據載荷(data payload)。如果操作代碼是不認識的,那么接收端應該斷開連接(fail the connection)。可選的操作代碼如下:
- %x0:表示一個延續幀。當Opcode為0時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;
- %x1:表示這是一個文本幀(frame);
- %x2:表示這是一個二進制幀(frame);
- %x3-7:保留的操作代碼,用于后續定義的非控制幀;
- %x8:表示連接斷開;
- %x8:表示這是一個ping操作;
- %xA:表示這是一個pong操作;
- %xB-F:保留的操作代碼,用于后續定義的控制幀。
- Mask: 1個比特
- 表示是否要對數據載荷進行掩碼操作。從客戶端向服務端發送數據時,需要對數據進行掩碼操作;從服務端向客戶端發送數據時,不需要對數據進行掩碼操作。
- 如果服務端接收到的數據沒有進行過掩碼操作,服務端需要斷開連接。
- 如果Mask是1,那么在Masking-key中會定義一個掩碼鍵(masking key),并用這個掩碼鍵來對數據載荷進行反掩碼。所有客戶端發送到服務端的數據幀,Mask都是1。
- Payload length:數據載荷的長度,單位是字節。為7位,或7+16位,或1+64位
- 假設數Payload length === x,如果:
- x為0~126:數據的長度為x字節;
- x為126:后續2個字節代表一個16位的無符號整數,該無符號整數的值為數據的長度;
- x為127:后續8個字節代表一個64位的無符號整數(最高位為0),該無符號整數的值為數據的長度。
此外,如果payload length占用了多個字節的話,payload length的二進制表達采用網絡序(big endian,重要的位在前)
- Masking-key:0或4字節(32位)
- 所有從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操作,Mask為1,且攜帶了4字節的Masking-key。如果Mask為0,則沒有Masking-key。
- 備注:載荷數據的長度,不包括mask key的長度。
- Payload data:(x+y) 字節
- 載荷數據:
- 包括了擴展數據、應用數據。其中,擴展數據x字節,應用數據y字節;
- 擴展數據:
- 如果沒有協商使用擴展的話,擴展數據數據為0字節。所有的擴展都必須聲明擴展數據的長度,或者可以如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。如果擴展數據存在,那么載荷數據長度必須將擴展數據的長度包含在內;
- 應用數據:
- 任意的應用數據,在擴展數據之后(如果存在擴展數據),占據了數據幀剩余的位置。載荷數據長度 減去 擴展數據長度,就得到應用數據的長度。
5.3 掩碼算法
- 掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數。掩碼操作不會影響數據載荷的長度。
掩碼、反掩碼操作都采用如下算法。
首先,假設:
original-octet-i:為原始數據的第i字節。
transformed-octet-i:為轉換后的數據的第i字節。
j:為i mod 4的結果。
masking-key-octet-j:為mask key第j字節。
算法描述為:
original-octet-i 與 masking-key-octet-j 異或后,得到 transformed-octet-i。
即:j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
6 數據傳遞
- 一旦WebSocket客戶端、服務端建立連接后,后續的操作都是基于數據幀的傳遞。WebSocket根據opcode來區分操作的類型。比如0x8表示斷開連接,0x0-0x2表示數據交互。
6.1 數據分片
- WebSocket的每條消息可能被切分成多個數據幀。當WebSocket的接收方收到一個數據幀時,會根據FIN的值來判斷,是否已經收到消息的最后一個數據幀。
- FIN=1表示當前數據幀為消息的最后一個數據幀,此時接收方已經收到完整的消息,可以對消息進行處理。FIN=0,則接收方還需要繼續監聽接收其余的數據幀。
- 此外,opcode在數據交換的場景下,表示的是數據的類型。0x01表示文本,0x02表示二進制。而0x00比較特殊,表示延續幀(continuation frame),顧名思義,就是完整消息對應的數據幀還沒接收完。
6.2 數據分片例子
客戶端向服務端兩次發送消息,服務端收到消息后回應客戶端,這里主要看客戶端往服務端發送的消息。
第一條消息:
FIN=1, 表示是當前消息的最后一個數據幀。服務端收到當前數據幀后,可以處理消息。opcode=0x1,表示客戶端發送的是文本類型。
第二條消息:
1)FIN=0,opcode=0x1,表示發送的是文本類型,且消息還沒發送完成,還有后續的數據幀;
2)FIN=0,opcode=0x0,表示消息還沒發送完成,還有后續的數據幀,當前的數據幀需要接在上一條數據幀之后;
3)FIN=1,opcode=0x0,表示消息已經發送完成,沒有后續的數據幀,當前的數據幀需要接在上一條數據幀之后。服務端可以將關聯的數據幀組裝成完整的消息。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
7 連接保持+心跳
- WebSocket為了保持客戶端、服務端的實時雙向通信,需要確保客戶端、服務端之間的TCP通道保持連接沒有斷開。然而,對于長時間沒有數據往來的連接,如果依舊長時間保持著,可能會浪費包括的連接資源。
- 但不排除有些場景,客戶端、服務端雖然長時間沒有數據往來,但仍需要保持連接。
- 這個時候,可以采用心跳來實現:
- 發送方->接收方:ping
- 接收方->發送方:pong
- ping、pong的操作,對應的是WebSocket的兩個控制幀,opcode分別是0x9、0xA。
8 Sec-WebSocket-Key/Accept的作用
Sec-WebSocket-Key/Sec-WebSocket-Accept在主要作用在于提供基礎的防護,減少惡意連接、意外連接。
- 作用大致歸納如下:
- 1)避免服務端收到非法的websocket連接(比如http客戶端不小心請求連接websocket服務,此時服務端可以直接拒絕連接);
- 2)確保服務端理解websocket連接。因為ws握手階段采用的是http協議,因此可能ws連接是被一個http服務器處理并返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協議。(并非百分百保險,比如總是存在那么些無聊的http服務器,光處理Sec-WebSocket-Key,但并沒有實現ws協議。。。);
- 3)用瀏覽器里發起ajax請求,設置header時,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端發送ajax請求時,意外請求協議升級(websocket upgrade);
- 4)可以防止反向代理(不理解ws協議)返回錯誤的數據。比如反向代理前后收到兩次ws連接的升級請求,反向代理把第一次請求的返回給cache住,然后第二次請求到來時直接把cache住的請求給返回(無意義的返回);
- 5)Sec-WebSocket-Key主要目的并不是確保數據的安全性,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的,而且非常簡單,最主要的作用是預防一些常見的意外情況(非故意的)。
強調:Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障,但連接是否安全、數據是否安全、客戶端/服務端是否合法的 ws客戶端、ws服務端,其實并沒有實際性的保證。
9 數據掩碼的作用
- WebSocket協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼并不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。除了加密通道本身,似乎沒有太多有效的保護通信安全的辦法。
- 那么為什么還要引入掩碼計算呢,除了增加計算機器的運算量外似乎并沒有太多的收益(這也是不少同學疑惑的點)。
- 答案還是兩個字:安全。但并不是為了防止數據泄密,而是為了防止早期版本的協議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題。
參考
http://www.lxweimin.com/p/9c09c9a75e9c
http://www.lxweimin.com/p/867274a5e054
http://www.lxweimin.com/p/fc09b0899141