websocket 協(xié)議是在tcp協(xié)議只上建立的數(shù)據(jù)傳輸協(xié)議。它也跟tcp協(xié)議一樣有個(gè)握手的過(guò)程, 但是它的握手過(guò)程是在http協(xié)議下進(jìn)行的。握手成功之后和服務(wù)器建立連接, 之后通過(guò)websocket的數(shù)據(jù)包協(xié)議進(jìn)行通信。如下圖:

其中1和2是握手,使用http協(xié)議進(jìn)行。3和4是數(shù)據(jù)交互,基于websocket數(shù)據(jù)包協(xié)議進(jìn)行。
握手
- 客戶端發(fā)送
首先客戶端通過(guò)tcp連接到服務(wù)器,然后發(fā)送http請(qǐng)求,請(qǐng)求只有請(qǐng)求頭,沒(méi)有正文。請(qǐng)求頭如下:
GET ws://{host}:{post}/ HTTP/1.1
Host: {host}:{port}
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: xxxxxxxx
其中{host}是主機(jī)地址或者域名, {post}是端口, 如果是80端口, 默認(rèn)可以省略。
Connection的值必須是Upgrade, Upgrade的值必須是websocket, 表示將當(dāng)前連接升級(jí)到websocket連接。
Sec-WebSocket-Version 是websocket的版本號(hào)
Sec-WebSocket-Key是客戶端生成的一個(gè)key,這個(gè)key服務(wù)器響應(yīng)的時(shí)候必須通過(guò)它和固定的算法生成一個(gè)新的key返回給客戶端。客戶端校驗(yàn)通過(guò)后才能建立連接。
- 服務(wù)端回復(fù)
服務(wù)端收到客戶端的連接升級(jí)請(qǐng)求之后,響應(yīng)如下http協(xié)議表示同意此次升級(jí):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aaaaaaa
HTTP/1.1 101 Switching Protocols是固定的, 表示切換協(xié)議, 接下來(lái)得到交互將用websocket數(shù)據(jù)包的協(xié)議進(jìn)行。
Connection的值必須是Upgrade, Upgrade的值必須是websocket。跟客戶端發(fā)送的一樣。
Sec-WebSocket-Accept 這個(gè)就是用客戶端發(fā)送的 Sec-WebSocket-Key和固定算法生成的一個(gè)key。生成方式如下:
- 將字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接到客戶端發(fā)來(lái)的Sec-WebSocket-Key的值的后面. - 對(duì)拼接結(jié)果進(jìn)行sha1計(jì)算, 得到一個(gè)20個(gè)字符(原始格式)的sha1值。
- 將sha1的結(jié)果進(jìn)行base64編碼得到對(duì)于的key
用php代碼實(shí)現(xiàn)如下:
base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))
至此,握手階段結(jié)束,接下來(lái)的數(shù)據(jù)交互將通過(guò)websocket的數(shù)據(jù)包協(xié)議進(jìn)行。
websocket 數(shù)據(jù)包協(xié)議
官方給出的協(xié)議格式如下(RFC6455):

它是由 數(shù)據(jù)包頭部 + 數(shù)據(jù)內(nèi)容 組成
為了更好理解,我把它的數(shù)據(jù)包頭部單獨(dú)畫(huà)成了這樣:

