Websocket協議 詳解(rfc6455)

Websocket 開發相關的兩篇文章

WebSocket協議 + nginx 動態負載均衡 (史上最全)
Websocket協議 詳解(rfc6455)

Websocket協議簡介

Websocket協議能在受控的環境內實現瀏覽器與服務器之間的雙向通訊(瀏覽器中的應用可能是不可靠的,但是仍然可以與服務器建立websocket連接),使得瀏覽器應用與服務器進行雙向通訊時不必同時打開多個HTTP連接(使用XMLHttpRequest 、iframe 或者長輪詢實現雙向通訊時經常會打開多個連接)。Websocket位於TCP之上(位於應用層),主要包括握手過程、數據傳輸兩個主要部分。

1.介紹

1.1.背景

本部分爲非權威描述

歷史上,web應用(即時通訊或者遊戲)爲了實現與服務器的雙向通訊,一般會建立一個發送消息的http連接與一個接收消息的http連接。 這樣會導致幾個問題:

  1. 服務器與每個客戶端維持多個連接。
  2. 網絡會產生過多的負載,因爲每一個http消息都有頭部信息。
  3. 客戶端需要把發送消息的連接與接收消息的連接建立映射。

Websocket協議設計的目標是使用一個連接實現客戶端與服務器之間的通訊以此替代http長輪訓。Websocket基於http實現雙向通訊可以從當前基礎設施(代理、過濾、認證)中獲得更多支持。 Websocket基於http協議的80和443端口工作,即使這樣會增加協議的複雜度。 當然,我們在設計Websocket時沒有限制必須基於http協議,未來也許會單獨開一個端口,然後使用更簡單的握手過程從而替換掉底層依賴的http協議。

1.2.協議概覽

本部分爲非權威描述

協議主要分爲兩部分:握手(handshake)和傳輸(data transfer)。

客戶端發起的握手協議如下:

        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

服務器響應的握手協議如下:

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

客戶端發送的首行遵循Request-Line格式。 服務器響應的首行遵循Status-Line格式。這兩種格式都在rfc2616中定義。

Request-Line或者Status-Line之後都跟隨着一組無序的頭部字段。這些字段的具體意義,在本文第四章有講述。其他的字段也可以使用,比如cookies(RFC6265),頭部字段的定義和解析在rfc2616中定義。 如果客戶端和服務器成功地完成了握手階段,那麼連接進入數據傳輸階段。這是一個雙向的連接,連接兩段都可以隨意發送數據。

握手成功之後,客戶端和服務器可以相互發送數據,我們把相互發送的數據單位稱作消息(message)。實際傳輸過程中,一個消息可能包含多個frame(幀)。

每個幀都有特定類型。同屬於一個消息的數據幀擁有相同的數據類型。數據類型包括文本(UTF-8編碼的字符)、二進制(具體解析方式由程序定義)、控制(用作控制作用,比如管理連接的打開和關閉)。這個版本的協議定義了六種數據類型,並且預留十種類型。

1.3.開始握手

本部分爲非權威描述
握手階段主要用來兼容http服務器或者中間件,這樣同一個端口就可以使用websocket協議和http協議。Websocket用來升級Http請求的報文如下:

        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

爲了與[RFC2616]兼容,頭部字段可以以任何順序排列。

Get方法中的Request-URI用來指定可以處理websocket請求的服務器接口,這樣可以在一個ip下部署多臺服務器,也可以在一臺服務器中部署多個應用服務器(根據Request-URI進行路由)。

客戶端在發送的請求頭部添加|Host|字段,指定要連接的主機名字。

其他的字段用來指定協議提供的其他選項。常見的選項有子協議(|Sec-WebSocket-Protocol|)、子協議擴展(|Sec-WebSocket-Extensions|)、|Origin|字段等。 |Sec-WebSocket-Protocol|列出了客戶端支持的子協議列表,服務器選擇其中一個協議並返回給客戶端。

Sec-WebSocket-Protocol: chat

|Origin|字段方便服務器識別未授權的瀏覽器應用發送的websocket連接建立請求。服務器可以通過這個字段獲取客戶端的Origin信息,如果服務器不接受來自這個Origin的連接建立請求,可以拒絕客戶端的請求。|Origin|字段的值是由瀏覽器設置的,非瀏覽器環境的客戶端可以根據當時環境設置合理的值。

爲了證明服務器收到了來自客戶端發送的握手信息,服務器需要使用兩個字符串生成一個響應信息。第一個信息來自客戶端握手消息中的|Sec-WebSocket-Key|字段:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

服務器把Sec-WebSocket-Key字段的值與GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”(如果網絡節點中沒有運行websocket協議的話會很難理解這個字符串的含義)連接生成一個字符串,然後進行SHA-1運算,最後進行base64編碼,生成的數據作爲服務器的響應。

比如:客戶端發送的字段|Sec-WebSocket-Key|中包含的值爲dGhlIHNhbXBsZSBub25jZQ,服務器把這個值與258EAFA5-E914-47DA-95CA-C5AB0DC85B11進行連接,然後生成dGhlIHNhbXBsZSBub25jZQ258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串,進行SHA-1運算,生成0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea,最後進行base64編碼生成s3pPLMBiTxaQ9kYGzzhZRbK+xOo=,然後放入服務器的|Sec-WebSocket-Accept|字段中並響應客戶端請求。

服務器返回的握手信息很簡單,第一行爲http狀態行,狀態爲101:

HTTP/1.1 101 Switching Protocols

其他狀態碼錶示握手還沒完成,狀態碼的含義依然遵循http定義。

|Connection|和|Upgrade|表示握手過程完成。|Sec-WebSocket-Accept|表示服務器是否接收這個連接,如果有這個字段,這個字段的值必須爲客戶端提供的|Sec-WebSocket-Key|字段的值與預先定義好的GUID值進行哈希,在進行base64編碼。任何其他的值都表明服務器沒有接受客戶端發起的請求。

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

這些字段會在客戶端進行校驗,如果|Sec-WebSocket-Accept|的值與客戶端期望的值不一致、沒有這個字段或者HTTP狀態碼不爲101,那麼連接不會被建立並且websocket數據幀不會發送。

|Sec-WebSocket-Protocol|表示服務器選擇的子協議。客戶端校驗服務器返回的值是否爲客戶端。

Sec-WebSocket-Protocol: chat

1.4.揮手過程

本部分爲非權威描述

揮手過程要比打開過程簡單的多。 任何一端都可以發送一個Close幀來開始揮手過程,Close幀可能帶有部分數據(比如描述關閉的原因以及狀態碼)。任何一端收到一個Close幀,如果之前沒有回覆過的話,需要發送Close幀。主動關閉的一端在收到對端返回的響應後,在確定沒有數據需要繼續接收之後,開始關閉底層連接(shutdown)。

在發送Close幀之後不應該發送任何數據幀,在收到對端發送過來的Close幀後,對於後續的數據,接收端不予處理。

兩端可以同時發送揮手控制幀。

揮手控制幀用來關閉兩端之間的tcp連接,因爲有時兩端之間並不是直接相連,中間有可能會有代理或者其他中間設備。

發送一個揮手控制幀然後等待響應可以防止某些情況下丟失數據。比如在某些軟件平臺,如果socket一端在接收隊列裏還有未處理的數據,但是關閉了連接,這時會向對端發送一個rst消息,對端在收到rst消息後會讓在recv()監聽的線程收到函數返回的錯誤信息,即使當時接收隊列裏還有數據(因爲兩端都沒有成功處理消息,所以兩端需要對類似的rst錯誤進行處理)。

1.5.設計哲學

本部分爲非權威描述

Websocket協議應該儘量減少使用幀相關的概念(只有在描述協議是基於幀的而不是基於流的時候與用來區分文本幀和二進制幀的時候會涉及到幀)。應用層在websocket層之上,所發送的數據都會經過websocket這一層去傳遞,這一點與http使用tcp去發送數據大致相同。

概念上講,websocket就是基於tcp的協議,擁有以下功能:

  1. 爲瀏覽器添加基於origin的安全模型
  2. 增加尋址和命名服務,可以實現多個服務(http和websocket)監聽同一個端口,同時也可以實現一個ip有多個主機名字。
  3. 在tcp之上建立分幀功能,類似tcp基於的ip協議,但是websocket的幀沒有長度限制。
  4. 增加揮手過程,用來解決鏈路中的代理或者其他中間系統。

除了上面列出的功能外,websocket沒有增加其他功能。考慮到web瀏覽器的限制,儘可能的只把原生的tcp接口暴露給腳本去調用。如果客戶端發送過來的是合法的http升級請求,那麼websocket服務器可以與http服務器共享一個端口。有人可能會使用其他協議來實現客戶端和服務器之間的消息通訊,但是websocket協議的設計初衷就是提供一個相對簡單的協議來與http協議或者http基礎設施(代理)共存,websocket提供的長連接使得經過這些基礎設施時與直接使用tcp一樣安全,並且通過增加一些附屬功能來簡化使用的方式。

