websocket 協(xié)議解析

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。生成方式如下:


  1. 將字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接到客戶端發(fā)來(lái)的Sec-WebSocket-Key的值的后面.
  2. 對(duì)拼接結(jié)果進(jìn)行sha1計(jì)算, 得到一個(gè)20個(gè)字符(原始格式)的sha1值。
  3. 將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):

websocket 數(shù)據(jù)包協(xié)議格式

它是由 數(shù)據(jù)包頭部 + 數(shù)據(jù)內(nèi)容 組成

為了更好理解,我把它的數(shù)據(jù)包頭部單獨(dú)畫(huà)成了這樣:

image.png

頭部解析

從上圖可以看出,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)題:

  1. 數(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

  1. 數(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容