websocket 協議解析 [RFC6455]

近期工做忙碌,爲了趕SegmentFault for Android 4.0版,到了發瘋的程度。
我來彙報一個進度,已經實現基於websocket的私信系統了,多虧了70大大不懈的努力,在不久的未來,咱們使用web給手機發送私信的願望很快就能夠達成了。html

那麼在這之餘,由於我的對各個通訊協議都有很有興趣,便順便去看了下ietf寫的關於websocket的文章,原文連接在這:java

https://tools.ietf.org/html/r...nginx

固然若是你對實現websocket協議並沒什麼興趣的話,本文能夠直接略過,由於你大概知道怎麼用就能夠了= =web

概念與背景

websocket 目前仍未有標準化的文檔,因此目前個人理解是基於RFC6455草案。ajax

websocket的誕生場景是由於咱們在瀏覽器上缺乏一種與服務器保持長鏈接的標準化的技術,由於HTTP 1.1(如下簡稱爲HTTP)協議只是一個標準的無狀態協議,並不存在除了requestresponse生命週期以外的通訊場景。若是咱們須要實如今線聊天的功能的話,那麼基於HTTP,咱們只能使用醜陋的long pollingajax 輪詢等非正常的技術實現咱們須要的功能。這些方案雖然解決了咱們的使用場景,但並非最好的方案,咱們的HTTP服務器軟件並非設計來保持長鏈接的。跨域

基於以上場景,websocket誕生了。瀏覽器

  1. 它是一種真正的長鏈接,能讓服務端和客戶端進行持久的通訊,輕鬆的實現服務器推送技術。安全

  2. 它和HTTP並無特別多的關係,可是它的握手 (handshake) 是利用HTTP來完成。服務器

  3. 它的本質是基於TCP的鏈接,能夠說和HTTP屬於平級的應用層協議。websocket

草案闡述了websocket的設計目標,除了創建起基於TCP的一層鏈接之外,還有如下幾個特色

  • 爲瀏覽器增長一個基於域名的安全模型(也就是跨域安全模型)

  • 增長一個基於域名和地址的機制,用來支持在一個IP地址上的一端口多域名的多服務模型(相似nginx在同一個ip上使用不一樣的域名來路由到不一樣的服務上)

  • 在流式的TCP創建一個幀數據包模型,而且沒有長度限制

  • 增長了一個額外的關閉鏈接握手,給代理和其餘中間件使用。

鏈接時握手

websocket的握手實際上就是給服務器發送一個GET請求,裏面帶上指定的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

其中比較特殊的是UpgradeConnection,和Sec開頭的幾個字段,那麼若是請求握手的話,

Upgrade: websocket
Connection: Upgrade

是固定的要填寫的兩個鍵值對。
Sec-WebSocket-Key是一個16位的隨機值,通過base64編碼後生成,給服務器進行UUID鏈接再編碼後由客戶端檢查用。
Sec-WebSocket-Version是使用的版本號。
Sec-WebSocket-Protocol是選用的子協議,此字段爲可選字段,由服務器選擇一個子協議與客戶端通訊,子協議是由websocket承載的協議。

response例子以下

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

咱們能夠看到這是一個狀態碼爲101的響應,響應的頭內容基本和request能夠對應,Sec-WebSocket-Accept是服務端利用KeyUUID拼接後再進行base64編碼產生的一個值,由客戶端進行驗證。
這樣,咱們的鏈接時握手就完成了。

數據幀

基礎幀協議

由於一些安全的緣由,從客戶端發送到服務端的幀所有要與掩碼進行異或運算過纔有效,而服務端發送到客戶端的幀不須要進行異或運算。

咱們來看下官方的一幅幀結構定義圖

clipboard.png

接下來逐一解釋。

名稱 長度 註釋
FIN 1bit 標明這一幀是不是整個消息體的最後一幀
RSV1 RSV2 RSV3 1bit 保留位,必須爲0,若是不爲0,則標記爲鏈接失敗
opcode 4bit 操做位,定義這一幀的類型
Mask 1bit 標明承載的內容是否須要用掩碼進行異或
Masking-key 0 or 4bytes 掩碼異或運算用的key
Payload length 7bit or 7 +16bit or 7 + 64bit 承載體的長度(後續會解釋爲何會有3種長度)

若是從結構角度講,那麼websocket幀結構就這麼簡單。

操做碼 (opcode)

在websocket中,咱們定義了幾種操做類型,也就是代表了數據包的行爲,數據包大致可分爲兩種,一種是字符數據包 (string),一種是字節數據包 (byte) 不一樣的數據包使用不一樣的opcode來傳輸,opcode定義以下:
首先Opcode佔用4bit的長度

定義
%x0 標明這一個數據包是上一個數據包的延續,它是一個延長幀 (continuation frame)
%x1 標明這個數據包是一個字符幀 (text frame)
%x2 標明這個數據包是一個字節幀 (binary frame)
%x3-7 保留值,供將來的非控制幀使用
%x8 標明這個數據包是用來告訴對方,我方須要關閉鏈接
%x9 標明這個數據包是一個心跳請求 (ping)
%xA 標明這個數據包是一個心跳響應 (pong)
%xB-F 保留至,供將來的控制幀使用

關於掩碼 (Mask)

若是是客戶端發送到服務端的數據包,咱們須要使用掩碼對payload的每個字節進行異或運算,生成masked payload 才能被服務器讀取。
具體的運算其實很簡單。

假設payload長度爲pLenmask-key長度爲mLeni做爲payload的遊標,j做爲mask-key的遊標,僞代碼以下:

for (i = 0; i < pLen; i++){
    int j = i % mLen;
    maskedPayload[i] = payload[i] ^ maskKey[j];
}

Payload長度

Payload Length位佔用了可選的7bit或者7 + 16bit 或者 7 + 64bit,這裏是什麼意思呢? MDN上有文章也是對websocket協議進行了很好的闡述,先貼原文:

編寫websocket服務器

引用其中關於Payload length定義的一段文字:

讀解負載數據長度

讀取負載數據,須要知道讀到那裏爲止。所以獲知負載數據長度很重要。這個過程稍微有點複雜,要如下這些步驟:

  1. 讀取9-15位 (包括9和15位自己),並轉換爲無符號整數。若是值小於或等於125,這個值就是長度;若是是 126,請轉到步驟 2。若是它是 127,請轉到步驟 3。

  2. 讀取接下來的 16 位並轉換爲無符號整數,並做爲長度。

  3. 讀取接下來的 64 位並轉換爲無符號整數,並做爲長度。

固然咱們這邊所使用的都是網絡字節序

關閉鏈接時的握手

關閉鏈接的時候,只用發送一個opcode爲0x08的幀,payload中前2個字節寫入定義的code,後續寫入關閉鏈接的reason,那麼一個關閉流程就握手就開始,此處再也不贅述。

總結

websocket協議總體看下來其實仍是很簡單,也爲咱們的工做節省了不少沒必要要的麻煩。也許咱們並不須要瞭解它實現的細節(除非你正在開發非瀏覽器的客戶端或者服務端)

總而言之,想要學習一個技術,看原始規範果真會讓人暢快淋漓啊~

相關文章
相關標籤/搜索