協議具有擴展性,未來版本可能會新增其他的功能(概念),比如多路複用。

1.6.安全模型

本部分爲非權威描述

Websocket協議使用與web瀏覽器一樣的安全模型(origin-based)來控制應用可以與哪些服務器建立連接。如果在一個專用的客戶端中使用websocket協議,這個安全模型就顯得沒有必要了,因爲客戶端可以提供任何可能的origin值。

運行[SMTP]與HTTP協議的服務器不會與websocket客戶端建立連接,但是如果HTTP服務器支持升級到websocket協議則可以建立連接。爲了保證協議的正確性,在握手沒有完成之前不可以發送應用數據。

如果websocket服務器接收到了其他協議的(主要指http協議數據)數據,那麼整個連接會被關閉。Websocket在握手階段會使用專用的頭部字段,服務器可以通過驗證這些專用的頭部字段來保證握手過程的合法性,在這個規範編寫的時候,網絡攻擊者不會在網頁應用(html和js)中使用XMLHttpRequest發送帶有|Sec-|前綴的頭部字段。

1.7.與TCP和HTTP的關係

本部分爲非權威描述

Websocket是基於TCP獨立設計的協議。與HTTP的唯一關係是,websocket的握手協議是通過HTTP的協議升級實現的。

默認情況下,普通websocket連接使用80端口,安全的websocket連接使用443端口,基於TLS安全層。

1.8.建立一個連接

本部分爲非權威描述

向一個既支持websocket協議又支持http協議的服務器發送websocket請求時,應該使用傳統GET請求,並且帶有Upgrade頭部字段。在簡單的部署場景中,一臺服務器可以同時支持websocket與http協議。在一些複雜的場景中(多機部署以及負載均衡部署),websocket服務器和http服務器分開部署易於管理。在編寫規範的時候,80端口和443端口的連接成功率不一樣,443的成功率要高點,這個可能隨着時間的變化會有所變化。

1.9.使用子協議

本部分爲非權威描述

客戶端可以在握手請求中添加|Sec-WebSocket-Protocol|字段要求服務器從中選擇一個支持的子協議。服務器在握手響應中需要包含這個字段並且選擇支持的子協議。

子協議按照[Section 11.5]規定註冊名字。爲了避免名字衝突,協議名字應該包含子協議指定方的主機名字,並且主機名字是ascii編碼格式的。舉個例子,比如Example公司打算創建一個chat子協議,這個協議可能會被網絡上的很多服務器使用,那麼這個子協議的名字可以命名chat.example.com。如果Example組織也創建了自己的子協議名稱,命名爲chat.example.org,那麼多個服務器可以同時實現,並且在握手過程中從客戶端提供的子協議列表中選擇一個。

子協議爲了實現向後兼容可以更改名稱,比如bookings.example.net改成v2.bookings.example.net。這些協議可以被客戶端輕鬆的分辨出來。重用相同的子協議名字也可以實現向後兼容,但是這麼做的時候需要認真設計子協議(比如通過其他擴展字段實現版本化來支持向後兼容)。

2.規範要求

如果有的圖表、示例、註釋在這個文檔中標記爲非規範的,表示非標準規範,沒有顯式標記的都是規範標準。

文本中的關鍵字,“MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL"都在[RFC2119]中進行記錄。 在描述算法的文字中,那些祈使句一般可以解釋成"MUST”, “SHOULD”, “MAY"等(比如"strip any leading space characters” or “return false and abort these steps”)。

規範如果被表述爲某些算法或者某些規定的步驟,那麼他們的實現方式可能是多樣性的,但是如果他們的結果是一樣的,那就是可以接受的(特別的,本規範裏規定的算法都很簡單,並且方便實現。)。

2.1.術語和其他約定

_ASCII_表示[ANSI.X3-4.1986]中描述的字符編碼。

本文檔使用定義在[RFC3629]中的UTF-8字符編碼。

關鍵字和算法命名與定義都用_this_表示。

頭部字段和變量使用|this|這種格式。

變量值使用/this/。

[Section 7.1.7]中描述的流程定義爲_Fail the WebSocketConnection_。

關鍵字_Converting a string to ASCII lowercase_ 是指把U+0041 到 U+005A區間的字符替換成U+0061 到 U+007A區間的字符。

_ASCII case-insensitive_表示比較兩個字符串是大小寫不敏感的,字母A-Z與a-z之間相應的字母認爲是相同的(A與a是相同的)。

URI的意思與[RFC3986]定義的一樣。

當websocket實現被要求_send_發送一個數據時,具體實現可以按照需要在某個時間去真正發送數據(數據可能事先在buffer中緩存)。

本文在不同的章節都使用使用[RFC5234]和[RFC2616]中的[ABNF]擴展語法規則。

3.WebSocket URIs

本規範使用了兩種URI框架,使用了[RFC 5234]中定義的ABNF語法與[RFC 3986 ]中定義的單詞和其他的一些術語。

    ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
    wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
    
    host = <host, defined in [RFC3986], Section 3.2.2>
    port = <port, defined in [RFC3986], Section 3.2.3>
    path = <path-abempty, defined in [RFC3986], Section 3.3>
    query = <query, defined in [RFC3986], Section 3.4>

端口組件是可選的,ws默認是80端口,wss默認端口是443。

如果端口組件是wss(大小寫不敏感)的話,那麼這個連接是安全連接。

資源名字可以由一下幾部分組成:

  • 如果path爲空,使用"/"。
  • path組件。
  • 如果query組件不爲空使用?。
  • query組件。

段落標識符在websocket uri中沒有意義,在所有的URI框架中,如果#不表示段落的開始,那麼應該使用%23進行轉義。

4.開始握手

4.1.客戶端要求

客戶端與服務器成功建立socket連接之後會發送websocket握手信息。Websocket在開始階段處於CONNECTING狀態。客戶端需要提供/host/, /port/, /resource name/和 /secure/,具體含義在第三章中有描述,除了這些參數之外有可能會提供/protocols/ 和 /extensions/列表,如果客戶端是瀏覽器,還需要添加/origin/。

便攜式設備中的瀏覽器訪問網絡時可能會通過某些代理軟件,所以,本規範的客戶端包括了便攜式設備中的瀏覽器軟件和代理軟件。

使用(/host/, /port/, /resource name/和 /secure/ )、/protocols/ 、 /extensions/、/origin/(如果客戶端是瀏覽器)來與服務器建立連接,然後發送握手請求,等待服務器的響應。具體的如何打開連接,如何發送握手請求,服務器如何對握手請求進行迴應,都會在下文進行描述。後續我們會使用第三章中規定的單詞進行討論(比如/host/與/secure/標識符)。

  1. 傳入的/host/, /port/, /resource name/和/secure/ 標誌位必須符合第三章中的規定,如果不符合,客戶端必須馬上_Fail the WebSocket Connection_,然後退出這個流程。
    如果客戶端與服務器(ip爲1.1.1.1)正在建立連接,即使服務器使用了其他的服務器名字(/host/),客戶端必須等待這個連接建立完成或者關閉連接,保證只有一個連接處於CONNECTING狀態。如果客戶端有多個連接同時連接一個服務器(同一個IP),那麼客戶端必須串行化這些連接操作,保證同一時刻只有一個連接執行如下步驟。

  2. 如果客戶端沒辦法判斷服務器的ip應該假設每個主機域名都對應一個ip地址,並且客戶端應該限制處於connecting狀態的連接數量(比如客戶端允許分別與a.example.com、b.example.com服務器建立連接)。瀏覽器通過限制用戶同時打開的tab數量來防止客戶端發送大量連接建立請求,以便防止DDOS攻擊。服務器遭受DDOS攻擊時會首先暫停接收新的連接建立請求,然後慢慢關閉部分已創建的連接,這樣可以防止因爲關閉連接過多而導致的客戶端大量重連操作。

    注意:客戶端和服務器之間可以建立的websocket連接(connected)數量沒有限制,如果一個客戶端建立了太多連接或者連接佔用太多服務器資源,服務器可以主動關閉這些連接。

  3. Proxy Usage:如果客戶端使用了代理,那麼客戶端與代理建立連接後應該通知代理與/host/和/port/指定的服務器建立tcp連接。

    假如客戶端使用了代理來處理http請求,在與example.com:80端口通訊時,可以向代理髮送如下指令:

       CONNECT example.com:80 HTTP/1.1
       Host:example.com
    
       如果有密碼字段,則可以發送如下執行:
    
       CONNECTexample.com:80 HTTP/1.1
       Host:example.com
    
       Proxy-authorization: Basic ZWRuYW1vZGU6bm9jYXBlcyE=
    

    如果客戶端沒有設置代理,那麼客戶端可以直接與指定的 /host/ 和/port/建立連接。

    注意:如果沒有UI界面來爲Websocket連接選擇代理,那麼建議使用SOCKs5來代理Websocket連接,如果不能選擇使用SOCKs5代理,建議使用HTTPs代理而不是HTTP代理。

  4. 如果不能打開連接,不管是由於直連或者通過代理連接,客戶端都應該關閉相應的連接並且不再嘗試建立連接。

  5. 如果設置了/secure/標識符,客戶端必須在連接上發送TLS握手請求[RFC2818],然後再發送websocket握手信息。如果TLS握手失敗(服務器證書不合格),客戶端應該關閉連接。如果成功的話,之後所有的數據都應該在安全的TLS通道上發送。

    客戶端在TLS握手過程中必須使用Server Name Indication extension[RFC6066]相關擴展。