頭部解析
從上圖可以看出,websocket數(shù)據(jù)包的頭部是變長(zhǎng)的, 由2-12個(gè)字節(jié)組成。
數(shù)據(jù)包的第一個(gè)字節(jié),包含了5個(gè)值, 分別是FIN、RSV1、RSV2、RSV3、opcode,其代表的含義如下:
FIN: 占用1位(bit), 取值0或1, 它是用來(lái)標(biāo)記是否為最終包, 也就是說(shuō)如果FIN為0, 則表示這個(gè)是分包, FIN為1, 表示最終包。例如,收到4個(gè)包,其FIN分別為 0,0,0,1則實(shí)際收到的內(nèi)容必須用這四個(gè)包的內(nèi)容合并。
RSV1、RSV2、RSV3:這三個(gè)值各占1位(bit),是保留字段,不使用都填充為0
opcode:操作碼,占4位,取值范圍是 0-15,十六進(jìn)制為0x0-0xF。其取值如下(十進(jìn)制):
-
0:標(biāo)識(shí)一個(gè)中間數(shù)據(jù)包 -
1:標(biāo)識(shí)一個(gè)text類(lèi)型數(shù)據(jù)包 -
2:標(biāo)識(shí)一個(gè)binary類(lèi)型數(shù)據(jù)包 -
3-7:保留 -
8:標(biāo)識(shí)一個(gè)斷開(kāi)連接類(lèi)型數(shù)據(jù)包 -
9:標(biāo)識(shí)一個(gè)ping類(lèi)型數(shù)據(jù)包 -
10:表示一個(gè)pong類(lèi)型數(shù)據(jù)包 -
11-15:保留
數(shù)據(jù)包的第二個(gè)字節(jié)包含了2個(gè)值, 分別是MASK、payload length。
MASK 占1位,取值為0或1,這個(gè)值如果是1表示對(duì)payload數(shù)據(jù)(也就是數(shù)據(jù)包的數(shù)據(jù)內(nèi)容部分)進(jìn)行mask計(jì)算(用mask key對(duì)數(shù)據(jù)進(jìn)行異或運(yùn)算)。
payload length 就是數(shù)據(jù)內(nèi)容的長(zhǎng)度,或者長(zhǎng)度標(biāo)記, 這里的payload length占7位,取值范圍為0-127。 這其中如果是小于126, 表示是數(shù)據(jù)內(nèi)容的長(zhǎng)度。如果是126,表示第3、4字節(jié)(雙字節(jié),最大值65535)用來(lái)存儲(chǔ)數(shù)據(jù)內(nèi)容的長(zhǎng)度。如果是127,表示第3、4、5、6、7、8、9、10(八字節(jié))用來(lái)存儲(chǔ)數(shù)據(jù)內(nèi)容的長(zhǎng)度。
payload length之后會(huì)有0或4個(gè)字節(jié)來(lái)表示mask key的值,該值取決于MASK標(biāo)記是否為1。只有MASK為1才會(huì)用4個(gè)字節(jié)來(lái)存儲(chǔ)mask key
mask計(jì)算用php代碼表示如下:
//mask轉(zhuǎn)換
if($mask) {
$maskKeyAry = array_map(function ($r) {return ord($r);}, str_split($maskKey, 1));
for($i=0; $i<$payloadLength; $i++) {
$payload[$i] = chr(ord($payload[$i]) ^ $maskKeyAry[$i%4]);
}
}
數(shù)據(jù)內(nèi)容解析
如果頭部沒(méi)有標(biāo)明使用了mask,則數(shù)據(jù)內(nèi)容為原數(shù)據(jù)內(nèi)容,不需要做任何轉(zhuǎn)換,直接截取。
在數(shù)據(jù)的接收過(guò)程中需要處理的以下問(wèn)題:
- 數(shù)據(jù)包的完整性
因?yàn)閣ebsocket是基于tcp協(xié)議的。所以一個(gè)websocket數(shù)據(jù)包在發(fā)送的時(shí)候,也是有可能出現(xiàn)tcp的多包形式,也就是說(shuō)如果websocket數(shù)據(jù)包比較大,在接收的時(shí)候可能并不是收到一次,可能收到多次這個(gè)數(shù)據(jù)包的片段。所以這里在接收的過(guò)程中需要對(duì)數(shù)據(jù)包進(jìn)行完整性確定。
例如: 有個(gè)數(shù)據(jù)包是這樣的
[頭部]1234567890abcdefg
通過(guò)tcp接收可能收到這樣的幾個(gè)包
[頭部]123
4567890abc
defg
如果第一個(gè)包直接解析,可以解析出內(nèi)容123。但是第二個(gè)數(shù)據(jù)包和第三個(gè)數(shù)據(jù)包就無(wú)法通過(guò)websocket數(shù)據(jù)包協(xié)議進(jìn)行解析,因?yàn)槿鄙倭祟^部。
所以解析websocket數(shù)據(jù)包時(shí), 需要處理數(shù)據(jù)包的完整性問(wèn)題,接收到數(shù)據(jù)包完整后再進(jìn)行websocket數(shù)據(jù)包協(xié)議的解析。
完整性可以通過(guò)以下方法進(jìn)行:
- 如果收到包之后沒(méi)有解析到
payload length,則繼續(xù)等待數(shù)據(jù) - 如果解析到了
payload length,則判斷數(shù)據(jù)內(nèi)容長(zhǎng)度是否足夠, 當(dāng)足夠時(shí)再進(jìn)行解析. - 當(dāng)數(shù)據(jù)包足夠時(shí), 將剩余的字符當(dāng)做下一個(gè)wensocket數(shù)據(jù)包協(xié)議進(jìn)行解析
因?yàn)樵诙鄠€(gè)數(shù)據(jù)包發(fā)送過(guò)來(lái)的時(shí)候, 有可能出現(xiàn)如下的情況:
[頭部]1234567890abcdefg[頭部]99i2i2i[頭部]123ioi2o3i
2389oeowioei[頭部]wiieowieoiwoe
- 數(shù)據(jù)的完整性
websocket是支持分包的。當(dāng)頭部的FIN為0時(shí), 表示是一個(gè)分包,遇到分包解析到數(shù)據(jù)之后,需要等待到一個(gè)FIN為1的包(最終包), 并解析數(shù)據(jù)。分包數(shù)據(jù)和最終包的數(shù)據(jù)合并才是一個(gè)完整的數(shù)據(jù)。
數(shù)據(jù)包示例如下(4條websocket消息):
[頭部-分包]1234567890abcdefg[頭部-分包]99i2i2i[頭部-終包]123ioi2o3i[頭部-分包]99i2i2i[頭部-終包]123ioi2o3i[頭部-終包]123ioi2o3[頭部-終包]123ioi2o3