公司項目使用WebSocket作為主要的請求方式,知其然也要知其所以然,會用也需要知道它的基本原理,所以寫此文章分享下自己的淺見,文章主要包括以下內(nèi)容:
- WebSocket是什么
- WebSocket和Socket區(qū)別
- 建立連接
- 數(shù)據(jù)幀格式
- 發(fā)送數(shù)據(jù)
聊天Demo代碼: github.com/madaoCN/Web… 包含tornado寫的 Server 和 Client 腳本 和 簡單ws使用實例的IOS代碼
WebSocket是什么
WebSocket是一種在單個 TCP 連接上進行 全雙工 通信的協(xié)議,WebSocket使得客戶端和服務器之間的數(shù)據(jù)交換變得更加簡單,允許服務端主動向客戶端推送數(shù)據(jù)。在WebSocket API中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸。WebSocket協(xié)議在2011年由 IETF 標準化為 RFC 6455
1. 優(yōu)勢
- 全雙工,服務器可以主動推送數(shù)據(jù)給客戶端,持久連接,實時性強
- 省流量,協(xié)議控制的數(shù)據(jù)包頭部較小,只需要進行一次完整的http握手,后續(xù)升級為WebSocket進行通信,而HTTP的header一般有幾十字節(jié),而且每次通信都需要攜帶完整的頭部
- 不僅可以發(fā)送文本,也可以發(fā)送二進制,對二進制數(shù)據(jù)比較友好
WebSocket和Socket聯(lián)系
Socket其實并不是一個協(xié)議,而是為了方便使用TCP或UDP而抽象出來的一層,是位于應用層和傳輸控制層之間的一組接口, 而WebSocket和Http一樣是屬于應用層協(xié)議。
當兩臺主機通信時,必須通過Socket連接,Socket則利用TCP/IP協(xié)議建立TCP連接。
建立連接
Websocket 握手過程復用了HTTP協(xié)議的信道,然后對服務進行升級,后續(xù)就使用Websocket協(xié)議進行通信,所以只需要一次 HTTP 握手,服務端就能一直與客戶端保持通信,直到關閉連接。
一、升級請求
GET / HTTP/1.1
Host: 192.168.1.250:6767
Sec-WebSocket-Version: 13
Upgrade: websocket
Sec-WebSocket-Key: cNtBvwgrxXtqDppb/0mcMw==
Connection: Upgrade
Origin: http://192.168.1.250:6767
與HTTP報文不太一樣的主要是 Connection: Upgrade : 標識要升級協(xié)議 Upgrade: websocket : 升級到 Websocket 協(xié)議 Sec-WebSocket-Version: 13 : 標明WebSocket協(xié)議的版本號 Sec-WebSocket-Key: cNtBvwgrxXtqDppb/0mcMw== 隨機生成的 ,與服務端響應 Sec-WebSocket-Accept 字段對應,提供基本的防護,防止惡意或者無意的連接,
二、服務端響應報文
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-Websocket-Accept: hutW70GFRNI1vI45roqiU0Lu33A=
Server: TornadoServer/5.0.2
Connection: Upgrade
Sec-Websocket-Accept : 是根據(jù)客戶端 Sec-WebSocket-Key 計算而來的
計算方法為:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
簡單的Python代碼驗證
#coding=utf8
import hashlib
import base64
if __name__ == "__main__":
sec_key = "cNtBvwgrxXtqDppb/0mcMw=="
static_key = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
contact_key = sec_key + static_key
sha1_rt = hashlib.sha1(contact_key).digest()
print sec_key
print base64.encodestring(sha1_rt)
三、抓包驗證
使用WireShark對WebSocket連接過程抓包
很直觀的可以看到 HTTP三次握手 -> 升級協(xié)議 -> 使用WebSocket通信 的過程
Websocket升級協(xié)議報文也與上文一致,大家可以自行運行Demo代碼,然后使用WireShark進行抓包驗證請求過程和報文
數(shù)據(jù)幀格式
如果我們數(shù)據(jù)幀格式都不清楚的話,更遑論說了解WebSocket協(xié)議了
數(shù)據(jù)幀概覽
數(shù)據(jù)幀(frame)是WebSocket通信的基本單位,一個消息由一個或者多個幀組成,內(nèi)容包括標志位,操作碼,掩碼,數(shù)據(jù)長度等
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
引用自 RFC 6455 - Base Framing Protocol 這個一章節(jié)
數(shù)據(jù)幀格式
- FIN1bit 表明是消息的最后一個數(shù)據(jù)幀,數(shù)據(jù)幀有可能是第一個,同時也是最后一個
- RSV1, RSV2, RSV3每個1 bit 一般都設置為0,如果客戶端和服務端采用了拓展,那么就可以為非0值,非0值的含義由拓展來決定,如果收到了非0的值,而且未采用拓展協(xié)商,那么接收終端就應該斷開WebSocket連接
- 操作碼4個bit 決定了后續(xù)的載荷數(shù)據(jù)(Payload data)的解析方式,如果收到了未知的操作碼,那么接收終端就應該斷開WebSocket連接,具體的操作碼如下:
* %x0 : 表明了這個一個持續(xù)幀(continuation frame),當操作碼為0時,說明使用了數(shù)據(jù)分片,該幀為數(shù)據(jù)分片中的一幀
* %x1 :表明了這是一個文本幀(text frame)
* %x2 :表明了這是一個二進制幀(binary frame)
* %x3-7 :保留操作碼,用于后續(xù)定義的非控制幀further non-control frames)
* %x8 :表明了這是一個關閉操作碼
* %x9 :表明了這是一個ping操作碼
* %xA :表明了這是一個pong操作碼
* %xB-F :保留操作碼, 用于后續(xù)定義的控制幀( further control frames)
復制代碼
- Mask 4個bit
表明是否要對載荷數(shù)據(jù)(Payload data)進行掩碼操作,如果被置為1,那么 Masking-key 會定義一個 32位,4個字節(jié)的值,用于對載荷數(shù)據(jù)(Payload data)的反掩碼操作,具體掩碼的算法在后續(xù)內(nèi)容中進行說明
值得注意的是,只有在客戶端向服務端發(fā)送數(shù)據(jù)時,Mask才為1,而服務端向客戶端發(fā)送數(shù)據(jù)時不需要進行掩碼操作
- Masking-key 0 或者4個bit
客戶端向服務端發(fā)送的數(shù)據(jù)都進行了掩碼操作,客戶端必須為發(fā)送的每一個frame選擇新的掩碼 (隨機生成),要求是這個掩碼無法被提供數(shù)據(jù)的終端應用(即客戶端)預測 備注:掩碼長度不計入載荷數(shù)據(jù)(Payload data)的長度
- Payload length 7 bits 或 7+16 bits 或 7+64 bits 長度為0 - 125 : 那么它就是載荷(playload)的長度 長度為126 : 那么接下來的2個字節(jié)(16位無符號整型)就是載荷(playload)的長度 長度為127 : 那么接下來的8個字節(jié)(64位無符號整型)就是載荷(playload)的長度
另外,Payload length采用了網(wǎng)絡字節(jié)序,也就是大端(big endian數(shù)據(jù)的高字節(jié)保存在內(nèi)存的低地址中),大小端具體詳情請看百度百科 大小端模式
- Payload data(x+y) 字節(jié) 載荷數(shù)據(jù)包括了應用數(shù)據(jù)(x)和拓展數(shù)據(jù)(y) 應用數(shù)據(jù) y 字節(jié): 如果存在拓展數(shù)據(jù)的話,占據(jù)了拓展數(shù)據(jù)之后的幀位置 拓展數(shù)據(jù) x字節(jié): 除非客戶端和服務端進行了協(xié)商,那么拓展數(shù)據(jù)應為0,否則拓展數(shù)據(jù)應當在握手過程中明確定義其長度,或者協(xié)商如何進行長度計算(如果存在拓展數(shù)據(jù),那么它的長度應當也計入載荷長度中)
- 掩碼的算法
/**
original-octet-i為原始數(shù)據(jù)的第i字節(jié)
masking-key-octet-j為masking-key的第j個字節(jié)
transformed-octet-i為掩碼計算后的第i個字節(jié)
*/
Octet i of the transformed data ("transformed-octet-i") is the XOR of
octet i of the original data ("original-octet-i") with octet at index
i modulo 4 of the masking key ("masking-key-octet-j"):
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
摘抄自 RFC : Client-to-Server Masking
也就是將原始數(shù)據(jù)和masking-key做異或操作(異或的位數(shù) j = i mode 4),獲得的就是轉(zhuǎn)換后的結果
發(fā)送數(shù)據(jù)
WebSocket根據(jù) opcode 操作碼來區(qū)分操作類型, %x0 %x1 %x2 代表了數(shù)據(jù)交互幀
- %x0 : 表明了這個一個持續(xù)幀(continuation frame),當操作碼為0時,說明使用了數(shù)據(jù)分片,該幀為數(shù)據(jù)分片中的一幀
- %x1 :表明了這是一個文本幀(text frame)
- %x2 :表明了這是一個二進制幀(binary frame
特別的操作碼 %x0 代表使用了數(shù)據(jù)分片,消息可能被切分成多個數(shù)據(jù)幀,筆者發(fā)現(xiàn) tornado 和 SocketRocket 并沒有實現(xiàn)數(shù)據(jù)分片,這里就暫不深入討論,詳情請參考 RFC: Fragmentation
存活心跳ping 和 pong
一旦建立與服務器的連接,客戶端和服務端都可以發(fā)起一個ping請求,當接收到一個ping請求,那么接收端必須要盡快回復pong請求,通過這種方式,來確認對方是否存活,確保客戶端、服務端之間的TCP通道保持連接沒有斷開
ping, pong操作碼分別為 %x9 %xA
斷開連接
客戶端和服務端都可以通過發(fā)送帶有特殊控制序列 %x8 的數(shù)據(jù)幀來發(fā)起斷開連接,一旦某一端收到該幀,需要也響應一個斷開幀,之后主動斷開的那端便可以關閉連接了,之前提到過Websocket 握手過程復用了HTTP協(xié)議的信道,那么很自然的,斷開連接期間也經(jīng)歷了四次揮手的過程
Sec-WebSocket-Key/Accept的作用
Sec-WebSocket-Key/Sec-WebSocket-Accept 的算法都是公開的,而且也不復雜,僅僅是提供一些基礎防護,防止一些意外鏈接,和惡意鏈接
[ WebSocket協(xié)議:5分鐘從入門到精通 ]( www.cnblogs.com/chyingp/p/w… )中已經(jīng)寫得很詳細,這邊就不做詳細探究
數(shù)據(jù)掩碼的作用
隨著websocket協(xié)議被開發(fā)出來,一項針對代理服務器的攻擊(污染那些廣泛部署的緩存代理服務器)實驗也開始進行。 一般形式的攻擊是跟被攻擊者控制的服務器建立連接,并構造一個類似WebSocket握手一樣的UPGRADE請求,隨后通過UPGRADE建立的連接發(fā)送看起來就像GET請求的frame去獲取一個已知資源(在攻擊場景中可能是一個點擊跟蹤腳本或廣告服務網(wǎng)絡中的資源) 之后遠程服務器會返回某些東西,就像對于這個偽造GET請求的響應,并且這個響應會被很多廣泛部署的網(wǎng)絡中間設備緩存,從而達到了污染緩存服務器的目的。對于這個攻擊的產(chǎn)生的效應,可能一個用戶被誘導訪問受攻擊者操控的服務器,攻擊者就有可能污染這個用戶以及其他共享相同緩存服務用戶的緩存服務器,并跨域執(zhí)行惡意腳本,破壞web安全模型
總結一下,掩碼的作用很重要兩點就是 防止攻擊者獲知網(wǎng)絡鏈路中傳輸?shù)脑紨?shù)據(jù) 和 避免緩存