連接建立成功之後,客戶端向服務器發送websocket握手消息。握手消息中包括一個HTTP Upgrade以及一系列必須的或者可選的頭部字段。具體的要求信息如下:

  1. 握手消息必須是一個合法的HTTP消息(rfc2616)。
  2. 請求方法必須爲GET,並且http版本必須爲1.1以上(包括1.1)。如果websocket的uri爲ws://example.com/chat,那麼請求行爲GET /chat HTTP/1.1。
  3. 請求地址(URI)(Request-URI)必須滿足[Section 3]中定義的規則(相對地址)或者絕對地址(http/https),通過解析後有/resource name/、 /host/和 /port/ ,並且滿足相應的ws/wss URI。
  4. 請求必須包括|Host|字段並且字段的值爲/host/加上相應的/port/(不使用默認端口的情況下)。
  5. 必須包括|Upgrade|字段並且值必須包含websocket關鍵字。
  6. 必須包括|Connection|字段,並且值爲Upgrade。
  7. 必須包括|Sec-WebSocket-Key|字段,值爲隨機選擇的沒有任何意義的16字節數據,並且經過base64編碼。每個連接的編碼都必須不一樣。
    舉個例子:如果選擇的值爲0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10,那麼經過base64編碼之後AQIDBAUGBwgJCgsMDQ4PEC==。
  8. 如果請求是從瀏覽器發送的,那麼必須包括|Origin|字段。如果是從非瀏覽器發送的請求並且符合當前描述的使用場景,也可以發送這個字段。這個字段的值爲運行前端應用的主機名字並且是經過ascii編碼的。[RFC6454]通過了解更多的賦值規則。
    舉個例子,前端應用從www.example.com地址下載的,應用與ww2.example.com服務器建立連接時,那麼這個值爲http://www.example.com。
  9. 必須包括|Sec-WebSocket-Version|字段,並且值爲13。
    注意:儘管這個協議已經有多個版本的草稿(-09、-10、-11和-12),但是他們仍然不會被當做Sec-WebSocket-Version的值使用。這些值註冊在IANA中,但是不會被使用。
  10. 請求可能包括|Sec-WebSocket-Protocol|字段。如果有這個字段,字段值爲客戶端希望使用的子協議列表,子協議按照期望程度進行排序。子協議的名字必須是唯一的並且由U+0021到U+007E之前的字符組成,不包括分隔符。這個字段值的命名規範使用ABNF中的1#token,這個規則在[RFC2616]中有描述。
  11. 請求可能包含|Sec-WebSocket-Extensions|字段。這個字段表示客戶端希望使用的協議擴展。具體的協議擴展格式在[Section 9.1]中有描述。
  12. 可能還包括其他字段,比如cookies[RFC6265]、授權相關的|Authorization|[RFC2616],這些字段的解析規則都要參考定義他們的文檔。

一旦客戶端發送了握手請求,客戶端必須等待服務器的響應,在獲取響應前不應該發送任何數據。客戶端也應該按照如下規則檢驗服務器的響應信息。

  1. 如果狀態碼不是101,那麼客戶端應該按照[RFC2616]中規定的過程處理響應的數據。如果收到401狀態碼,客戶端需要進行認證過程。服務器可能通過3xx狀態碼要求客戶端進行重定向(客戶端可以不進行重定向操作)。其他情況按照如下規則處理。
  2. 如果響應頭部沒有|Upgrade|字段或者字段的值在大小寫不敏感的情況下與websocket不匹配,那麼客戶端可以直接_Fail the WebSocket Connection_。
  3. 如果響應頭部沒有|Connection|字段,並且字段值沒有包含大小寫不敏感的“Upgrade”,那麼客戶端應該_Fail the WebSocket Connection_。
  4. 如果響應頭部沒有|Sec-WebSocket-Accept|字段或者|Sec-WebSocket-Accept|包含的值不是|Sec-WebSocket-Key|(沒有經過base64加密的)與258EAFA5-E914-47DA-95CA-C5AB0DC85B11連接並且經過base64-encoded SHA-1的值(出去前後的空白字符),客戶端必須_Fail the WebSocket Connection_。
  5. 如果包括|Sec-WebSocket-Extensions|字段,並且值並不是客戶端提供的,客戶端必須_Fail the WebSocket Connection_。
  6. 如果包括|Sec-WebSocket-Protocol|字段,並且值並不是客戶端提供的,客戶端必須_Fail the WebSocket Connection_。

如果服務器響應不符合[Section 4.2.2]規定的要求,客戶端必須_Fail the WebSocket Connection_。

根據[RFC2616]中的規定,http中的所有請求字段和響應字段都是大小寫不敏感的。

如果服務器響應通過上述驗證規則,那麼websocket連接就進入打開狀態。當前使用的協議擴展是服務器響應頭部返回的|Sec-WebSocket-Extensions|字段的值,如果響應中沒有指定協議擴展,那麼當前連接就沒有使用協議擴展。正在使用的子協議也是使用服務器返回的|Sec-WebSocket-Protocol|字段中的值,如果響應中沒有指定子協議,那麼當前連接就沒有使用子協議。除此之外,如果在握手階段服務器要求設置cookies[RFC6265],那麼cookies就是在握手階段設置的cookies。

4.2.服務端要求

服務器可能使用網絡代理管理連接(比如負載均衡服務器或者反向代理)。在這種場景中,協議指定的服務器包括了服務端的所有基礎設施,從接受tcp連接的設施到處理客戶端請求的設施。

4.2.1.讀取客戶端的握手請求

客戶端的握手請求包含如下幾部分。如果服務器在讀取客戶端的握手請求時發現客戶端沒有發送協議指定的字段,並且字段的名字或者值不符合ABNF規定的,服務器可以直接不處理這個握手請求,並且返回錯誤碼(400 Bad Request)。

  1. HTTP協議版本爲1.1或者更高,並且爲Get請求,包括一個Request-URI[RFC2616]地址,這個URI應該被解析成/resource name/。如果是絕對地址,這個URI至少應該包括/resource name/。
  2. |Host|頭部字段。
  3. |Upgrade|字段值爲websocket並且大小寫不敏感。
  4. |Connection|字段值爲Upgrade並且大小寫不敏感。
  5. |Sec-WebSocket-Key|字段,值爲[Section 4 of RFC4648]中規定的格式,經過base64反解碼可以得到16個字節的數據。
  6. |Sec-WebSocket-Version|值爲13.
  7. |Origin|字段,所有的瀏覽器都應該有這個字段,如果請求沒有這個字段,則發送請求的客戶端不應該被當做瀏覽器對待。
  8. 可選的 |Sec-WebSocket-Protocol|字段列出了客戶端期望使用的協議,根據期望程度進行排序。
  9. 可選的|Sec-WebSocket-Extensions|字段列出了客戶端期望使用的協議擴展。這個字段的解析在[Section 9.1]中進行了描述。
  10. 其他可選的字段比如認證或者cookies。其他沒有在[RFC2616]裏面描述的字段都忽略。

4.2.2.發送服務器的握手響應

