1 述
- WebSocket是一種網(wǎng)絡通信協(xié)議
- WebSocket 協(xié)議在2008年誕生谚中,2011年成為國際標準。
- HTML5開始提供的一種瀏覽器與服務器進行全雙工通訊的網(wǎng)絡技術墓陈,屬于應用層協(xié)議。它基于TCP傳輸協(xié)議贡必,并復用HTTP的握手通道兔港,可以傳輸基于消息的文本和二進制數(shù)據(jù)。
- 兩端都可以隨時向另一端發(fā)送數(shù)據(jù)仔拟。
- 任何事物都不是完美的衫樊,設計限制和性能權衡始終會有,利用WebSocket 也不例外科侈,在提供自定義數(shù)據(jù)交換協(xié)議同時载佳,也不再享有在一些本由瀏覽器提供的服務和優(yōu)化臀栈,如狀態(tài)管理、壓縮权薯、緩存等。
2websocket對比http
2.1 雙向通信
Http 有 Keep-Alive
- HTTP1.1 默認使用持久連接(persistent connection)崭闲,在一個 TCP 連接上也可以傳輸多個 Request/Response 消息對,但是 HTTP 的基本模型還是一個 Request 對應一個 Response刁俭。
- 在雙向通信(客戶端要向服務器傳送數(shù)據(jù)韧涨,同時服務器也需要實時的向客戶端傳送信息,一個聊天系統(tǒng)就是典型的雙向通信)時一般會使用這樣幾種解決方案:
- 1.輪詢(polling)虑粥,輪詢就會造成對網(wǎng)絡和通信雙方的資源的浪費,且非實時娩贷。
- 2.長輪詢,?長輪詢主要是發(fā)出一個HTTP請求到服務器彬祖,然后保持連接打開以允許服務器在稍后的時間響應(由服務器確定)。
- 3.長連接HTTP 的長連接甜熔,本質(zhì)上還是 Request/Response 消息對,仍然會造成資源的浪費腔稀、實時性不強等問題
2.2相同點
- 都是基于 TCP 的應用層協(xié)議
- 都使用 Request/Response 模型進行連接的建立
- 在連接的建立過程中對錯誤的處理方式相同羽历,在這個階段 WS 可能返回和 HTTP 相同的返回碼
- 都可以在網(wǎng)絡中傳輸數(shù)據(jù)
2.3 不同點
- WS 使用 HTTP 來建立連接焊虏,但是定義了一系列新的 header 域秕磷,這些域在 HTTP 中并不會使用
- WS 的連接不能通過中間人來轉發(fā),它必須是一個直接連接
- WS 連接建立之后跳夭,通信雙方都可以在任何時刻向另一方發(fā)送數(shù)據(jù)
- WS 連接建立之后涂圆,數(shù)據(jù)的傳輸使用幀來傳遞,不再需要 Request 消息
- WS 的數(shù)據(jù)幀有序
2 Socket 與 WebSocket 的關系
Socket 其實并不是一個協(xié)議模狭,它工作在 OSI 模型會話層(第5層),是為了方便大家直接使用更底層協(xié)議(一般是 TCP或 UDP)而存在的一個抽象層嚼鹉。
2 和 TCP 以及 HTTP 之間的關系
WebSocket 是一個獨立的基于 TCP 的協(xié)議驱富,它與 HTTP 之間的唯一關系就是它的握手請求可以作為一個升級請求(Upgrade request)經(jīng)由 HTTP 服務器解釋(也就是可以使用 Nginx 反向代理一個 WebSocket)。
默認情況下褐鸥,WebSocket 協(xié)議使用 80 端口作為一般請求的端口,端口 443 作為基于傳輸加密層連(TLS)RFC2818 接的端口
3 優(yōu)點
說到優(yōu)點叫榕,這里的對比參照物是HTTP協(xié)議,概括地說就是:支持雙向通信晰绎,更靈活,更高效伶选,可擴展性更好。
具體優(yōu)化如下:
- 1)支持雙向通信仰税,實時性更強会宪;
- 2)更好的二進制支持肖卧;
- 3)較少的控制開銷:
- 連接創(chuàng)建后掸鹅,ws客戶端、服務端進行數(shù)據(jù)交換時巍沙,協(xié)議控制的數(shù)據(jù)包頭部較小葵姥。在不包含頭部的情況下句携,服務端到客戶端的包頭只有2~10字節(jié)(取決于數(shù)據(jù)包長度),客戶端到服務端的的話,需要加上額外的4字節(jié)的掩碼牍疏。而HTTP協(xié)議每次通信都需要攜帶完整的頭部拨齐;
- 4)支持擴展:
- ws協(xié)議定義了擴展鳞陨,用戶可以擴展協(xié)議瞻惋,或者實現(xiàn)自定義的子協(xié)議(比如支持自定義壓縮算法等)。
子協(xié)議
在使用 WebSocket 協(xié)議連接到一個 WebSocket 服務器時歼狼,客戶端可以指定其 Sec-WebSocket-Protocol 為其所期望采用的子協(xié)議集合,而服務端則可以在此集合中選取一個并返回給客戶端趟咆。
作為服務端梅屉,必須確保選的是客戶端握手請求中的幾個子協(xié)議中的一個:
Sec-WebSocket-Protocol: chat
4 建立連接
- WebSocket復用了HTTP的握手通道忍啸。具體指的是履植,客戶端通過HTTP請求與WebSocket服務端協(xié)商升級協(xié)議悄晃。協(xié)議升級完成后,后續(xù)的數(shù)據(jù)交換則遵照WebSocket的協(xié)議妈橄。
4.1 客戶端:申請協(xié)議升級
首先,客戶端發(fā)起協(xié)議升級請求眷蚓。可以看到叉钥,采用的是標準的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:表示要升級協(xié)議
Upgrade: websocket:表示要升級到websocket協(xié)議。
Sec-WebSocket-Version: 13:表示websocket的版本爵川。如果服務端不支持該版本,需要返回一個Sec-WebSocket-Versionheader,里面包含服務端支持的版本號值依。
Sec-WebSocket-Key:與后面服務端響應首部的Sec-WebSocket-Accept是配套的碟案,提供基本的防護愿险,比如惡意的連接蟆淀,或者無意的連接。
注意:上面請求省略了部分非重點請求首部熔任。由于是標準的HTTP請求,類似Host甫匹、Origin、Cookie等請求首部會照常發(fā)送兵迅。在握手階段薪贫,可以通過相關請求首部進行 安全限制恍箭、權限校驗等瞧省。
4.2 服務端:響應協(xié)議升級
任何其他的非 101 表示 WebSocket 握手還沒有結束,客戶端需要使用原有的 HTTP 的方式去響應那些狀態(tài)碼鞍匾。
服務端返回內(nèi)容如下,狀態(tài)代碼101表示協(xié)議切換:
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
到此完成協(xié)議升級构拳,后續(xù)的數(shù)據(jù)交互都按照新的協(xié)議來梁棠。
備注:每個header都以\r\n結尾置森,并且最后一行加上一個額外的空行\(zhòng)r\n掰茶。此外,服務端回應的HTTP狀態(tài)碼只能在握手階段使用盐碱。過了握手階段后把兔,就只能采用特定的錯誤碼瓮顽。
4.3 Sec-WebSocket-Accept的計算
- Sec-WebSocket-Accept根據(jù)客戶端請求首部的Sec-WebSocket-Key計算出來。
計算公式為:- 1)將Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接缕贡;
- 2)通過SHA1計算出摘要,并轉成base64字符串晾咪。
4.4 關閉握手
- 任意一端都可以選擇關閉握手過程。
需要關閉握手的一方通過發(fā)送一個特定的控制序列去開始一個關閉握手的過程谍倦。- 一端一旦接受到了來自另一端的請求關閉控制幀后泪勒,接收到關閉請求的一端如果還沒有返回一個作為響應的關閉幀的話,那么它需要先發(fā)送一個關閉幀圆存。
- 在接受到了對方響應的關閉幀之后,發(fā)起關閉請求的那一端就可以關閉連接了沦辙。
- 在發(fā)送了請求關閉控制序列之后,發(fā)送請求的一端將不可以再發(fā)送其他的數(shù)據(jù)內(nèi)容油讯;
- 同樣的,一但接收到了一端的請求關閉控制序列之后,來自那一端的其他數(shù)據(jù)內(nèi)容將被忽略衫冻。
- 注意這里的說的是數(shù)據(jù)內(nèi)容,控制幀還是可以響應的隅俘。
- 兩邊同時發(fā)起關閉請求也是可以的。
作者:mconintet
鏈接:http://www.reibang.com/p/867274a5e054
來源:簡書
著作權歸作者所有碌宴。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處贰镣。
5 數(shù)據(jù)幀格式
- 客戶端、服務端數(shù)據(jù)的交換碑隆,離不開數(shù)據(jù)幀格式的定義。因此上煤,在實際講解數(shù)據(jù)交換之前,我們先來看下WebSocket的數(shù)據(jù)幀格式拴疤。
- WebSocket客戶端独泞、服務端通信的最小單位是幀(frame)呐矾,由1個或多個幀組成一條完整的消息(message)阐肤。
詳情如下:- 發(fā)送端:將消息切割成多個幀,并發(fā)送給服務端孕惜;
- 接收端:接收消息幀,并將關聯(lián)的幀重新組裝成完整的消息毫炉。
5.1 數(shù)據(jù)幀格式概覽
- 從左到右削罩,單位是比特瞄勾。比如FIN弥激、RSV1各占據(jù)1比特,opcode占據(jù)4比特微服;
- 內(nèi)容包括了標識、操作代碼糙麦、掩碼、數(shù)據(jù)赡磅、數(shù)據(jù)長度等宝与。
5.2 數(shù)據(jù)幀格式詳解
- FIN:1個比特
如果是1冶匹,表示這是消息(message)的最后一個分片(fragment),如果是0徙硅,表示不是是消息(message)的最后一個分片(fragment)搞疗。
- RSV1, RSV2, RSV3:各占1個比特
一般情況下全為0。當客戶端匿乃、服務端協(xié)商采用WebSocket擴展時,這三個標志位可以非0幢炸,且值的含義由擴展進行定義。如果出現(xiàn)非零的值佛嬉,且并沒有采用WebSocket擴展,連接出錯暖呕。
- Opcode: 4個比特
- 操作代碼苞氮,Opcode的值決定了應該如何解析后續(xù)的數(shù)據(jù)載荷(data payload)湾揽。如果操作代碼是不認識的笼吟,那么接收端應該斷開連接(fail the connection)〈铮可選的操作代碼如下:
- %x0:表示一個延續(xù)幀。當Opcode為0時毫目,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片诲侮,當前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片箱蟆;
- %x1:表示這是一個文本幀(frame)沟绪;
- %x2:表示這是一個二進制幀(frame)空猜;
- %x3-7:保留的操作代碼恨旱,用于后續(xù)定義的非控制幀坝疼;
- %x8:表示連接斷開;
- %x8:表示這是一個ping操作钝凶;
- %xA:表示這是一個pong操作;
- %xB-F:保留的操作代碼耕陷,用于后續(xù)定義的控制幀。
- Mask: 1個比特
- 表示是否要對數(shù)據(jù)載荷進行掩碼操作饺蔑。從客戶端向服務端發(fā)送數(shù)據(jù)時,需要對數(shù)據(jù)進行掩碼操作猾警;從服務端向客戶端發(fā)送數(shù)據(jù)時隆敢,不需要對數(shù)據(jù)進行掩碼操作。
- 如果服務端接收到的數(shù)據(jù)沒有進行過掩碼操作筑公,服務端需要斷開連接。
- 如果Mask是1匣屡,那么在Masking-key中會定義一個掩碼鍵(masking key),并用這個掩碼鍵來對數(shù)據(jù)載荷進行反掩碼誉结。所有客戶端發(fā)送到服務端的數(shù)據(jù)幀,Mask都是1惩坑。
- Payload length:數(shù)據(jù)載荷的長度也拜,單位是字節(jié)。為7位慢哈,或7+16位,或1+64位
- 假設數(shù)Payload length === x卵贱,如果:
- x為0~126:數(shù)據(jù)的長度為x字節(jié)侣集;
- x為126:后續(xù)2個字節(jié)代表一個16位的無符號整數(shù)兰绣,該無符號整數(shù)的值為數(shù)據(jù)的長度;
- x為127:后續(xù)8個字節(jié)代表一個64位的無符號整數(shù)(最高位為0)缀辩,該無符號整數(shù)的值為數(shù)據(jù)的長度。
此外斋泄,如果payload length占用了多個字節(jié)的話,payload length的二進制表達采用網(wǎng)絡序(big endian炫掐,重要的位在前)
- Masking-key:0或4字節(jié)(32位)
- 所有從客戶端傳送到服務端的數(shù)據(jù)幀睬涧,數(shù)據(jù)載荷都進行了掩碼操作,Mask為1畦浓,且攜帶了4字節(jié)的Masking-key。如果Mask為0讶请,則沒有Masking-key。
- 備注:載荷數(shù)據(jù)的長度论巍,不包括mask key的長度风响。
- Payload data:(x+y) 字節(jié)
- 載荷數(shù)據(jù):
- 包括了擴展數(shù)據(jù)嘉汰、應用數(shù)據(jù)状勤。其中,擴展數(shù)據(jù)x字節(jié)密似,應用數(shù)據(jù)y字節(jié);
- 擴展數(shù)據(jù):
- 如果沒有協(xié)商使用擴展的話残腌,擴展數(shù)據(jù)數(shù)據(jù)為0字節(jié)。所有的擴展都必須聲明擴展數(shù)據(jù)的長度废累,或者可以如何計算出擴展數(shù)據(jù)的長度脱盲。此外,擴展如何使用必須在握手階段就協(xié)商好钱反。如果擴展數(shù)據(jù)存在,那么載荷數(shù)據(jù)長度必須將擴展數(shù)據(jù)的長度包含在內(nèi)哎壳;
- 應用數(shù)據(jù):
- 任意的應用數(shù)據(jù)尚卫,在擴展數(shù)據(jù)之后(如果存在擴展數(shù)據(jù))归榕,占據(jù)了數(shù)據(jù)幀剩余的位置吱涉。載荷數(shù)據(jù)長度 減去 擴展數(shù)據(jù)長度,就得到應用數(shù)據(jù)的長度怎爵。
5.3 掩碼算法
- 掩碼鍵(Masking-key)是由客戶端挑選出來的32位的隨機數(shù)特石。掩碼操作不會影響數(shù)據(jù)載荷的長度鳖链。
掩碼、反掩碼操作都采用如下算法逞敷。
首先著恩,假設:
original-octet-i:為原始數(shù)據(jù)的第i字節(jié)。
transformed-octet-i:為轉換后的數(shù)據(jù)的第i字節(jié)顶瞳。
j:為i mod 4的結果。
masking-key-octet-j:為mask key第j字節(jié)慨菱。
算法描述為:
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 數(shù)據(jù)傳遞
- 一旦WebSocket客戶端、服務端建立連接后畏腕,后續(xù)的操作都是基于數(shù)據(jù)幀的傳遞。WebSocket根據(jù)opcode來區(qū)分操作的類型描馅。比如0x8表示斷開連接,0x0-0x2表示數(shù)據(jù)交互铭污。
6.1 數(shù)據(jù)分片
- WebSocket的每條消息可能被切分成多個數(shù)據(jù)幀。當WebSocket的接收方收到一個數(shù)據(jù)幀時岂膳,會根據(jù)FIN的值來判斷磅网,是否已經(jīng)收到消息的最后一個數(shù)據(jù)幀。
- FIN=1表示當前數(shù)據(jù)幀為消息的最后一個數(shù)據(jù)幀知市,此時接收方已經(jīng)收到完整的消息,可以對消息進行處理嫂丙。FIN=0,則接收方還需要繼續(xù)監(jiān)聽接收其余的數(shù)據(jù)幀诽表。
- 此外,opcode在數(shù)據(jù)交換的場景下竿奏,表示的是數(shù)據(jù)的類型腥放。0x01表示文本,0x02表示二進制秃症。而0x00比較特殊,表示延續(xù)幀(continuation frame)种柑,顧名思義,就是完整消息對應的數(shù)據(jù)幀還沒接收完荠雕。
6.2 數(shù)據(jù)分片例子
客戶端向服務端兩次發(fā)送消息,服務端收到消息后回應客戶端炸卑,這里主要看客戶端往服務端發(fā)送的消息。
第一條消息:
FIN=1, 表示是當前消息的最后一個數(shù)據(jù)幀盖文。服務端收到當前數(shù)據(jù)幀后,可以處理消息。opcode=0x1蒋失,表示客戶端發(fā)送的是文本類型。
第二條消息:
1)FIN=0篙挽,opcode=0x1,表示發(fā)送的是文本類型链韭,且消息還沒發(fā)送完成煮落,還有后續(xù)的數(shù)據(jù)幀敞峭;
2)FIN=0蝉仇,opcode=0x0,表示消息還沒發(fā)送完成沉迹,還有后續(xù)的數(shù)據(jù)幀,當前的數(shù)據(jù)幀需要接在上一條數(shù)據(jù)幀之后鞭呕;
3)FIN=1宛官,opcode=0x0,表示消息已經(jīng)發(fā)送完成摘刑,沒有后續(xù)的數(shù)據(jù)幀,當前的數(shù)據(jù)幀需要接在上一條數(shù)據(jù)幀之后枷恕。服務端可以將關聯(lián)的數(shù)據(jù)幀組裝成完整的消息。
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通道保持連接沒有斷開锡搜。然而瞧掺,對于長時間沒有數(shù)據(jù)往來的連接耕餐,如果依舊長時間保持著辟狈,可能會浪費包括的連接資源。
- 但不排除有些場景明未,客戶端壹蔓、服務端雖然長時間沒有數(shù)據(jù)往來,但仍需要保持連接佣蓉。
- 這個時候,可以采用心跳來實現(xiàn):
- 發(fā)送方->接收方:ping
- 接收方->發(fā)送方: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協(xié)議擂涛,因此可能ws連接是被一個http服務器處理并返回的,此時客戶端可以通過Sec-WebSocket-Key來確保服務端認識ws協(xié)議恢暖。(并非百分百保險狰右,比如總是存在那么些無聊的http服務器,光處理Sec-WebSocket-Key挟阻,但并沒有實現(xiàn)ws協(xié)議峭弟。。瞒瘸。);
- 3)用瀏覽器里發(fā)起ajax請求省撑,設置header時俯在,Sec-WebSocket-Key以及其他相關的header是被禁止的。這樣可以避免客戶端發(fā)送ajax請求時跷乐,意外請求協(xié)議升級(websocket upgrade);
- 4)可以防止反向代理(不理解ws協(xié)議)返回錯誤的數(shù)據(jù)愕提。比如反向代理前后收到兩次ws連接的升級請求馒稍,反向代理把第一次請求的返回給cache住,然后第二次請求到來時直接把cache住的請求給返回(無意義的返回)浅侨;
- 5)Sec-WebSocket-Key主要目的并不是確保數(shù)據(jù)的安全性纽谒,因為Sec-WebSocket-Key、Sec-WebSocket-Accept的轉換計算公式是公開的如输,而且非常簡單鼓黔,最主要的作用是預防一些常見的意外情況(非故意的)央勒。
強調(diào):Sec-WebSocket-Key/Sec-WebSocket-Accept 的換算,只能帶來基本的保障请祖,但連接是否安全订歪、數(shù)據(jù)是否安全、客戶端/服務端是否合法的 ws客戶端肆捕、ws服務端刷晋,其實并沒有實際性的保證。
9 數(shù)據(jù)掩碼的作用
- WebSocket協(xié)議中慎陵,數(shù)據(jù)掩碼的作用是增強協(xié)議的安全性眼虱。但數(shù)據(jù)掩碼并不是為了保護數(shù)據(jù)本身席纽,因為算法本身是公開的捏悬,運算也不復雜。除了加密通道本身润梯,似乎沒有太多有效的保護通信安全的辦法过牙。
- 那么為什么還要引入掩碼計算呢,除了增加計算機器的運算量外似乎并沒有太多的收益(這也是不少同學疑惑的點)纺铭。
- 答案還是兩個字:安全寇钉。但并不是為了防止數(shù)據(jù)泄密,而是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊(proxy cache poisoning attacks)等問題舶赔。
參考
http://www.reibang.com/p/9c09c9a75e9c
http://www.reibang.com/p/867274a5e054
http://www.reibang.com/p/fc09b0899141