序
近期工作忙碌,為了趕SegmentFault for Android 4.0版,到了發(fā)瘋的程度。
我來(lái)匯報(bào)一個(gè)進(jìn)度,已經(jīng)實(shí)現(xiàn)基于websocket的私信系統(tǒng)了,多虧了70大大不懈的努力,在不久的將來(lái),我們使用web給手機(jī)發(fā)送私信的愿望很快就可以達(dá)成了。
那么在這之余,因?yàn)閭€(gè)人對(duì)各個(gè)通信協(xié)議都有頗有興趣,便順便去看了下ietf寫的關(guān)于websocket的文章,原文鏈接在這:
https://tools.ietf.org/html/rfc6455
當(dāng)然如果你對(duì)實(shí)現(xiàn)websocket協(xié)議并沒(méi)什么興趣的話,本文可以直接略過(guò),因?yàn)槟愦蟾胖涝趺从镁涂梢粤? =
概念與背景
websocket 目前仍未有標(biāo)準(zhǔn)化的文檔,所以目前我的理解是基于RFC6455草案。
websocket的誕生場(chǎng)景是因?yàn)槲覀冊(cè)跒g覽器上缺少一種與服務(wù)器保持長(zhǎng)連接的標(biāo)準(zhǔn)化的技術(shù),因?yàn)?code>HTTP 1.1(以下簡(jiǎn)稱為HTTP)協(xié)議只是一個(gè)標(biāo)準(zhǔn)的無(wú)狀態(tài)協(xié)議,并不存在除了request和response生命周期之外的通信場(chǎng)景。如果我們需要實(shí)現(xiàn)在線聊天的功能的話,那么基于HTTP,我們只能使用丑陋的long polling和ajax 輪詢等非正常的技術(shù)實(shí)現(xiàn)我們需要的功能。這些方案雖然解決了我們的使用場(chǎng)景,但并不是最好的方案,我們的HTTP服務(wù)器軟件并不是設(shè)計(jì)來(lái)保持長(zhǎng)連接的。
基于以上場(chǎng)景,websocket誕生了。
- 它是一種真正的長(zhǎng)連接,能讓服務(wù)端和客戶端進(jìn)行持久的通信,輕松的實(shí)現(xiàn)服務(wù)器推送技術(shù)。
- 它和
HTTP并沒(méi)有特別多的關(guān)系,但是它的握手 (handshake) 是利用HTTP來(lái)完成。- 它的本質(zhì)是基于
TCP的連接,可以說(shuō)和HTTP屬于平級(jí)的應(yīng)用層協(xié)議。
草案闡述了websocket的設(shè)計(jì)目標(biāo),除了建立起基于TCP的一層連接意外,還有以下幾個(gè)特點(diǎn)
- 為瀏覽器增加一個(gè)基于域名的安全模型(也就是跨域安全模型)
- 增加一個(gè)基于域名和地址的機(jī)制,用來(lái)支持在一個(gè)IP地址上的一端口多域名的多服務(wù)模型(類似nginx在同一個(gè)ip上使用不同的域名來(lái)路由到不同的服務(wù)上)
- 在流式的TCP建立一個(gè)幀數(shù)據(jù)包模型,并且沒(méi)有長(zhǎng)度限制
- 增加了一個(gè)額外的關(guān)閉連接握手,給代理和其他中間件使用。
連接時(shí)握手
websocket的握手實(shí)際上就是給服務(wù)器發(fā)送一個(gè)GET請(qǐng)求,里面帶上指定的header即可。
request例子如下
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
其中比較特殊的是Upgrade,Connection,和Sec開(kāi)頭的幾個(gè)字段,那么如果請(qǐng)求握手的話,
Upgrade: websocket
Connection: Upgrade
是固定的要填寫的兩個(gè)鍵值對(duì)。
Sec-WebSocket-Key是一個(gè)16位的隨機(jī)值,經(jīng)過(guò)base64編碼后生成,給服務(wù)器進(jìn)行UUID連接再編碼后由客戶端檢查用。
Sec-WebSocket-Version是使用的版本號(hào)。
Sec-WebSocket-Protocol是選用的子協(xié)議,此字段為可選字段,由服務(wù)器選擇一個(gè)子協(xié)議與客戶端通信,子協(xié)議是由websocket承載的協(xié)議。
response例子如下
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
我們可以看到這是一個(gè)狀態(tài)碼為101的響應(yīng),響應(yīng)的頭內(nèi)容基本和request可以對(duì)應(yīng),Sec-WebSocket-Accept是服務(wù)端利用Key和UUID拼接后再進(jìn)行base64編碼產(chǎn)生的一個(gè)值,由客戶端進(jìn)行驗(yàn)證。
這樣,我們的連接時(shí)握手就完成了。
數(shù)據(jù)幀
基礎(chǔ)幀協(xié)議
因?yàn)橐恍┌踩脑颍瑥目蛻舳税l(fā)送到服務(wù)端的幀全部要與掩碼進(jìn)行異或運(yùn)算過(guò)才有效,而服務(wù)端發(fā)送到客戶端的幀不需要進(jìn)行異或運(yùn)算。
我們來(lái)看下官方的一幅幀結(jié)構(gòu)定義圖
接下來(lái)逐一解釋。
| 名稱 | 長(zhǎng)度 | 注釋 |
|---|---|---|
| FIN | 1bit | 標(biāo)明這一幀是否是整個(gè)消息體的最后一幀 |
| RSV1 RSV2 RSV3 | 1bit | 保留位,必須為0,如果不為0,則標(biāo)記為連接失敗 |
| opcode | 4bit | 操作位,定義這一幀的類型 |
| Mask | 1bit | 標(biāo)明承載的內(nèi)容是否需要用掩碼進(jìn)行異或 |
| Masking-key | 0 or 4bytes | 掩碼異或運(yùn)算用的key |
| Payload length | 7bit or 7 +16bit or 7 + 64bit | 承載體的長(zhǎng)度(后續(xù)會(huì)解釋為什么會(huì)有3種長(zhǎng)度) |
如果從結(jié)構(gòu)角度講,那么websocket幀結(jié)構(gòu)就這么簡(jiǎn)單。
操作碼 (opcode)
| 值 | 定義 |
|---|---|
| %x0 | 標(biāo)明這一個(gè)數(shù)據(jù)包是上一個(gè)數(shù)據(jù)包的延續(xù),它是一個(gè)延長(zhǎng)幀 (continuation frame) |
| %x1 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)字符幀 (text frame) |
| %x2 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)字節(jié)幀 (binary frame) |
| %x3-7 | 保留值,供未來(lái)的非控制幀使用 |
| %x8 | 標(biāo)明這個(gè)數(shù)據(jù)包是用來(lái)告訴對(duì)方,我方需要關(guān)閉連接 |
| %x9 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)心跳請(qǐng)求 (ping) |
| %xA | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)心跳響應(yīng) (pong) |
| %xB-F | 保留至,供未來(lái)的控制幀使用 |
在websocket中,我們定義了幾種操作類型,也就是表明了數(shù)據(jù)包的行為,數(shù)據(jù)包大體可分為兩種,一種是字符數(shù)據(jù)包 (string),一種是字節(jié)數(shù)據(jù)包 (byte)不同的數(shù)據(jù)包使用不同的opcode來(lái)傳輸,opcode定義如下:
首先Opcode占用4bit的長(zhǎng)度
| 值 | 定義 |
|---|---|
| %x0 | 標(biāo)明這一個(gè)數(shù)據(jù)包是上一個(gè)數(shù)據(jù)包的延續(xù),它是一個(gè)延長(zhǎng)幀 (continuation frame) |
| %x1 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)字符幀 (text frame) |
| %x2 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)字節(jié)幀 (binary frame) |
| %x3-7 | 保留值,供未來(lái)的非控制幀使用 |
| %x8 | 標(biāo)明這個(gè)數(shù)據(jù)包是用來(lái)告訴對(duì)方,我方需要關(guān)閉連接 |
| %x9 | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)心跳請(qǐng)求 (ping) |
| %xA | 標(biāo)明這個(gè)數(shù)據(jù)包是一個(gè)心跳響應(yīng) (pong) |
| %xB-F | 保留至,供未來(lái)的控制幀使用 |
關(guān)于掩碼 (Mask)
如果是客戶端發(fā)送到服務(wù)端的數(shù)據(jù)包,我們需要使用掩碼對(duì)payload的每一個(gè)字節(jié)進(jìn)行異或運(yùn)算,生成masked payload 才能被服務(wù)器讀取。
具體的運(yùn)算其實(shí)很簡(jiǎn)單。
假設(shè)payload長(zhǎng)度為pLen,mask-key長(zhǎng)度為mLen,i作為payload的游標(biāo),j作為mask-key的游標(biāo),偽代碼如下:
for (i = 0; i < pLen; i++){
int j = i % mLen;
maskedPayload[j] = payload[j] ^ maskKey[j];
}
Payload長(zhǎng)度
Payload Length位占用了可選的7bit或者7 + 16bit 或者 7 + 64bit,這里是什么意思呢? MDN上有文章也是對(duì)websocket協(xié)議進(jìn)行了很好的闡述,先貼原文:
引用其中關(guān)于Payload length定義的一段文字:
讀解負(fù)載數(shù)據(jù)長(zhǎng)度
讀取負(fù)載數(shù)據(jù),需要知道讀到那里為止。因此獲知負(fù)載數(shù)據(jù)長(zhǎng)度很重要。這個(gè)過(guò)程稍微有點(diǎn)復(fù)雜,要以下這些步驟:
- 讀取9-15位 (包括9和15位本身),并轉(zhuǎn)換為無(wú)符號(hào)整數(shù)。如果值小于或等于125,這個(gè)值就是長(zhǎng)度;如果是 126,請(qǐng)轉(zhuǎn)到步驟 2。如果它是 127,請(qǐng)轉(zhuǎn)到步驟 3。
- 讀取接下來(lái)的 16 位并轉(zhuǎn)換為無(wú)符號(hào)整數(shù),并作為長(zhǎng)度。
- 讀取接下來(lái)的 64 位并轉(zhuǎn)換為無(wú)符號(hào)整數(shù),并作為長(zhǎng)度。
當(dāng)然我們這邊所使用的都是網(wǎng)絡(luò)字節(jié)序。
關(guān)閉連接時(shí)的握手
關(guān)閉連接的時(shí)候,只用發(fā)送一個(gè)opcode為0x08的幀,payload中前2個(gè)字節(jié)寫入定義的code,后續(xù)寫入關(guān)閉連接的reason,那么一個(gè)關(guān)閉流程就握手就開(kāi)始,此處不再贅述。
總結(jié)
websocket協(xié)議整體看下來(lái)其實(shí)還是很簡(jiǎn)單,也為我們的工作節(jié)省了很多不必要的麻煩。也許我們并不需要了解它實(shí)現(xiàn)的細(xì)節(jié)(除非你正在開(kāi)發(fā)非瀏覽器的客戶端或者服務(wù)端)
總而言之,想要學(xué)習(xí)一個(gè)技術(shù),看原始規(guī)范果然會(huì)讓人暢快淋漓啊~