當客戶端與服務器建立websocket連接時,服務器必須執行如下流程來接收這個連接,並且返回響應。

  1. 如果連接的端口是443端口,那麼服務器必須執行一個TLS握手過程。如果TLS握手失敗(客戶端在擴展字段中指定的host地址和服務器的地址不一致),服務器關閉這個連接。如果成功,所有後續的通訊都要在這個加密通道中傳輸[RFC5246]。

  2. 服務器可以執行一些認證,比如,返回401狀態碼同時帶有|WWW-Authenticate|字段,這個字段在[RFC2616]中有描述。

  3. 服務器可能通過3xx[RFC2616]狀態碼要求客戶端跳轉到指定位置。這一步可能在認證之前也可能之後執行。

  4. 證實如下信息:

    /origin/表示程序從哪裏下載的。origin的值爲ASCII字符並且是小寫的。服務器可能使用這個值來判斷是否接收這個連接。如果服務器不檢查這個字段的值,那麼服務器會接收來自任何客戶端的連接請求。如果服務器不打算接收這個連接可以返回一個錯誤碼(比如403拒絕)。更多相關信息可以查看[Section 10]。

    |Sec-WebSocket-Key| 這個頭部字段是客戶端在握手過程中發送的,經過base64加密的,如果解密的話會獲得16個字節的數據。服務器在生成握手響應的時候會使用這個數據來表示接收了客戶端的握手請求。

    |Sec-WebSocket-Version|表示客戶端希望使用的websocket版本號。如果服務器沒有客戶端請求使用的協議版本號,那麼服務器應該返回一個錯誤碼(比如426)並且使用|Sec-WebSocket-Version|表示服務器可以使用的協議版本號。

    /resource name/表示服務器提供的某種服務。如果服務器提供多種服務,那麼應該從客戶端的握手信息(Request-URI[RFC2616])中提取到具體的值來確定使用哪些服務。如果服務器沒有對應的服務,那麼應該返回一個HTTP錯誤碼(比如404 Not Found)。

    /subprotocol/表示服務器打算使用的一個子協議。這個子協議的值必須是從客戶端握手請求中提供的子協議列表中選擇的,在|Sec-WebSocket-Protocol|字段中指定。如果客戶端沒有使用這個字段或者服務器沒有選擇子協議的話,那麼這個連接不使用任何子協議。沒有這個字段與有這個字段但是值爲空(null)的效果是一樣的,如果服務器不選擇任何一個子協議的話,握手響應中不能有這個字段(|Sec-WebSocket-Protocol|)。空字符串與null的值是不一樣的,空字符串不是一個合法的取值。值的格式準尋ABNF規則,在[RFC2616]中有描述相關規則。

    /extensions/列出了服務器打算使用的協議擴展(有可能也沒有列出),這些擴展值必須從客戶端握手請求中的|Sec-WebSocket-Extensions|字段值中選擇出來的。沒有|Sec-WebSocket-Extensions|這個字段與空值(null)是一樣的效果。空字符串不是合法的值。具體的選擇規則以及解析規則在[Section 9.1]中有描述。

  5. 如果服務器打算接收客戶端發送的連接請求,必須按照如下流程做出響應。

    1. HTTP/1.1 101 Switching Protocols。
    2. |Upgrade|:websocket。
    3. |Connection|:"Upgrade"。
    4. |Sec-WebSocket-Accept|字段,取值爲上面第四步裏描述的/key/值與258EAFA5-E914-47DA-95CA-C5AB0DC85B11串聯起來,進行sha-1運算最終獲得20個字節,再最後進行base64編碼。
       ABNF定義的值爲:
          Sec-WebSocket-Accept    = base64-value-non-empty
          base64-value-non-empty 	= (1*base64-data [ base64-padding ]) |base64-padding
          base64-data      		= 4base64-character
          base64-padding   		= (2base64-character "==") |(3base64-character "=")
          base64-character 		= ALPHA | DIGIT | "+" | "/"
    比如:客戶端在|Sec-WebSocket-Key|字段中提供了“dGhlIHNhbXBsZSBub25jZQ==”這個值,服務器把這個值與“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”進行拼接獲得“dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11”之後進行SHA-1運算獲得
    0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea,
    最後進行base64編碼獲得“s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”,把這個值作爲|Sec-WebSocket-Accept|字段的值返回。
    |Sec-WebSocket-Protocol|值爲上面中定義的/subprotocol/。
    5.可選的|Sec-WebSocket-Protocol|字段取值爲上面第四步中定義的/subprotocol/。
    6.可選的|Sec-WebSocket-Extensions|字段取值爲上面第四步中定義的/extensions/,如果有多個擴展,可以再這個字段中列出來,也可以使用多個|Sec-WebSocket-Extensions|字段。
    

這樣就算完成了握手過程。如果服務器沒有關閉連接,那麼websocket連接便進入OPEN狀態,之後的數據就可以在這個連接上進行傳遞。

4.3.在握手階段使用的並且符合ABNF規則的新增字段

本部分使用了在 Section 2.1 of[RFC2616]描述的ABNF語法規則,同時也包括隱式的 *LWS規則。 如下描述的ABNF規則在本段落使用。規則描述了對應字段值的格式。比如Sec-WebSocket-Key描述了|Sec-WebSocket-Key|值的規則。帶有-Client後綴的規則描述了客戶端請求中的字段值的規則。帶有-Server後綴的規則描述了服務器響應中的字段值的規則。比如,Sec-WebSocket-Protocol-Client描述了客戶端發送的|Sec-WebSocket-Protocol|字段值的規則。如下字段是客戶端發送到服務器的字段值的規則:

      Sec-WebSocket-Key = base64-value-non-empty
      Sec-WebSocket-Extensions = extension-list
      Sec-WebSocket-Protocol-Client = 1#token
      Sec-WebSocket-Version-Client = version

      base64-value-non-empty = (1*base64-data [ base64-padding ]) |
                                base64-padding
      base64-data      = 4base64-character
      base64-padding   = (2base64-character "==") |
                         (3base64-character "=")
      base64-character = ALPHA | DIGIT | "+" | "/"
      extension-list = 1#extension
      extension = extension-token *( ";" extension-param )
      extension-token = registered-token
      registered-token = token
            extension-param = token [ "=" (token | quoted-string) ]
           ; When using the quoted-string syntax variant, the value
           ; after quoted-string unescaping MUST conform to the
           ; 'token' ABNF.
      NZDIGIT       =  "1" | "2" | "3" | "4" | "5" | "6" |
                       "7" | "8" | "9"
      version = DIGIT | (NZDIGIT DIGIT) |
                ("1" DIGIT DIGIT) | ("2" DIGIT DIGIT)
                ; Limited to 0-255 range, with no leading zeros

如下字段描述了服務器返回的字段的值的格式:

      Sec-WebSocket-Extensions = extension-list
      Sec-WebSocket-Accept     = base64-value-non-empty
      Sec-WebSocket-Protocol-Server = token
      Sec-WebSocket-Version-Server = 1#version

4.4.支持多版本的websocket協議

本部分給出了實現客戶端和服務器之間支持多個版本的指導意見。

客戶端通過|Sec-WebSocket-Version|字段聲明它希望使用的協議版本。如果服務器支持客戶端提供的版本並且客戶端提供的其他字段也是合法的,服務器會接收這個版本。如果服務器不支持客戶端提供的協議版本,服務器必須返回一個|Sec-WebSocket-Version|(或者多個字段)字段來聲明它所支持的協議。如果客戶端支持其中一個協議,可以繼續使用上述過程把支持的協議版本發送到服務器。

以下示例描述了協議版本的協商過程:

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      ...
      Sec-WebSocket-Version: 25

服務器的響應信息如下:

      HTTP/1.1 400 Bad Request
      ...
      Sec-WebSocket-Version: 13, 8, 7

服務器也可能返回如下:

      HTTP/1.1 400 Bad Request
      ...
      Sec-WebSocket-Version: 13
      Sec-WebSocket-Version: 8, 7

客戶端再次發送握手請求:

      GET /chat HTTP/1.1
      Host: server.example.com
      Upgrade: websocket
      Connection: Upgrade
      ...
      Sec-WebSocket-Version: 13

5.數據分幀

5.1.總覽

在websocket協議中,數據是通過一系列幀進行傳遞的。爲了防止網絡攻擊(Websocekt爲網絡安全帶來哪些挑戰?),客戶端在發送數據時必須對數據幀進行掩碼(數據幀進行掩碼與是否運行在TLS安全層上無關)。服務器如果收到沒有掩碼的數據幀,需要立即關閉這個連接。在這種情況下,服務器可能發送一個關閉幀,然後狀態碼爲1002(Section 7.4.1)。服務器不用對數據幀進行掩碼。客戶端如果收到掩碼的數據,必須關閉這個連接。在這種情況下客戶端可能需要返回1002(Section 7.4.1)狀態碼。這些規則限制可能在協議的未來版本中變得寬鬆。

幀協議定義了表示類型的opcode字段、數據長度(payload length)字段、表示擴展數據(designated locations for “Extension data”)和應用數據(Application data)位置的字段,擴展數據和應用數據共同組成了數據部分(Payload data)。其他數據位和opcode值預留給未來擴展使用。客戶端和服務器建立連接後如果沒有關閉連接的話,可以雙向發送數據幀。

5.2.協議的基本分幀功能

數據傳輸的格式通過ABNF規則描述[RFC5234]。注意,不像其他章節的ABNF規範一樣,本章的ABNF主要是用來描述位組,位組的長度在註釋中標註了。當編碼時,最高位爲最左邊的數據位。數據格式的總體描述如下圖。如果同一個規則在下圖和下面使用的ABNF規則裏都描述了,以圖爲準。

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-------+-+-------------+-------------------------------+
     |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
     |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
     |N|V|V|V|       |S|             |   (if payload len==126/127)   |
     | |1|2|3|       |K|             |                               |
     +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
     |     Extended payload length continued, if payload len == 127  |
     + - - - - - - - - - - - - - - - +-------------------------------+
     |                               |Masking-key, if MASK set to 1  |
     +-------------------------------+-------------------------------+
     | Masking-key (continued)       |          Payload Data         |
     +-------------------------------- - - - - - - - - - - - - - - - +
     :                     Payload Data continued ...                :
     + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
     |                     Payload Data continued ...                |
     +---------------------------------------------------------------+
  • FIN: 1 bit

    表示是否爲最後一幀。第一個數據幀可能同時也是最後一個幀。

  • RSV1, RSV2, RSV3: 每個1位

    如果協商過程中沒有確定意義的話,每個值都爲0。如果收到一個非0的設置,但是在協商過程中又沒有規定具體的意義,接收端應該關閉這個連接。

  • Opcode 四位<

    定義Payload data的類型,如果收到一個無法理解的值,接收方應該立即關閉這個連接。具體的定義如下:

      *  %x0 表示後續還有數據幀
      *  %x1 表示文本幀
      *  %x2 表示二進制幀
      *  %x3-7 預留
      *  %x8 表示連接關閉
      *  %x9 ping
      *  %xA pong
      *  %xB-F 預留
  • Mask: 1 bit

    標記Payload data是否經過掩碼。如果設置爲1,masking-key字段的值用來編碼,也可以用來解碼。所有從客戶端發送到服務器的數據都必須進行掩碼(mask標記必須爲1)。

  • Payload length: 7 bits, 7+16 bits, or 7+64 bits

Payload data長度,以字節爲單位,如果是0-125表示Payload data的字節數。如果是126,後續的2個字節爲無符號數,表示Payload data的長度。如果是127,後面的8個字節表示爲無符號數,表示Payload data的長度。多字節數據按照網絡字節序(大端)處理。注意:在所有場景中,最少的字節數表示Payload data的長度,比如124個字節長度的字符串不能表示爲126, 0, 124。payload length表示Extension data數據的長度和Application data的長度總和。Extension data的長度有可能爲0,這時候payload length的長度爲Application data(應用數據)的長度。

  • Masking-key: 0 or 4 bytes

    所有從客戶端發送到服務器的數據幀都需要與一個32位長的key進行掩碼。這個32位長的key隨數據幀一起發送。如果MASK設置爲1,key有值。詳細的編碼過程參照Section 5.3

  • Payload data: (x+y) bytes

Payload data是由Extension data" 與 "Application data"組成。

  • Extension data: x bytes

如果在協商階段沒有規定擴展數據的話,這個長度爲0。如果協商階段說明了擴展數據,擴展數據的長度必須標明,如果沒有標明長度,也要在協商的過程中說明如何計算擴展數據長度並且也同時要說明如何使用這些擴展數據。

  • Application data: y bytes

應用數據緊隨擴展數據之後,應用數據的長度爲Payload length減去擴展數據的長度。

基本的分幀操作由下面的ABNF進行描述。注意,這裏的數據都是二進制數據,不是ASCII字符。比如 %x0 / %x1代表一位數據,值爲0或者1,不是一個ASCII字符中的0或者1(佔用一個字節)。佔四位的字段%x0-F,表示四個位,並不是四個ASCII字符。在ABNF中,一個字符只是表示一個非負的整數。在某些場景中會指定某些編碼值與某些字符集(ASCII)進行映射。每個字段的值都用固定位數的二進制值表示,不同字段的值的長度可能不一樣。

 ws-frame                   = frame-fin           ; 1 bit in length
                              frame-rsv1          ; 1 bit in length
                              frame-rsv2          ; 1 bit in length
                              frame-rsv3          ; 1 bit in length
                              frame-opcode        ; 4 bits in length
                              frame-masked        ; 1 bit in length
                              frame-payload-length   ; either 7, 7+16,
                                                     ; or 7+64 bits in
                                                     ; length
                              [ frame-masking-key ]  ; 32 bits in length
                              frame-payload-data     ; n*8 bits in
                                                     ; length, where
                                                     ; n >= 0

    frame-fin               = %x0 ; more frames of this message follow
                            / %x1 ; final frame of this message
                                  ; 1 bit in length

    frame-rsv1              = %x0 / %x1
                              ; 1 bit in length, MUST be 0 unless
                              ; negotiated otherwise

    frame-rsv2              = %x0 / %x1
                              ; 1 bit in length, MUST be 0 unless
                              ; negotiated otherwise

    frame-rsv3              = %x0 / %x1
                              ; 1 bit in length, MUST be 0 unless
                              ; negotiated otherwise

    frame-opcode            = frame-opcode-non-control /
                              frame-opcode-control /
                              frame-opcode-cont

    frame-opcode-cont       = %x0 ; frame continuation

    frame-opcode-non-control= %x1 ; text frame
                            / %x2 ; binary frame
                            / %x3-7
                            ; 4 bits in length,
                            ; reserved for further non-control frames

    frame-opcode-control    = %x8 ; connection close
                            / %x9 ; ping
                            / %xA ; pong
                            / %xB-F ; reserved for further control
                                    ; frames
                                    ; 4 bits in length
                                    
        frame-masked            = %x0
                            ; frame is not masked, no frame-masking-key
                            / %x1
                            ; frame is masked, frame-masking-key present
                            ; 1 bit in length

    frame-payload-length    = ( %x00-7D )
                            / ( %x7E frame-payload-length-16 )
                            / ( %x7F frame-payload-length-63 )
                            ; 7, 7+16, or 7+64 bits in length,
                            ; respectively

    frame-payload-length-16 = %x0000-FFFF ; 16 bits in length

    frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
                            ; 64 bits in length

    frame-masking-key       = 4( %x00-FF )
                              ; present only if frame-masked is 1
                              ; 32 bits in length

    frame-payload-data      = (frame-masked-extension-data
                               frame-masked-application-data)
                            ; when frame-masked is 1
                              / (frame-unmasked-extension-data
                                frame-unmasked-application-data)
                            ; when frame-masked is 0

    frame-masked-extension-data     = *( %x00-FF )
                            ; reserved for future extensibility
                            ; n*8 bits in length, where n >= 0

    frame-masked-application-data   = *( %x00-FF )
                            ; n*8 bits in length, where n >= 0

    frame-unmasked-extension-data   = *( %x00-FF )
                            ; reserved for future extensibility
                            ; n*8 bits in length, where n >= 0

    frame-unmasked-application-data = *( %x00-FF )
                            ; n*8 bits in length, where n >= 0

5.3.客戶端的掩碼操作

經過掩碼的數據幀中的frame-masked標誌必須置爲1。掩碼key會放在frame-masking-key字段隨着數據幀一起發送。

Key的值是客戶端隨機選擇的32位的數據。當進行掩碼時,客戶端需要獲取一個新的32位長度的值。這個key必須是不確定的,並且不能讓服務器或者代理通過當前的key來猜測到下一個key的值。Key的不確定性可以防止惡意用戶構造惡意請求。RFC 4086描述了對於安全比較敏感的應用如何產生比較安全的掩碼key。

掩碼操作不影響Payload data數據的長度。掩碼或者解碼獲取原數據都可以遵循如下規則。

i表示數據幀的第i個字節,4表示4個字節的mask key。

   j                   = i MOD 4
   transformed-octet-i = original-octet-i XOR masking-key-octet-j

數據幀中frame-payload-length字段表明的數據長度不包括masking key。

5.4.分幀

分幀的目的就是發送未知長度的消息。如果不能分幀,發送端需要緩存整個消息。如果有了分幀,發送端或者中間設備可以隨意設置緩存,在緩存滿的時候,把數據包裝成一個數據幀發送出去。

分幀的第二個作用就是多路複用,如果一個大消息發送時沒有分幀的話,會一直佔用邏輯通道,這樣會影響其他使用通道的應用。

除非設置了擴展標誌,否則數據幀沒有其他的特殊處理邏輯。客戶端和服務器之間如果沒有協商某些擴展信息的話,中間設備可以隨意組合和拆分數據幀,如果客戶端和服務器之間進行了擴展信息的協商,而且中間設備也瞭解這些擴展信息,中間設備也可以自由的組合與拆分這些數據幀。

分幀的規則如下:

  • 未分幀的消息只包含一個數據幀,數據幀的FIN標誌會被設置並且opcode值不爲0.

  • 如果消息經過了分幀,第一個分幀的FIN被設置爲0,opcode不爲0,後續跟着0或者多個數據幀,這些數據幀的FIN爲0,並且opcode爲0,最後的數據幀FIN標記設置爲1,opcode也爲0。分幀的數據幀概念上是一個整體的消息,消息的長度就是這些數據幀長度的總和,如果存在擴展數據的話,這個規則可能不會成立,因爲擴展數據的位置與解析由擴展信息決定。比如,有的擴展數據可能只在第一個分幀數據幀中,也有可能存在後面的具有擴展標誌的數據幀中。先不考慮擴展數據,數據幀的具體操作流程如下:

    比如,對於一個文本數據,第一個數據幀的opcode設置爲1,FIN標誌位0,第二個數據幀的opcode爲0,FIN爲0,第三個數據幀的opcode爲0,FIN爲1。

  • 控制幀可以放在分幀的消息中間。控制幀不能繼續分幀。

  • 數據段接收方收到的順序和發送方發送的順序必須一致。

  • 兩個消息的分幀不能交叉,除非在握手協商過程中說明了如何解析這種交叉分幀。

  • 終端可以處理位於中間的控制幀。

  • 發送方可以發送任意大小的數據分幀。

  • 客戶端和服務器必須可以處理經過分幀的和未經過分幀的消息。

  • 因爲控制幀不能被分幀,所以中間設備必須不能對控制幀進行分幀。

  • 中間設備如果不能理解預留字段的具體含義不能對數據幀進行進一步操作。

  • 在一個連接中,如果消息的擴展信息在握手協商中確定了具體的意義,但是中間設備不瞭解這些擴展信息的具體意義,所以不能改變這些消息的分幀信息。同樣的,如果中間設備不瞭解websocekt的握手過程,同樣不能更改消息的分幀操作。

  • 根據上述規則,一個消息的所有分幀類型都必須一樣,分幀類型只在第一個分幀進行設置。因爲控制幀不能分幀,所以經過分幀的消息類型都是文本、二進制或者其他的自定義類型。

注意:如果控制幀不能插入數據段中間,那麼在一個非常大的消息後面發送一個ping消息會有很大的延遲。

協議實現注意:如果沒有特殊規定的話,接收方不必等到數據接收完成再處理,比如在流處理應用中,可以把部分數據交由應用處理。這種要求可能隨着不同的websocket版本的不同而不同。

5.5.控制幀

opcode字段的最高位爲1表示控制幀。當前定義的控制幀包括:0x8 (Close), 0x9 (Ping), and 0xA (Pong)。值在0xB-0xF中間的表示預留值。

控制幀用來交流websocket連接狀態。控制幀可以放在數據幀中間。

所有的控制幀payload的長度必須爲125字節或者比125少,並且都不能分幀。

5.5.1.Close控制幀

opcode標誌爲0x8。

Close幀可能包含body(幀的數據部分),這個body可能描述了關閉連接的原因,比如服務器宕機、收到一個很大的數據幀、或者收到一個不能識別的數據格式的幀。如果有數據部分,那麼前兩個字節(網絡字節序)爲無符號的整數,這個整數代表狀態碼(Section 7.4中有描述)。隨後跟着UTF-8編碼的文本。這個文本不一定必須是可以直接閱讀,也有可能是與調試有關的數據。因爲這個數據不一定方便人類閱讀,所以客戶端不能把這個數據直接展示給終端用戶。

從客戶端發送到服務器的Close幀必須按照Section 5.3規則進行掩碼。

應用在發送Close幀之後不能再發送任何應用數據。

如果一個終端收到一個close幀,但是之前沒有發送過close幀,這個終端應該發送一個close幀(在響應close幀時,終端應該把收到的close幀中的狀態值放在自己發送的響應的close幀中)。這個響應的發送時機視具體情況而定。一個終端可能在發送完自己的數據後纔會發送close幀。

發送完close幀之後並且也收到對端發送的close幀,終端可以關閉底層的TCP連接。服務器必須馬上關閉TCP連接,客戶端可以等待服務器關閉TCP連接,也可以在任何時候關閉TCP連接。

如果客戶端和服務器同時發送了Close幀,兩端都發送並且收到close幀可以認爲WebSocket關閉了,並且關閉底層TCP連接。

5.5.2.Ping控制幀

Ping幀的opcode爲0x9。

可能包含Application data。

收到Ping幀後,需要立馬發送一個Pong幀作爲響應,如果已經收到Close幀的話,就不用發送Pong幀。發送Pong的時機視具體情況而定。Pong在下一節描述。

終端可以在連接建立後到關閉前的任何時刻發送Ping幀。

注意:Ping可以用來判斷連接是否存活。

5.5.3.Pong控制幀

Ping幀的opcode爲0xA。

對於Ping控制幀的要求同樣適用於Pong控制幀。

Pong幀中的應用數據應該與Ping中的應用數據一樣。

如果終端還沒來得及響應之前的Ping幀,又收到一個Ping幀,它可以選擇響應最近的一個Ping幀。

一個Pong幀可能在沒有接收到Ping幀的情況下發送,這種一般出現在單向心跳的情況下。Pong幀不需要響應。

5.6. 數據幀

opcode字段的最高位爲0表示數據幀。目前表示數據幀的爲 0x1 (Text), 0x2 (Binary)。opcodes爲0x3-0x7爲預留值,表示非控制幀。

數據幀包含應用數據或者擴展數據。opcode的值表示數據的解釋方式。

Text

數據被編碼成UTF-8字符。一個數據幀可能包含一個字符的部分UTF-8編碼序列,但是整個消息的編碼必須正確。包含非法的UTF-8編碼序列的消息處理方法在8.1小節描述。

Binary

應用數據可以是任意的二進制數據,這些數據的具體含義由應用層負責解釋。

5.7. 例子

  • 包含一個沒有掩碼的數據幀的消息,
    1. 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains “Hello”)
  • 包含一個掩碼的數據幀的消息,
    1. 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(contains “Hello”)
  • 分段的未掩碼的文本消息,
    1. 0x01 0x03 0x48 0x65 0x6c (contains “Hel”),
    2. 0x80 0x02 0x6c 0x6f (contains “lo”)
  • 未掩碼的Ping請求和掩碼的Ping響應,
    1. 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of “Hello”,but the contents of the body are arbitrary),
    2. 0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58 (contains a body of “Hello”, matching the body of the ping)
  • 一個包含256字節的二進制的消息未掩碼的數據幀。
    1. 0x82 0x7E 0x0100 [256 bytes of binary data]
  • 一個包含64k字節的二進制的消息未掩碼的數據幀。
    1. 0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]

5.8. 擴展

協議支持擴展以實現對基礎功能的增強。客戶端和服務器在握手階段必須協商擴展數據的具體含義。協議中的opcode的值在 0x3到0x7,0xB 到 0xF表示擴展,同時還提供了"Extension data" 字段、以及frame-rsv1,frame-rsv2,frame-rsv3這三個擴展位。擴展的協商過程在9.1章節進行討論。

下面是一個可能的擴展用法,這些用法不是完備的並且也不是規範的。

  • 在Payload data中,Extension data可能在Application data之前。
  • 每個幀的預留位可以獨立定義。
  • 預留操作碼的值可以定義。
  • 如果需要更多的操作碼值,可以使用預留位(Reserved bits)進行補充。
  • 可以使用Payload data對操作碼或者預留位進行擴展。

6. 發送和接收數據

6.1. 發送數據

在websocket連接上發送消息必須執行如下流程:

  1. 發送端必須確保webcosket連接處於打開狀態。如果任何時刻連接狀態發生變化,發送端可以不執行如下流程。
  2. 發送端必須把數據包裝到websocket幀中(幀格式在5.2章節中有介紹)。如果發送的數據很多或者在發送時不知道數據大小,發送端可以按照5.4章節介紹的過程對數據進行分幀。
  3. 第一個幀必須設置text或者binary類型,這樣接收方就可以根據類型來解析數據。
  4. 最後一個發送的幀必須設置Fin爲1.
  5. 如果是客戶端發送的數據,每個幀必須設置Mask爲1.
  6. 如果建立連接時協商了擴展點,應該對每個擴展點都進行詳細的考慮。
  7. 生成的幀必須通過網絡連接(tcp)進行發送。

6.2. 接收數據

接收端接收到的字節必須解析成5.2章節定義的數據格式。必須按照5.5章節定義的方式處理控制幀。對於數據幀,接收方必須解析數據幀的數據類型(5.2)。如果收到一個沒有分幀的數據,那麼就可以確認收到一個類型爲/type/和數據爲/data/的消息。如果收到一個分幀的數據,那麼應用數據就是所有後續分幀的/data/組合。當最後一個Fin設置爲1的數據幀到達後,那麼一個完整的應用數據就接收完畢了,後續的幀就屬於新的消息。

擴展設置可以改變數據解析的方式,可能包括消息邊界。擴展設置除了可以在應用數據前添加擴展數據外,還有可能會對應用數據進行壓縮。

正如5.3章節中描述的一樣,服務器收到客戶端發送的數據幀時,必須解碼。

7.關閉連接

7.1.定義

7.1.1.關閉websocket連接

可以通過關閉底層TCP連接來關閉websocket連接。關閉TCP連接要乾脆利落,同時也要關閉相關的TLS會話,如果還有未來得及處理的數據,也需要丟棄這些數據(可能在接收緩存中)。當受到網絡攻擊時,主機可以通過任何可以使用的方法來關閉連接。

在大部分場景下,底層的TCP連接都是由服務器關閉,這樣服務器會保持TIME_WAIT狀態一段時間(如果是客戶端首先關閉連接,客戶端需要保持2MSL時間才能重新打開連接),但是這樣對服務器沒有什麼影響,只要SYN帶有更大的seq,服務器可以重新打開這個連接。在非正常情況下(在一段時間後客戶端沒有服務器發送的Close請求),客戶端可以首先發起關閉TCP連接請求。同樣地,當服務器被要求關閉websocket連接的時候,服務器應該立馬發起連接關閉請求,當客戶端被要求關閉連接時,應該等待服務器發送的關閉連接請求。

當使用C編程語言時,在關閉伯克利socket時,我們可以調用shutdown()方法,並且附帶SHUT_WR這個參數,之後調用recv()方法,並且等待獲取一個值爲0的字節,來表示對端也調用了shutdown()方法,最後調用socket的close方法來關閉連接。

7.1.2.發起Websocket關閉握手

關閉websocket連接時需要在Close控制幀中指定一個code值和原因。當一端既發送了Close控制幀,也收到一個Close控制幀,可以按照7.1.1章節介紹的規則關閉TCP連接。

7.1.3.Websocket關閉握手已經開始

只要發送或者收到一個Close控制幀,標誌着Websocket關閉握手已經開始,並且Websocket連接已經進入CLOSING狀態。

7.1.4.Websocket連接已經關閉

當底層的TCP連接已經關閉,表明Webcosket連接已經關閉,並且進入了CLOSED狀態。當TCP連接在Websocket關閉流程完成後關閉,可以說Webcosket連接被優雅地關閉了。

當Webcosket連接不能建立,也可以說Webcosket連接被關閉了,只不過不是優雅地關閉(_ The WebSocket Connection is Closed_, but not _cleanly _)。

7.1.5.Websocket連接關閉狀態碼

正如5.5.1和7.4章節介紹一樣,一個Close控制幀可能會包含一個狀態碼用來表明關閉連接的理由。Websocket關閉請求可以由任何一端發送,也有可能是同時發送。_The WebSocket Connection Close Code_定義爲第一個Close控制幀中包含的並且在7.4章節中定義的狀態碼。當Close 控制幀沒有狀態碼,_The WebSocket Connection Close Code_被認爲是1005.當Websocket連接被關閉了,但是沒有收到Close控制幀(底層TCP連接直接關閉),_The WebSocket Connection Close Code_被認爲是1006.

注意:連接的兩端可能存在_The WebSocket Connection Close Code_數值不一致的情況。比如,遠端發送了一個Close控制幀,但是本地沒有讀取TCP中的數據,也就是沒有讀取遠端發送的Close控制幀信息,本地應用打算關閉連接並且發送了一個Close 控制幀,這樣的話,兩端都發送並且收到一個Close控制幀,並且後續不會再發送Close控制幀。兩端都看到了對面發送的_The WebSocket Connection Close Code_。這樣的話,兩端可能看到不同的_The WebSocket Connection Close Code_,這種情況是在兩端幾乎同時開啓關閉握手時出現。

7.1.6.Websocket連接關閉原因

在5.5.1和7.4章節講到,一個Close控制幀可能包含一個狀態碼用來指示關閉的原因,同時也能包含一個UTF-8編碼的數據,這個數據的具體解釋方式由接收方的應用處理。_The WebSocket Connection Close Reason_定義爲附加在狀態碼之後的UTF-8編碼的數據,這個數據包含在第一個Close控制幀中。如果Close控制幀中沒有包含這個數據,那麼_The WebSocket Connection Close Reason_被認爲是空字符串。

注意:和7.1.5章節描述的一樣,兩端接收到的關閉原因可能不一樣。

7.1.7.Websocket連接失敗

某些算法或者規範要求終端可以_Fail the WebSocket Connection_。爲了實現這個功能,客戶端必須_Close the WebSocket Connection_並且向用戶以合適的方式上報錯誤。同樣的,服務器也應該_Close the WebSocket Connection_並且把錯誤日誌打印出來。

如果_The WebSocket Connection is Established_在_Fail the WebSocket Connection_之前執行,終端應該_Fail the WebSocket Connection_這個連接並且向對面發送一個帶有狀態碼的Close控制幀,然後再執行_Close the WebSocket Connection_。如果一個終端已經瞭解到對面不會處理任何websocket消息了,那麼這個終端有可能就不會再發送Close幀,因爲Websocket在建立連接的時候有可能就沒有建立成功。當終端被命令_Fail the WebSocket Connection_時,它不能繼續處理任何從對端發送過來的數據(包括對端發送過來的Close幀)。

除了上述情況或者應用主動關閉Websocket連接,其他情況下不應該關閉websocket連接。

7.2. 異常關閉

7.2.1. 客戶端發起的關閉

某些算法,尤其在建立連接的握手階段,要求客戶端可以_Fail the WebSocket Connection_。爲了實現這個功能,客戶端必須按照7.1.7章節描述的過程去執行相應的步驟。

在任何時刻如果底層的TCP連接丟失了,客戶端必須_Fail the WebSocket Connection_。

除了上述情況或者應用主動關閉Websocket連接,其他情況下不應該關閉websocket連接。

7.2.2. 服務端發起的關閉

某些算法,尤其在建立連接的握手階段,要求服務端可以_Fail the WebSocket Connection_。爲了實現這個功能,客戶端必須按照7.1.7章節描述的過程去執行相應的步驟。

7.2.3. 從異常關閉中恢復

很多情況都會導致連接異常關閉。常見的是底層鏈路的連接錯誤,這種情況下可以重新建立連接。還有其他非鏈路錯誤,客戶端可能非正常關閉了連接,但是又立刻或者持續地進行重連,服務器可能會經歷類似於拒絕式服務攻擊,因爲很多客戶端會嘗試進行連接。最後可能會導致服務器無法在短時間內進行恢復,或者恢復過程變得很困難。

爲了防止這種情況,客戶端在遇到連接非正常關閉的情況下應該採用某種回退機制。

在經過某個隨機時間後再進行重連操作。具體的隨機算法由客戶端去決定,0到5秒可能是一個不錯的選擇,不過客戶端依然可以根據經驗或者具體情況去選擇重連的回退時間。

如果第一次重連失敗,第二次重連時間應該適當增長,比如採用截斷二進制指數退避算法(truncated binary exponential backoff)。

7.3. 正常關閉

服務器可以根據情況關閉webcosket連接。客戶端不應該隨意關閉連接。不管誰關閉連接,都應該遵守7.1.2描述的過程去_Start the WebSocket Closing Handshake_。

7.4. 狀態碼

終端在關閉已經建立的websocket連接時應該指定關閉的理由。當前規範沒有定義接收到關閉理由時應該進行什麼樣的操作。Close幀可以選擇是否記錄狀態碼和相關文本。

7.4.1. 預留狀態碼

終端在發送Close幀時可以使用以下預留的狀態碼。

  • 1000

    1000表示正常關閉連接。

  • 1001

    1001表示終端已經“going away”,比如服務器宕機或者瀏覽器跳轉到其他頁面。

  • 1002

    1002表示終端由於協議錯誤終止了連接。

  • 1003

    1003表示終端因爲接收到了不能處理的數據類型,所以打算關閉連接(比如終端只理解text數據,但是收到了binary類型的消息)。

  • 1004

    預留。

  • 1005

    1005是一個預留值,表示終端期望收到狀態碼但是沒有收到,不能放在Close幀中。

  • 1006

    預留值,用來表示連接被異常關閉(沒有發送或者收到Close幀),不能放在Close幀中。

  • 1007

    1007表示終端關閉了連接,因爲發現收到的數據內容和實際的消息類型不匹配。(比如非UTF-8編碼的數據放在了text消息中)

  • 1008

    1008表示收到一個不符合規則的消息,並打算關閉連接。這是一個比較通用的狀態碼,當沒有更合適的狀態碼或者希望隱藏一些具體的細節的時候可以選擇使用。

  • 1009

    1009表示收到一個比較大的不能處理的消息。

  • 1010

    用來關閉連接,因爲在握手階段,客戶端希望服務器使用多個擴展,但是服務器沒有返回相應的擴展信息。客戶端在發送Close幀時把希望使用的擴展列表放在/reason/(關閉原因)中。服務器不需要發送這個狀態碼,因爲服務器可以直接執行_ Fail the WebSocket Connection _。

  • 1011

    表示服務器遇到某些不能完成的請求。

  • 1015

    表示不能進行TLS握手的時候發送(比如服務器的證書不能得到驗證),是一個預留狀態碼,不能在Close幀中使用。

7.4.2.預留狀態碼範圍

  • 0-999

    還沒有被使用。

  • 1000-2999

    這個範圍的狀態碼被設計爲協議預留,在協議未來的擴展或者校訂版本中會用到這個範圍的狀態碼。

  • 3000-3999

    這個範圍的狀態碼預留給類庫、框架、應用使用(基礎應用比如apache http這樣的應用或者一些開源框架)。這些狀態碼在IANA中直接註冊。本協議沒有規定具體含義。

  • 4000-4999

    這個範圍的狀態碼沒有在IANA中註冊可以隨意使用,具體的意義由websocket應用程序規定。

8.錯誤處理

8.1.處理關於UTF-8編碼的數據異常

如果按照UTF-8格式不能成功解析字節流,終端必須_ Fail the WebSocket Connection _。握手或者後續的傳遞數據階段都適用於這個規則。

9.擴展

Websocket客戶端可能要求使用協議擴展。服務器可能接受部分或者所有來自客戶端的擴展請求。如果客戶端沒有請求相關的擴展,服務器一定不能響應。如果在客戶端和服務器協商階段指定了部分擴展參數,那麼參數的使用必須準守相關的擴展規則。

9.1.協商擴展

客戶端使用擴展時可以在請求頭中使用|Sec-WebSocket-Extensions| 字段,請求頭的name和value規則遵循http請求頭的相關規則。本章節使用ABNF規則來定義請求頭。如果客戶端或者服務器收到不符合ABNF規則的value,可以立刻_ Fail the WebSocket Connection _。

       Sec-WebSocket-Extensions = extension-list
         extension-list = 1#extension
         extension = extension-token *( ";" extension-param )
         extension-token = registered-token
         registered-token = token
         extension-param = token [ "=" (token | quoted-string) ]
             ;When using the quoted-string syntax variant, the value
             ;after quoted-string unescaping MUST conform to the
             ;'token' ABNF.

和其他HTTP頭部字段一樣,一個header值可以被拆分或者組裝成多行。所以如下實例是一樣的。

      Sec-WebSocket-Extensions: foo
      Sec-WebSocket-Extensions: bar; baz=2

等於

      Sec-WebSocket-Extensions: foo, bar; baz=2

使用的extension-token必須是已經註冊的(在11.4章節介紹)。必須使用與擴展相關的參數。如果服務器沒有確認某些擴展,客戶端不能使用這些擴展的功能。

擴展項的排列順序是有特殊意義的。擴展項之間的交互順序都在定義他們的文檔中有相關描述。如果沒有相關文檔進行定義,在客戶端請求頭部中列出的擴展項的順序,就是它期望的順序。服務器返回的擴展項的順序是實際使用的順序。擴展項處理數據的順序就是按照他們在服務器握手響應頭部中出現的順序。

如果服務器返回的頭部信息中|Sec-WebSocket-Extensions|中有“foo”和“bar”兩個擴展項,那麼對於數據的處理過程就是 bar(foo(data))。如果數據是分幀接收的話,那麼把這個幀組裝後再進行處理。

服務器一個不規範的擴展頭部實例:

         Sec-WebSocket-Extensions: deflate-stream
         Sec-WebSocket-Extensions: mux; max-channels=4; flow-control,
          deflate-stream
         Sec-WebSocket-Extensions: private-extension

服務器通過|Sec-WebSocket-Extensions|頭包含一個或者多個客戶端發送過來的擴展項。服務器返回的擴展參數,以及返回什麼樣的值,都由具體的擴展項定義。

9.2.已知擴展

擴展可以爲協議提供一些新的功能。本文檔不描述任何擴展相關的信息。協議的實現可能會描述使用的相關擴展項.

10.安全方面的考慮

本章節討論Websocket協議關於安全方面的點。特殊方面的安全點在後續的章節有講述。

10.1.非瀏覽器客戶端

Websocket在受信任的應用裏運行的時候(比如瀏覽器)可以防止惡意JavaScript腳本的運行,比如檢查|Origin|字段。查看1.6章節來了解更詳細的內容。這個假設在其他類型的客戶端中不成立。

然而這個協議本身就是爲了讓在網頁中的腳本語言使用的,當然也可以被其他主機應用使用,所以這些主機可能隨意的發送|Origin|,這樣有可能會迷惑服務器。服務器應該謹慎的去與客戶端交流,不能單純的認爲對面就是已知主機上的腳本。所以,服務器不能認爲客戶端發送的都是合法的。

舉例:如果服務器使用前端傳過來的sql進行數據庫查詢,所有的sql文本應該經過轉義再發送到數據庫,以免服務器遭受sql注入攻擊。

10.2.Origin注意事項

服務器如果不是接收所有主機的請求的話,應該檢查請求頭中|Origin|的值。如果服務器不接受|Origin|主機的請求,應該在握手階段返回一個http 403相應。

|Origin|可以防止惡意代碼在受保護的應用中運行時發起的某些攻擊。客戶端可以通過|Origin|機制來確定是否需要爲腳本程序授予通訊權限。這種機制不是防止非瀏覽器應用去建立Websocket連接,而是防止惡意的JavaScript發送一個虛擬的Websocket握手請求。

10.3.基礎設施的攻擊(Masking,掩碼)

Websocket除了會攻擊終端設備以外,還會攻擊網絡基礎設施,比如代理服務器。

在協議的過程中,有一項實驗用來模擬一類代理服務器攻擊,這類攻擊會污染一部分透明代理類(基於ip做轉發的,不是普通的http代理服務器,比如帶有http緩存功能的網關或者網橋設備,可以參考Talking to Yourself for Fun and Profit 中文版)。

爲了防止中間代理服務器被攻擊,現在對每一個客戶端發送的數據進行掩碼操作,這樣攻擊者就不能僞造http請求來攻擊中間代理服務器了。

客戶端必須爲每個數據幀選擇一個新的掩碼key,不能預測下一個key的算法最安全。如果客戶端使用了一個可以判斷下一個key的算法,那麼攻擊者可能會使用某個數據,在與mask key進行掩碼後剛好就是http請求,這樣同樣會對中間代理服務器進行緩存投毒。

如果數據幀已經在發送過程中,那麼就不能再對數據進行修改。否則,攻擊者的腳本可能在開始發送的時候會寫一串0的數據,然後邊發送邊修改數據幀的值,比如改成http請求。

我們假定的攻擊模型是客戶端在發送數據時發送一個http請求,所以需要掩碼的就是客戶端發送到服務器的數據。服務器到客戶端的數據可以看成是一個響應,所以沒有必要對服務器的響應數據進行掩碼。

10.4.實現方面的一些限制

應該對消息的大小進行限制,來避免攻擊者會發送一個很大的消息或者分段的數據幀組合後出現很大的消息(2^60),這樣容易使對面的服務器內存耗盡或者出現拒絕服務。

10.5.客戶端認證

本協議不描述關於服務器如何認證客戶端的相關規則。websocket服務器可以使用當前http服務器普遍使用的認證方式,比如cookies、http 認證、或者tls認證。

10.6.連接的保密性和完整性

連接的保密性和完整性通過使用TLS協議來實現。websocket協議的實現必須支持TLS。
使用TLS的連接,保密程度完全取決於在TLS握手階段協商的加密算法是否夠強。爲了達到期望的安全程度,客戶端應該使用更強的TLS算法。Web Security Context: User Interface Guidelines描述相關算法。

10.7.處理無效數據

客戶端和服務器必須對接收到的數據進行檢查。當終端接收到不能理解的或者接收到的數據違反了終端制定的安全準則再或者握手過程中遇到了之前沒有交流過的某些值,終端可以直接關閉TCP連接。如果在握手成功之後,收到了無效的數據,終端應該發送一個Close幀和一個狀態碼,之後再執行_ Close the WebSocket Connection _流程。Close幀中寫入狀態碼可以方便排查問題。如果在握手階段接收到了無效數據,服務器可以返回Http相關狀態碼。

在發送文本數據的時候使用了錯誤的編碼可能會引發一些安全方面的問題。本協議中的文本都是UTF-8編碼。儘管協議指定了數據的長度,並且應用按照這個長度去讀取消息,但是發送沒有正確編碼的數據仍然可能會導致數據丟失或者一些潛在的安全問題。

10.8.握手階段使用SHA-1

這裏描述的握手過程不依賴SHA-1相關的安全屬性。

翻譯自:https://tools.ietf.org/html/rfc6455

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章