WebSocket 協議

1.1 背景知識

因爲歷史緣由,在建立一個具備雙向通訊機制的 web 應用程序時,須要利用到 HTTP 輪詢的方式。圍繞輪詢產生了 「短輪詢」 和 「長輪詢」。html

短輪詢

瀏覽器賦予了腳本網絡通訊的編程接口 XMLHttpRequest,以及定時器接口 setTimeout。所以,客戶端腳本能夠每隔一段時間就主動的向服務器發起請求,詢問是否有新的信息產生:jquery

  1. 客戶端向服務器發起一個請求,詢問 「有新信息了嗎」
  2. 服務端接收到客戶端的請求,可是此時沒有新的信息產生,因而直接回復 「沒有」,並關閉連接
  3. 客戶端知道了沒有新的信息產生,那麼就暫時什麼都不作
  4. 間隔 5 秒鐘以後,再次從步驟 1 開始循環執行

長輪詢

使用短輪詢的方式有一個缺點,因爲客戶端並不知道服務器端什麼時候會產生新的消息,所以它只有每隔一段時間不停的向服務器詢問 「有新信息了嗎」。而長輪詢的工做方式能夠是這樣:web

  1. 客戶端向服務器發起一個請求,詢問 「有新信息了嗎」
  2. 服務器接收到客戶端的請求,此時並無新的信息產生,不過服務器保持這個連接,像是告訴客戶端 「稍等」。因而直到有了新的信息產生,服務端將新的信息返回給客戶端。
  3. 客戶端接收到消息以後顯示出來,並再次由步驟 1 開始循環執行

能夠看到 「長輪詢」 相較於 「短輪詢」 能夠減小大量無用的請求,而且客戶端接收到新消息的時機將會有可能提早。算法

繼續改進

咱們知道 HTTP 協議在開發的時候,並非爲了雙向通訊程序準備的,起初的 web 的工做方式只是 「請求-返回」 就夠了。編程

可是因爲人們須要提升 web 應用程序的用戶體驗,以及 web 技術自己的便捷性 - 不須要另外的安裝軟件,使得瀏覽器也須要爲腳本提供一個雙向通訊的功能,好比在瀏覽器中作一個 IM(Instant Message)應用或者遊戲。api

經過 「長、短輪詢」 模擬的雙向通訊,有幾個顯而易見的缺點:跨域

  1. 每次的請求,都有大量的重複信息,好比大量重複的 HTTP 頭。
  2. 即便 「長輪詢」 相較 「短輪詢」 而言使得新信息到達客戶端的及時性可能會有所提升,可是仍有很大的延遲,由於一條長鏈接結束以後,服務器端積累的新信息要等到下一次客戶端和其創建連接時才能傳遞出去。
  3. 對於開發人員而言,這種模擬的方式是難於調試的

因而,須要一種能夠在 「瀏覽器-服務器」 模型中,提供簡單易用的雙向通訊機制的技術,而肩負這個任務的,就是 WebSocket瀏覽器

1.2 協議概覽

協議分爲兩部分:「握手」 和 「數據傳輸」。緩存

客戶端發出的握手信息相似:安全

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

首行以後的部分,都是沒有順序要求的 HTTP Headers。其中的一些 HTTP頭 的意思稍後將會介紹,不過也可包括例子中沒有說起的頭信息,好比 Cookies 信息,見 RFC6265。HTTP頭的格式以及解析方式見 RFC2616

一旦客戶端和服務端都發送了它們的握手信息,握手過程就完成了,隨後就開始數據傳輸部分。由於這是一個雙向的通訊,因此客戶端和服務端均可以首先發出信息。

在數據傳輸時,客戶端和服務器都使用 「消息 Message」 的概念去表示一個個數據單元,而消息又由一個個 「幀 frame」 組成。這裏的幀並非對應到具體的網絡層上的幀。

一個幀有一個與之相關的類型。屬於同一個消息的每一個幀都有相同的數據類型。粗略的說,有文本類型(以 UTF-8 編碼 RFC3629)和二進制類型(能夠表示圖片或者其餘應用程序所需的類型),控制幀(不是傳遞具體的應用程序數據,而是表示一個協議級別的指令或者信號)。協議中定義了 6 中幀類型,而且保留了 10 種類型爲了之後的使用。

1.3 開始握手

握手部分的設計目的就是兼容現有的基於 HTTP 的服務端組件(web 服務器軟件)或者中間件(代理服務器軟件)。這樣一個端口就能夠同時接受普通的 HTTP 請求或則 WebSocket 請求了。爲了這個目的,WebSocket 客戶端的握手是一個 HTTP 升級版的請求(HTTP Upgrade 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

爲了遵循協議 RFC2616,握手中的頭字段是沒有順序要求的。

跟在 GET 方法後面的 「請求標識符 Request-URI」 是用於區別 WebSocket 連接到的不一樣終節點。一個 IP 能夠對應服務於多個域名,這樣一臺機器上就能夠跑多個站點,而後經過 「請求標識符」,單個站點中又能夠含有多個 WebSocket 終節點。

Host 頭中的服務器名稱可讓客戶端標識出哪一個站點是其須要訪問的,也使得服務器得知哪一個站點是客戶端須要請求的。

其他的頭信息是用於配置 WebSocket 協議的選項。典型的一些選項就是,子協議選項 Sec-WebSocket-Protocol、列出客戶端支出的擴展 Sec-WebSocket-Extensions、源標識 Origin 等。Sec-WebSocket-Protocol 子協議選項,是用於標識客戶端想和服務端使用哪種子協議(都是應用層的協議,好比 chat 表示採用 「聊天」 這個應用層協議)。客戶端能夠在 Sec-WebSocket-Protocol 提供幾個供服務端選擇的子協議,這樣服務端從中選取一個(或者一個都不選),並在返回的握手信息中指明,好比:

Sec-WebSocket-Protocol: chat

Origin能夠預防在瀏覽器中運行的腳本,在未經 WebSocket 服務器容許的狀況下,對其發送跨域的請求。瀏覽器腳本在使用瀏覽器提供的 WebSocket 接口對一個 WebSocket 服務發起鏈接請求時,瀏覽器會在請求的 Origin 中標識出發出請求的腳本所屬的,而後 WebSocket 在接受到瀏覽器的鏈接請求以後,就能夠根據其中的源去選擇是否接受當前的請求。

好比咱們有一個 WebSocket 服務運行在 http://websocket.example.com,而後你打開一個網頁 http://another.example.com,在個 another 的頁面中,有一段腳本試圖向咱們的 WebSocket 服務發起連接,那麼瀏覽器在其請求的頭中,就會標註請求的源爲 http://another.example.com,這樣咱們就能夠在本身的服務中選擇接收或者拒絕該請求。

服務端爲了告知客戶端它已經接收到了客戶端的握手請求,服務端須要返回一個握手響應。在服務端的握手響應中,須要包含兩部分的信息。第一部分的信息來自於客戶端的握手請求中的 Sec-WebSocket-Key 頭字段:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

客戶端握手請求中的 Sec-WebSocket-Key 頭字段中的內容是採用的 base64 編碼 RFC4648 的。服務端並不須要將這個值進行反編碼,只須要將客戶端傳來的這個值首先去除首尾的空白,而後和一段固定的 GUID RFC4122 字符串進行鏈接,固定的 GUID 字符串爲 258EAFA5-E914-47DA-95CA-C5AB0DC85B11。鏈接後的結果使用 SHA-1(160數位)FIPS.180-3 進行一個哈希操做,對哈希操做的結果,採用 base64 進行編碼,而後做爲服務端響應握手的一部分返回給瀏覽器。

好比一個具體的例子:

  1. 客戶端握手請求中的 Sec-WebSocket-Key 頭字段的值爲 dGhlIHNhbXBsZSBub25jZQ==
  2. 服務端在解析了握手請求的頭字段以後,獲得 Sec-WebSocket-Key 字段的內容爲 dGhlIHNhbXBsZSBub25jZQ==,注意先後沒有空白
  3. 將 dGhlIHNhbXBsZSBub25jZQ== 和一段固定的 GUID 字符串進行鏈接,新的字符串爲 dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  4. 使用 SHA-1 哈希算法對上一步中新的字符串進行哈希。獲得哈希後的內容爲(使用 16 進制的數表示每個字節中內容):0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea
  5. 對上一步獲得的哈希後的字節,使用 base64 編碼,獲得最後的字符串s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  6. 最後獲得的字符串,須要放到服務端響應客戶端握手的頭字段 Sec-WebSocket-Accept中。

服務端的握手響應和客戶端的握手請求很是的相似。第一行是 HTTP狀態行,狀態碼是 101

HTTP/1.1 101 Switching Protocols

任何其餘的非 101 表示 WebSocket 握手尚未結束,客戶端須要使用原有的 HTTP 的方式去響應那些狀態碼。狀態行以後,就是頭字段。

Connection 和 Upgrade 頭字段完成了對 HTTP 的升級。Sec-WebSocket-Accept 中的值表示了服務端是否接受了客戶端的請求。若是它不爲空,那麼它的值包含了客戶端在其握手請求中 Sec-WebSocket-Key 頭字段所帶的值、以及一段預約義的 GUID 字符串(上面已經介紹過怎麼由兩者合成新字符串的)。任何其餘的值都被認爲服務器拒絕了請求。服務端的握手響應相似:

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

這些字符須要被 WebSocket 的客戶端(通常就是瀏覽器)檢查覈對以後,才能決定是否繼續執行相應的客戶端腳本,或者其餘接下來的動做。

可選的頭字段也能夠被包含在服務端的握手響應中。在這個版本的協議中,主要的可選頭字段就是 Sec-WebSocket-Protocol,它能夠指出服務端選擇哪個子協議。客戶端須要驗證服務端選擇的子協議,是不是其當初的握手請求中的 Sec-WebSocket-Protocol 中的一個。做爲服務端,必須確保選的是客戶端握手請求中的幾個子協議中的一個:

Sec-WebSocket-Protocol: chat
服務端也能夠設置 cookie 見RFC6265,可是這不是必須的。

1.4 關閉握手

關閉握手的操做也很簡單。

任意一端均可以選擇關閉握手過程。須要關閉握手的一方經過發送一個特定的控制序列(第 5 節會描述)去開始一個關閉握手的過程。一端一旦接受到了來自另外一端的請求關閉控制幀後,接收到關閉請求的一端若是尚未返回一個做爲響應的關閉幀的話,那麼它須要先發送一個關閉幀。在接受到了對方響應的關閉幀以後,發起關閉請求的那一端就能夠關閉鏈接了。

在發送了請求關閉控制序列以後,發送請求的一端將不能夠再發送其餘的數據內容;一樣的,一但接收到了一端的請求關閉控制序列以後,來自那一端的其餘數據內容將被忽略。注意這裏的說的是數據內容,控制幀仍是能夠響應的。不然就下面一句就沒有意義了。

兩邊同時發起關閉請求也是能夠的。

之因此須要這樣作,是由於客戶端和服務器之間可能還存在其餘的中間件。一段關閉以後,也須要通知另外一端也和中間件斷開鏈接。

1.5 設計理念

WebSocket 協議的設計理念就是提供極小的幀結構(幀結構存在的目的就是使得協議是基於幀的,而不是基於流的,同時幀能夠區分 Unicode 文本和二進制的數據)。它指望能夠在應用層中使得元數據能夠被放置到 WebSocket 層上,也就是說,給應用層提供一個將數據直接放在 TCP 層上的機會,再簡單的說就能夠給瀏覽器腳本提供一個使用受限的 Raw TCP 的機會。

從概念上來講,WebSocket 只是一個創建於 TCP 之上的層,它提供了下面的功能:

  • 給瀏覽器提供了一個基於源的安全模型(origin-based security model)
  • 給協議提供了一個選址的機制,使得在同一個端口上能夠創立多個服務,而且將多個域名關聯到同一個 IP
  • 在 TCP 層之上提供了一個相似 TCP 中的幀的機制,可是沒有長度的限制
  • 提供了關閉握手的方式,以適應存在中間件的狀況

從概念上將,就只有上述的幾個用處。不過 WebSocket 能夠很好的和 HTTP 協議一同協做,而且能夠充分的利用現有的 web 基礎設施,好比代理。WebSocket 的目的就是讓簡單的事情變得更加的簡單。

協議被設計成可擴展的,未來的版本中將極可能會添加關於多路複用的概念。

1.6 安全模型

WebSocket 協議使用源模型(origin model),這樣瀏覽器中的一個頁面中的腳本須要訪問其餘源的資源時將會有所限制。若是是在一個 WebSocket 客戶端中直接使用了 WebSocet(而不是在瀏覽器中),源模型就沒有什麼做用,由於客戶端能夠設置其爲任意的值。

而且協議的設計目的也是不但願干擾到其餘協議的工做,由於只有經過特定的握手步驟才能創建 WebSocket 鏈接。另外因爲握手的步驟,其餘已經存在的協議也不會干擾到 WebSocket 協議的工做。好比在一個 HTTP 表單中,若是表單的地址是一個 WebSocket 服務的話,將不會創建鏈接,由於到目前本文成文爲止,在瀏覽器中是不能夠經過 HTML 和 Javascript APIs 去設置 Sec- 頭的。

1.7 和 TCP 以及 HTTP 之間的關係

WebSocket 是一個獨立的基於 TCP 的協議,它與 HTTP 之間的惟一關係就是它的握手請求能夠做爲一個升級請求(Upgrade request)經由 HTTP 服務器解釋(也就是可使用 Nginx 反向代理一個 WebSocket)。

默認狀況下,WebSocket 協議使用 80 端口做爲通常請求的端口,端口 443 做爲基於傳輸加密層連(TLS)RFC2818 接的端口

1.8 創建一個鏈接

由於 WebSocket 服務一般使用 80 和 443 端口,而 HTTP 服務一般也是這兩個端口,那麼爲了將 WebSocket 服務和 HTTP 服務部署到同一個 IP 上,能夠限定流量從同一個入口處進入,而後在入口處對流量進行管理,概況的說就是使用反向代理或者是負載均衡。

1.9 WebSocket 協議的子協議

在使用 WebSocket 協議鏈接到一個 WebSocket 服務器時,客戶端能夠指定其 Sec-WebSocket-Protocol 爲其所指望採用的子協議集合,而服務端則能夠在此集合中選取一個並返回給客戶端。

這個子協議的名稱應該遵循第 11 節中的內容。爲了防止潛在的衝突問題,應該在域名的基礎上加上服務組織者的名稱(或者服務名稱)以及協議的版本。好比 v2.bookings.example.net 對應的就是 版本號-服務組織(或服務名)-域名

2 一致性的要求

見原文 section-2

3 WebSocket URIs

在這份技術說明中,定義了兩種 URI 方案,使用 ABNF 語法 RFC 5234,以及 URI 技術說明 RFC3986 中的生產式。

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。

若是資源標識符(URI)的方案(scheme)部分使用的是大小寫不敏感的 「wss」 的話,那麼就說這個 URI 是 「可靠的 secure」,而且說明 「可靠標記(secure flag)已經被設置」。

「資源名稱 resource-name」 也就是 4.1 節中的 /resource name/,能夠按下面的部分(順序)鏈接:

  • 若是不用路徑不爲空,加上 「/」
  • 緊接着就是路徑部分
  • 若是查詢組件不爲空 ,加上 「?「
  • 緊接着就是查詢部分

片斷標識符(fragment identifier) 「#」 在 WebSocket URIs 的上下文是沒有意義的,不能出如今 URIs 中。在 WebSocket 的 URI 中,若是出現了字符 「#」 須要使用 %23 進行轉義。

4.1 客戶端要求

爲了創建一個 WebSocket 鏈接,由客戶端打開一個鏈接而後發送這一節中定義的握手信息。鏈接初始的初始狀態被定義爲 「鏈接中 CONNECTING」。客戶端須要提供 /host/,/port/,/resource name/ 和 /secure/ 標記,這些都是上一節中的 WebSocket URI 中的組件,若是有的話,還須要加上使用的 /protocols/ 和 /extensions/。另外,若是客戶端是瀏覽器,它還須要提供 /origin/。

鏈接開始前須要的設定信息爲(/host/, /port/, /resource name/ 和 /secure/)以及須要使用的 /protocols/ 和 /extensions/,若是在瀏覽器下還有 /origin/。這些設定信息選定好了以後,就必須打開一個網絡鏈接,發送握手信息,而後讀取服務端返回的握手信息。具體的網絡鏈接應該如何被打開,如何發送握手信息,如何解釋服務端的握手響應,這些將在接下來的部分討論。咱們接下來的文字中,將使用第 3 節中定義的項目名稱,好比 「/host/」 和 「/secure/」。

  1. 在解析 WebSocket URI 的時候,須要使用第 3 節中提到的技術說明去驗證其中的組件。若是包含了任何無效的 URI 組件,客戶端必須將鏈接操做標記爲失敗,並中止接下來的步驟
  2. 能夠經過 /host/ 和 /port/ 這一對 URI 組件去標識一個 WebSocket 鏈接。這一部分的意思就是,若是能夠肯定服務端的 IP,那麼就使用 「服務端 IP + port」 去標識一個鏈接。這樣的話,若是已經存在一個鏈接是 「鏈接中 CONNECTING」 的狀態,那麼其餘具備相同標識的鏈接必須等待那個正在鏈接中的鏈接完成握手後,或是握手失敗後關閉了鏈接後,才能夠嘗試和服務器創建鏈接。任什麼時候候只能有一個具備相同的標識的鏈接是 「正在鏈接中」 的狀態。

    可是若是客戶端沒法知道服務器的IP(好比,全部的鏈接都是經過代理服務器完成的,而 DNS 解析部分是交由代理服務器去完成),那麼客戶端就必須假設每個主機名稱對應到了一個獨立服務器,而且客戶端必須對同時等待鏈接的的鏈接數進行控制(好比,在沒法獲知服務器 IP 的狀況下,能夠認爲 a.example.com 和 b.example.com 是兩臺不一樣的服務器,可是若是每臺服務器都有三十個須要同時發生的鏈接的話,可能就應該不被容許)

    注意:這就使得腳本想要執行 「拒絕服務攻擊 denial-of-service attack」 變得困難,否則的話腳本只須要簡單的對一個 WebSocket 服務器打開不少的鏈接就能夠了。服務端也能夠進一步的有一個隊列的概念,這樣將暫時沒法處理的鏈接放到隊列中暫停,而不是將它們馬上關閉,這樣就能夠減小客戶端重連的比率。

    注意:對於客戶端和服務器之間的鏈接數是沒有限制的。在一個客戶端請數目(根據 IP)達到了服務端的限定值或者服務端資源緊缺的時候,服務端能夠拒絕或者關閉客戶端鏈接。

  3. 使用代理:若是客戶端但願在使用 WebSocket 的時候使用代理的話,客戶端須要鏈接到代理服務器並要求代理服務器根據其指定的 /host/,/port/ 對遠程服務器打開一個 TCP 鏈接,有興趣的能夠看 Tunneling TCP based protocols through Web proxy servers

    若是可能的話,客戶端能夠首選適用於 HTTPS 的代理設置。

    若是但願使用 PAC 腳本的話,WebSocket URIs 必須根據第 3 節說的規則。

    注意:在使用 PAC 的時候,WebSocket 協議是能夠特別標註出來的,使用 「ws」 和 「wss」。

  4. 若是網絡鏈接沒法打開,不管是由於代理的緣由仍是直連的網絡問題,客戶端必須將鏈接動做標記爲失敗,並終止接下來的行爲。

  5. 若是設置了 /secure/,那麼客戶端在和服務端創建了鏈接以後,必需要先進行 TLS 握手,TLS 握手成功後,才能夠進行 WebSocket 握手。若是 TLS 握手失敗(好比服務端證書不能經過驗證),那麼客戶端必須關閉鏈接,終止其後的 WebSocket 握手。在 TLS 握手成功後,全部和服務的數據交換(包括 WebSocket 握手),都必須創建在 TLS 的加密隧道上。

    客戶端在使用 TLS 時必須使用 「服務器名稱標記擴展 Server Name Indication extension」 RFC6066

一旦客戶端和服務端的鏈接創建好(包括經由代理或者經過 TLS 加密隧道),客戶端必須向服務端發送 WebSocket 握手信息。握手內容包括了 HTTP 升級請求和一些必選以及可選的頭字段。握手的細節以下:

  1. 握手必須是一個有效的 HTTP 請求,有效的 HTTP 請求的定義見 RFC2616

  2. 請求的方法必須是 GET,而且 HTTP 的版本必須至少是 1.1

    好比,若是 WebSocket 的 URI 是 ws://example.com/chat,那麼請求的第一行必須是 GET /chat HTTP/1.1

  3. 請求的 Request-URI 部分必須遵循第 3 節中定義的 /resource name/ 的定義。可使相對路徑或者絕對路徑,好比:

    相對路徑:GET /chat HTTP/1.1 中間的 /chat 就是請求的 Request-URI,也是 /resource name/
    絕對路徑:GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1,其中的 /resource name/ 就是 /pub/WWW/TheProject.html 感謝 @forl 的指正

    絕對路徑解析以後會有 /resource name/,/host/ 或者可能會有 /port/。/resource name/ 可能會有查詢參數的,只不過例子中沒有。

  4. 請求必須有一個 |Host| 頭字段,它的值是 /host/ 主機名稱加上 /port/ 端口名稱(當不是使用的默認端口時必須顯式的指明)

  5. 請求必須有一個 |Upgrade| 頭字段,它的值必須是 websocket 這個關鍵字(keyword)

  6. 請求必須有一個 |Connection| 頭字段,它的值必須是 Upgrade 這個標記(token)

  7. 請求必須有一個 |Sec-WebSocket-Key| 頭字段,它的值必須是一個噪音值,由 16 個字節的隨機數通過 base64 編碼而成。每一個鏈接的噪音必須是不一樣且隨機的。

    注意:做爲一個例子,若是選擇的隨機 16 個字節的值是 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10,那麼頭字段中的值將是 AQIDBAUGBwgJCgsMDQ4PEC==

  8. 若是鏈接來自瀏覽器客戶端,那麼 |Origin| RFC6454 就是必須的。若是鏈接不是來自於一個瀏覽器客戶端,那麼這個值就是可選的。這個值表示的是發起鏈接的代碼在運行時所屬的源。關於源是由哪些部分組成的,見 RFC6454

    做爲一個例子,若是代碼是從 http://cdn.jquery.com 下載的,可是運行時所屬的源是 http://example.com,若是代碼向 ww2.example.com 發起鏈接,那麼請求中 |Origin| 的值將是 http://example.com

  9. 請求必須有一個 |Sec-WebSocket-Version| 頭字段,它的值必須是 13

  10. 請求能夠有一個可選的頭字段 |Sec-WebSocket-Protocol|。若是包含了這個頭字段,它的值表示的是客戶端但願使用的子協議,按子協議的名稱使用逗號分隔。組成這個值的元素必須是非空的字符串,而且取值範圍在 U+0021 到 U+007E 之間,不能夠包含定義在 RFC2616 的分隔字符(separator character),而且每一個以逗號分隔的元素之間必須相互不重複。

  11. 請求能夠有一個可選的頭字段 |Sec-WebSocket-Extensions|。若是包含了這個字段,它的值表示的是客戶端但願使用的協議級別的擴展,具體的介紹以及它的格式在第 9 節

  12. 請求能夠包含其餘可選的頭字段,好比 cookies RFC6265,或者認證相關的頭字段,好比 |Authorization| 定義在 RFC2616,它們的處理方式就參照定義它們的技術說明中的描述。

一旦客戶端的握手請求發送完成後,客戶端必須等待服務端的握手響應,在此期間不能夠向服務器傳輸任何數據。客戶端必須按照下面的描述去驗證服務端的握手響應:

  1. 若是服務端傳來的狀態碼不是 101,那麼客戶端能夠按照通常的 HTTP 請求處理狀態碼的方式去處理。好比服務端傳來 401 狀態碼,客戶端能夠執行一個受權驗證;或者服務端回傳的是 3xx 的狀態碼,那麼客戶端能夠進行重定向(可是客戶端不是非得這麼作)。若是是 101 的話,就接着下面的步驟。

  2. 若是服務端回傳的握手中沒有 |Upgrade| 頭字段或者 |Upgrade| 都字段的值不是 ASCII 大小寫不敏感的 websocket 的話,客戶端必須標記 WebSocket 鏈接爲失敗。

  3. 若是服務端回傳的握手中沒有 |Connection| 頭字段或者 |Connection| 的頭字段內容不是大小寫敏感的 Upgrade 的話,客戶端必須表示 WebSocket 鏈接爲失敗。

  4. 若是服務端的回傳握手中沒有 |Sec-WebSocket-Accept| 頭字段或者 |Sec-WebSocket-Accept| 頭字段的內容不是 |Sec-WebSocket-Key| 的內容(字符串,不是 base64 解碼後的)聯結上字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 的字符串進行 SHA-1 得出的字節再 base64 編碼獲得的字符串的話,客戶端必須標記 WebSocket 鏈接爲失敗。

    簡單的說就是客戶端也必須按照服務端生成 |Sec-WebSocket-Accept| 頭字段值的方式也生成一個字符串,與服務端回傳的進行對比,若是不一樣就標記鏈接爲失敗的。

  5. 若是服務端回傳的 |Sec-WebSocket-Extensions| 頭字段的內容不是客戶端握手請求中的擴展集合中的元素或者 null 的話,客戶端必須標記鏈接爲失敗。這個頭字段的解析規則在第 9 節中進行了描述。

    好比客戶端的握手請求中的指望使用的擴展集合爲:

    Sec-WebSocket-Extensions: bar; baz=2

    那麼服務端能夠選擇使用其中的某個(些)擴展,經過在回傳的 |Sec-WebSocket-Extensions| 頭字段中代表:

    Sec-WebSocket-Extensions: bar; baz=2

    上面的服務端返回表示都使用。也可使用其中的一個:

    Sec-WebSocket-Extensions: bar

    若是服務端但願表示一個都不使用,即表示 null,那麼服務端回傳的信息中將不能夠包含 |Sec-WebSocket-Extensions|。

    失敗的界定就是,若是客戶端握手請求中有 |Sec-WebSocket-Extensions|,可是服務端返回的 |Sec-WebSocket-Extensions| 中包含了客戶端請求中沒有包含的值,那麼必須標記鏈接爲失敗。服務端的返回中不包含 |Sec-WebSocket-Extensions| 是能夠的,表示客戶端和服務端之間將不使用任何擴展。

  6. 若是客戶端在握手請求中包含了子協議頭字段 |Sec-WebSocket-Protocol|,其中的值表示客戶端但願使用的子協議的集合。若是服務端回傳信息的 |Sec-WebSocket-Protocol| 值不屬於客戶端握手請求中的子協議集合的話,那麼客戶端必須標記鏈接爲失敗。

若是服務端的握手響應不符合 4.2.2 小節中的服務端握手定義的話,客戶端必須標記鏈接爲失敗。

請注意,根據 RFC2616 技術說明,請求和響應的中全部頭字段的名稱都是大小寫不敏感的(不區分大小寫)。

若是服務端的響應符合上述的描述的話,那麼就說明 WebSocket 的鏈接已經創建了,而且鏈接的狀態變爲 「OPEN 狀態」。另外,服務端的握手響應中也能夠包含 cookie 信息,cookie 信息被稱爲是 「服務端開始握手的 cookie 設置」。

4.2 服務端要求

WebSocket 服務器可能會卸下一些對鏈接的管理操做,而將這些管理操做交由網絡中的其餘代理,好比負載均衡服務器或者反向代理服務器。對於這種狀況,在這個技術說明中,將組成服務端的基礎設施的全部部分合起來視爲一個總體。

好比,在一個數據中心,會有一個服務器專門用戶響應客戶端的握手請求,在握手成功以後將鏈接轉交給實際處理任務的服務器。在這份技術說明中,服務端指代的就是這裏的兩臺機器的組成的總體。

4.2.1 讀取客戶端的握手請求

當客戶端發起一個 WebSocket 請求時,它會發送握手過程種屬於它那一部分的內容。服務端必須解析客戶端提交的握手請求,以從中得到生成服務端響應內容的必要的信息。

客戶端的握手請求有接下來的幾部分構成。服務端在讀取客戶端請求時,發現握手的內容和下面的描述不相符(注意 RFC2616,頭字段的順序是不重要的),包括但不限於那些不符合相關 ABNF 語法描述的內容時,必須中止對請求的解析並返回一個具備適當的狀態碼 HTTP 響應(好比 400 Bad Request)。

  1. 必須是 HTTP/1.1 或者以上的 GET 請求,包含一個 「請求資源標識符 Request-URI」,請求資源標識符遵循第 3 節中定義的 /resource name/。

  2. 一個 |Host| 頭字段,向服務器指明須要訪問的服務名稱(域名)

  3. 一個 |Upgrade| 頭字段,值爲大小寫不敏感的 websocket 字符串

  4. 一個 |Connection| 頭字段,它的值是大小寫不敏感的字符串 Upgrade

  5. 一個 |Sec-WebSocket-Key| 頭字段,它的值是一段使用 base64 編碼Section 4 of [RFC4648] 後的字符串,解碼以後是 16 個字節的長度。

  6. 一個 |Sec-WebSocket-Version| 頭字段,它的值是 13.

  7. 可選的,一個 |Origin| 頭字段。這個是全部瀏覽器客戶度必須發送的。若是服務端限定只能由瀏覽器做爲其客戶端的話,在缺乏這個字段的狀況下,能夠認定這個握手請求不是由瀏覽器發起的,反之則不行。

  8. 可選的,一個 |Sec-WebSocket-Protocol| 頭字段。由一些值組成的列表,這些值是客戶端但願使用的子協議,按照優先級從左往右排序。

  9. 可選的,一個 |Sec-WebSocket-Extensions| 頭字段。有一些值組成的列表,這些值是客戶端但願使用的擴展。具體的表示在第 9 節。

  10. 可選的,其餘頭字段,好比那些用於向服務端發送 cookie 或則認證信息。未知的頭字段將被忽略 RFC2616

4.2.2 發送服務端的握手響應

當客戶端對服務端創建了一個 WebSocket 鏈接以後,服務端必須完成接下來的步驟,以此去接受客戶端的鏈接,並回應客戶端的握手。

  1. 若是鏈接發生在 HTTPS(基於 TLS 的 HTTP)端口上,那麼要執行一個 TLS 握手。若是 TLS 握手失敗,就必須關閉鏈接;不然的話以後的全部通訊都必須創建在加密隧道上。

  2. 服務端能夠對客戶端執行另外的受權認證,好比經過返回 401 狀態碼和 對應的 |WWW-Authenticate|,相關描述在 RFC2616

  3. 服務端也能夠對客戶端進行重定向,使用 3xx 狀態碼 RFC2616。注意這一步也能夠發生在上一步以前。

  4. 確認下面的信息:

    /origin/
    客戶端握手請求中的 |origin| 頭字段代表了腳本在發起請求時所處的源。源被序列化成 ASCII 而且被轉換成了小寫。服務端能夠選擇性地使用這個信息去決定是否接受這個鏈接請求。若是服務端不驗證源的話,那麼它將接收來自任何地方的請求。
    若是服務端不想接收這個鏈接的話,它必須返回適當的 HTTP 錯誤狀態碼(好比 403 Forbidden)而且終止接下來的 WebSocket 握手過程。更詳細的內容,見第 10/key/ 客戶端握手請求中的 |Sec-WebSocket-Key| 頭字段包含了一個使用 base64 編碼後的值,若是解碼的話,這個值是 16 字節長的。這個編碼後的值用於服務端生成表示其接收客戶端請求的內容。服務端沒有必要去將這個值進行解碼。 /version/ 客戶端握手請求中的 |Sec-WebSocket-Version| 頭字段包含了客戶端但願進行通訊的 WebSocket 協議的版本號。若是服務端不能理解這個版本號的話,那麼它必須終止接下來的握手過程,並給客戶端返回一個適當的 HTTP 錯誤狀態碼
    (好比 426 Upgrade Required),同時在返回的信息中包含一個 |Sec-WebSocket-Version| 頭字段,經過其值指明服務端可以理解的協議版本號。 /subprotocol/ 服務端能夠選擇接受其中一個子協議,或者 null。子協議的選取必須來自客戶端的握手信息中的 |Sec-WebSocket-Protocol| 頭字段的元素集合。若是客戶端沒有發送 |Sec-WebSocket-Protocol| 頭字段,或者客戶端發送的 |Sec-WebSocket-Protocol|
    頭字段中沒有一個能夠被當前服務端接受的話,服務端惟一能夠返回值就是 null。不發送這個頭字段就表示其值是 null。注意,空字符串並不表示這裏的 null 而且根據 RFC2616 中的 ABNF 定義,空字符串也是不合法的。根據協議中的描述,客戶端握手請求中的
    |Sec-WebSocket-Protocol| 是一個可選的頭字段,因此若是服務端必須使用這個頭字段的話,能夠選擇性的拒絕客戶端的鏈接請求。 /extensions/ 一個能夠爲空的列表,表示客戶端但願使用的協議級別的擴展。若是服務端支持多個擴展,那麼必須從客戶端握手請求中的 |Sec-WebSocket-Extensions| 按需選擇多個其支持的擴展。若是客戶端沒有發送次頭字段,則表示這個字段的值是 null
    空字符並不表示 null。返回的 |Sec-WebSocket-Extensions| 值中不能夠包含客戶端不支持的擴展。這個字段值的選擇和解釋將在第 9 節中討論

     

  5. 若是服務端選擇接受來自客戶端的鏈接,它必須回答一個有效的 HTTP 響應:

    一個狀態行,包含了響應碼 101。好比 HTTP/1.1 101 Switching Protocols
    一個 |Upgrade| 頭字段,值爲 websocket
    一個 |Connection| 頭字段,值爲 Upgrade
    一個 |Sec-WebSocket-Accept| 頭字段。這個值經過鏈接定義在 4.2.2 節中的第 4 步的 /key/ 和字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,鏈接後的字符串運用 SHA-1 獲得一個 20 字節的值,
    最後使用 base64 將這 20 個字節的內容編碼,獲得最後的用於返回的字符串。 相應的 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| 頭字段,它的值已經在第 4.2.2 節中的第 4 步定義了 可選的,一個 |Sec-WebSocket-Extensions| 頭字段,它的值已經在第4.2.2 節中的第 4 步定義了。若是有服務端選擇了多個擴展,能夠將它們分別放在 |Sec-WebSocket-Extensions| 頭字段中,或者合併到一塊兒放到一個
    |Sec-WebSocket-Extensions| 頭字段中。

這樣就完成了服務端的握手。若是服務端沒有發生終止的完成了全部的握手步驟,那麼服務端就能夠認爲鏈接已經創建了,而且 WebSocket 鏈接的狀態變爲 OPEN。在這時,客戶端和服務端就能夠開始發送(或者接收)數據了。

4.3 握手中使用的新的頭字段的 ABNF

這一節中將使用定義在 Section 2.1 of [RFC2616] ABNF 語法/規則,包括隱含的 LWS 規則(implied *LWS rule)。爲了便於閱讀,這裏給出 LWS 的簡單定義:任意數量的空格,水平 tab 或者換行(換行指的是 CR(carriage return) 後面跟着 LF(linefeed),使用轉義字符表示就是 \r\n)。

注意,接下來的一些 ABNF 約定將運用於這一節。一些規則的名稱與與之對應的頭字段相關。這些規則表示相應的頭字段的值的語法,好比 Sec-WebSocket-Key ABNF 規則,它描述了 |Sec-WebSocket-Key| 頭字段的值的語法。名字中具備 -Client 後綴的 ABNF 規則,表示的是客戶端向服務端發送請求時的字段值語法;名字中具備 -Server 後綴的 ABNF 規則,表示的是服務端向客戶端發送請求時的字段值語法。好比 ABNF 規則 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
View Code

下面的新字段能夠在握手期間由服務端發往客戶端:

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 協議

這一節對在客戶端和服務端之間提供多個版本的 WebSocket 協議提供了一些指導意見。

使用 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

客戶端如今就能夠從新採用版本 13 (若是客戶端也支持的話)進行握手請求了:

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

5. 使用幀去組織數據

5.1 概覽

在 WebSocket 協議中,數據的傳輸使用一連串的幀。爲了使得中間件不至於混淆(好比代理服務器)以及爲了第 10.3 節將討論安全緣由,客戶端必須將要發送到服務端的幀進行掩碼,掩碼將在第 5.3 節詳細討論。(注意,無論 WebSocket 有沒有運行在 TLS 之上,都必須有掩碼操做)服務端一旦接收到沒有進行掩碼的幀的話,必須關閉鏈接。這種狀況下,服務端能夠發送一個關閉幀,包含一個狀態碼 1002(協議錯誤 protocol error),相關定義在 Section 7.4.1。服務端沒必要對發送到客戶端的任何幀進行掩碼。若是客戶端接收到了服務端的掩碼後的幀,客戶端必須關閉鏈接。在這個狀況下,客戶端能夠向服務器發送關閉幀,包含狀態碼 1002(協議錯誤 protocol error),相關定義在 Section 7.4.1。(這些規則可能在未來技術說明中沒有嚴格要求)

基礎幀協議經過操做碼(opcode)定義了一個幀類型,一個有效負荷長度,以及特定的位置存放 「擴展數據 Extension data」 和 「應用數據 Application data」,擴展數據和應用數據合起來定義了 「有效負荷數據 Payload data」。某些數位和操做碼是保留的,爲了未來的使用。

在客戶端和服務端完成了握手以後,以及任意一端發送的關閉幀(在第 5.5.1 節介紹)以前,客戶端能夠和服務端均可以在任什麼時候間發送數據幀。

基礎幀協議

這一節中將使用 ABNF 詳細定義數據傳輸的格式。(注意,和這文檔中的其餘 ABNF 不一樣,這一節中 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 ...                |
 +---------------------------------------------------------------+
View Code
  • FIN: 1 個數位(bit)

    標記這個幀是否是消息中的最後一幀。第一個幀也能夠是最後一幀。

  • RSV1,RSV2,RSV3: 各 1 個數位

    必須是 0,除非有擴展賦予了這些數位非 0 值的意義。若是接收到了一個非 0 的值而且沒有擴展賦予這些非 0 值的意義,那麼接收端須要標記鏈接爲失敗。

  • 操做碼:4 個數位
    定義瞭如何解釋 「有效負荷數據 Payload data」。若是接收到一個未知的操做碼,接收端必須標記 WebSocket 爲失敗。定義了以下的操做碼:

    • %x0 表示這是一個繼續幀(continuation frame)
    • %x1 表示這是一個文本幀 (text frame)
    • %x2 表示這是一個二進制幀 (binary frame)
    • %x3-7 爲未來的非控制幀(non-control frame)而保留的
    • %x8 表示這是一個鏈接關閉幀 (connection close)
    • %x9 表示這是一個 ping 幀
    • %xA 表示這是一個 pong 幀
    • xB-F 爲未來的控制幀(control frame)而保留的
  • 掩碼標識 Mask:1 個數位

    定義了 「有效負荷數據」 是不是被掩碼的。若是被設置爲 1,那麼在 masking-key 部分將有一個掩碼鑰匙(masking key),而且使用這個掩碼鑰匙去將 「有效負荷數據」 進行反掩碼操做(第 5.3 節描述)。全部的由客戶端發往服務端的幀此數位都被設置成 1。

  • 有效負荷長度(Payload length): 七、7+16 或者 7+64 數位

    表示了 「有效負荷數據 Payload data」 的長度,以字節爲單位:若是是 0-125,那麼就直接表示了負荷長度。若是是 126,那麼接下來的兩個字節表示的 16 位無符號整型數則是負荷長度。若是是 127,則接下來的 8 個字節表示的 64 位無符號整型數則是負荷長度。表示長度的數值的字節是按網絡字節序(network byte order 即大端序)表示的。注意在全部狀況下,必須使用最小的負荷長度,好比,對於一個 124 字節長度的字符串,長度不能夠編碼成 126,0,124。負荷長度是 「擴展數據 Extension data」 長度 + 「應用數據Application data」 長度 。「擴展數據」 的長度能夠是 0,那麼此時 「應用數據」 的長度就是負荷長度。

  • 掩碼鑰匙 Masking key:0 或者 4 個數位

    全部由客戶端發往服務端的幀中的內容都必須使用一個 32 位的值進行掩碼。這個字段有值的時候(佔 4 個數位)僅當掩碼標識位設置成了 1,若是掩碼標識位設置爲 0,則此字段沒有值(佔 0 個數位)。對於進一步掩碼操做,見第 5.3 節。

  • 有效負荷數據 Payload data:(x+y) 字節 byte

    「有效負荷數據」 的定義是 「擴展數據」 聯合 「應用數據」。

  • 擴展數據 Extension data: x 字節

    「擴展數據是」 0 個字節的,除非協商了一個擴展。任何的擴展都必須提供 「擴展數據」 的長度或者該長度應該如何計算,以及在握手階段如何使用 「擴展數據」 進行擴展協商。若是 「擴展數據」 存在,那麼它的長度被包含在了負荷長度中。

  • 應用數據 Application data: y字節

    能夠是任意的 「應用數據」,它在一個幀的範圍內緊接着 「擴展數據」。「應用數據」 的長度等於負荷長度減去 「擴展數據」 的長度

基礎幀協議經過接下來的 ABNF RFC5234 來定義其形式。一個重要的注意點就是下面的 ABNF 表示的是二進制數據,而不是其表面上的字符串。好比, %x0 和 %x1 各表示一個數位,數位上的值爲 0 和 1,而不是表示的字符 「0」 和 「1」 的 ASCII 編碼。RFC5234 沒有定義 ABNF 的字符編碼。在這裏,ABNF 被特定了使用的是二進制編碼,這裏二進制編碼的意思就是每個值都被編碼成具備特定數量的數位,具體的數量因不一樣的字段而異。

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
View Code

5.3 客戶端到服務端掩碼

一個被掩碼的幀須要將掩碼標識位(第 5.2 節定義)設置爲 1。

掩碼鑰匙 masking key 整個都在幀中,就像第 5.2 節定義的。它用於對 「有效負荷數據」 進行掩碼操做,包括 「擴展數據」 和 「應用數據」。

掩碼鑰匙由客戶端隨機選取一個 32 位的值。在每次準備對幀進行掩碼操做時,客戶端必須選擇在可選的 32 位數值集合中選取一個新的掩碼鑰匙。掩碼鑰匙的值須要是不可被預測的;所以,掩碼鑰匙必須來源於一個具備很強保密性質的生成器,而且 服務器/代理 不可以輕易的預測到一連串的幀中使用的掩碼鑰匙。不可預測的掩碼鑰匙能夠防止惡意程序在幀的傳輸過程當中探測到掩碼鑰匙的內容。RFC4086 具體討論了爲何對於一個安全性比較敏感的應用程序須要使用一個很強保密性質的生成器。

掩碼不會影響 「有效負載數據」 的長度。爲了將掩碼後的數據進行反掩碼,或者倒過來,可使用下面的算法。一樣的算法適用於不一樣方向發來的幀,好比,對於掩碼和反掩碼使用相同的步驟。

傳輸數據中的每 8 個數位的字節 i (transformed-octet-i),生成方式是經過原數據中的每 8 個數位的字節 i (original-octet-i)與以 i 與 4 取模後的數位爲索引的掩碼鑰匙中的 8 爲字節 j(masking-key-octet-j) 進行異或(XOR)操做:

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

負載的長度不包括掩碼鑰匙的長度,它是 「有效負載數據 Payload data」 的長度,好比,位於掩碼鑰匙後的字節的長度。

5.4 消息碎片化

消息碎片化的目的就是容許發送那些在發送時不知道其緩衝的長度的消息。若是消息不能被碎片化,那麼一端就必須將消息整個地載入內存緩衝,這樣在發送消息前才能夠計算出消息的字節長度。有了碎片化的機制,服務端或者中間件就能夠選取其適用的內存緩衝長度,而後當緩衝滿了以後就發送一個消息碎片。

碎片機制帶來的另外一個好處就是能夠方便實現多路複用。沒有多路複用的話,就須要將一整個大的消息放在一個邏輯通道中發送,這樣會佔用整個輸出通道。多路複用須要能夠將消息分割成小的碎片,使這些小的碎片能夠共享輸出通道。(注意多路複用的擴展在這片文檔中並無進行描述)

除非運用了特定的擴展,不然幀是沒有特定的語義的。在客戶端和服務端協商了某個擴展,或者客戶端和服務端沒有協商擴展的狀況下,中間件都有可能將幀進行 合併/分隔。也就是說,在客戶端和服務端沒有協商某個擴展時,雙方都不該該猜想幀與幀之間的邊界。注:這裏的某個擴展的意思就是賦予了幀特定的語義的擴展,好比多路複用擴展。

下面的規則解釋瞭如何進行碎片化:

  • 一個沒有被碎片化的消息只包含一個幀,而且幀的 FIN 數位被設置爲 1,且操做碼 opcode 不爲 0。

  • 一個碎片化的消息包含了一個 FIN 未被置爲 0 的幀,且這個幀的 opcode 不爲 0,在這個幀以後,將有 0 個或者多個 FIN 爲 0 且 opcode 爲 0 的幀,最後以一個 FIN 爲 1 和 opcode 爲 0 的幀結束。對於一個碎片化後的消息,它的有效負荷就等於將碎片化後的幀的有效負荷按順序鏈接起來;不過當存在擴展時,這一點就不必定正確了,由於擴展可能會設置幀的 「擴展數據」。在沒有 「擴展數據」 的狀況下,下面的例子演示了碎片化是如何工做的。

    例子:對於一個以三個幀發送的文本消息,其第一個幀的 opcode 是 0x1 而且 FIN 位是 0,第二個幀的 opcode 是 0x0 且 FIN 位是 0,第三個幀的 opcode 是 0x0 且 FIN 位是 1。

  • 控制幀(見第 5.5 節),可能會夾雜在消息幀之間。控制幀是不能被碎片化的。

  • 消息幀必須以其被髮送時的順序傳遞到接收端。

  • 不一樣消息的消息幀之間不能夠相互夾雜,除非協商了一個定義瞭如何解釋這種夾雜行爲的擴展。

  • 發送端能夠建立任意大小的非控制幀。

  • 客戶端和服務端必須支持發送和接受碎片化或者非碎片化的消息。

  • 一個控制幀是不能夠被碎片化的,中間件必須不能夠試圖將控制幀進行碎片化。

  • 若是幀中使用了 RSV 數位,可是中間件不理解其中的任意的 RSV 數位 的值時,它必須不能夠改變消息的原有的碎片化幀。

  • 在中間件不能肯定客戶端和服務端進行了哪些擴展協商的狀況下,中間件必須不能夠修改原有的碎片化幀。

  • 最後,組成消息的全部幀都是相同的數據類型,在第一個幀中的 opcode 中指明。由於控制幀不能被碎片化,組成消息的碎片類型必須是文本、二進制、或者其餘的保留類型。

注意:若是控制幀不能夾雜在消息幀的話,那麼將致使 ping 的結果產生延遲,好比在處理了一個很是長的消息後才響應 ping 控制幀時。所以,要求在處理消息幀的期間能夠響應控制幀。

重點注意:在沒有擴展的狀況下,接收端爲了處理消息不是非得緩衝全部的幀。好比若是使用了 流API (streaming API),數據幀能夠直接傳遞給應用層。不過這樣假設並不必定在全部的擴展中都適用。

5.5 控制幀

控制幀是經過它的 opcode 的最高有效位是 1 去肯定的。當前已經定義了的控制幀包括 0x8 (close)0x9 (Ping)0xA (Pong)。操做碼 0xB-0xF 是爲未來的控制幀保留的,目前還沒有定義。

控制幀是爲了在 WebSocket 中通訊鏈接狀態。控制幀能夠夾雜在消息幀之間發送。

全部的控制幀的負載長度都必須是 125 字節,而且不能被碎片化。

關閉幀

關閉幀的操做碼 opcode 是 0x8

關閉幀能夠包含消息體(經過幀的 「應用數據」 部分)去表示關閉的緣由,好比一端正在關閉服務,一端接收到的幀過大,或者一端接收到了不遵循格式的幀。若是有消息體的話,消息體的前兩個字節必須是無符號的整型數(採用網絡字節序),以此整型數去表示狀態碼 /code/ 定義在第 7.4 節。在兩個字節的無符號整型數以後,能夠跟上以 UTF-8 編碼的數據表示 /reason/,/reason/ 數據的具體解釋方式此文檔並無定義。而且 /reason/ 的內容不必定是人類可讀的數據,只要是有利於發起鏈接的腳本進行調試就能夠。由於 /reason/ 並不必定就是人類可讀的,因此客戶端必須不將此內容展現給最終用戶。

客戶端發送的每個幀都必須按照第 5.3 節中的內容進行掩碼。

應用程序在發送了關閉幀以後就不能夠再發送其餘數據幀了。

若是接收到關閉幀的一端以前沒有發送過關閉幀的話,那麼它必須發送一個關閉幀做爲響應。(當發送一個關閉幀做爲響應的時候,發送端一般在做爲響應的關閉幀中採用和其接收到的關閉幀相同的狀態碼)。而且響應必須儘快的發送。一端能夠延遲關閉幀的發送,好比一個重要的消息已經發送了一半,那麼能夠在消息的剩餘部分發送完以後再發送關閉幀。可是做爲首先發送了關閉幀,並在等待另外一端進行關閉響應的那一端來講,並不必定保證其會繼續處理數據內容。

在發送和接收到了關閉幀以後,一端就能夠認爲 WebSocket 鏈接已經關閉,而且必須關閉底層相關的 TCP 鏈接。若是是服務端首先發送了關閉幀,那麼在接收到客戶端返回的關閉幀以後,服務端必須當即關閉底層相關的 TCP 鏈接;可是若是是客戶端首先發送了關閉幀,並接收到了服務端返回的關閉幀以後,能夠選擇其認爲合適的時間關閉鏈接,好比,在一段時間內沒有接收到服務端的 TCP 關閉握手。

若是客戶端和服務端同時發送了關閉消息,那麼它們兩端都將會接收到來自對方的關閉消息,那麼它們就能夠認爲 WebSocket 鏈接已經關閉,而且關閉底層相關的 TCP 鏈接。

5.5.2 Ping

Ping 幀的操做碼是 0x9

Ping 幀也能夠有 「應用數據」

一旦接收到了 Ping 幀,接收到的一端必須發送一個 Pong 幀做爲響應,除非它已經接收到了關閉幀。響應的一端必須儘快的作出響應。Pong 幀定義在第 5.5.3 節。

一端能夠在鏈接創建以後,到鏈接關閉以前的任意時間點發送 Ping 幀。

注意:Ping 幀的目的能夠是保持鏈接(keepalive)或者是驗證服務端是否仍是有響應的。

5.5.3 Pong

Pong 幀的操做碼是 0xA

第 5.5.2 節的要求同時適用於 Ping 幀和 Pong 幀。

Pong 幀的 「應用數據」 中的內容必須和其響應的 Ping 幀中的 「應用數據」 的內容相同。

若是一端接收到了 Ping 幀而且在沒有來得及響應的時候又接收到了新的 Ping 幀,那麼響應端能夠選擇最近的 Ping 幀做爲響應的對象。

Pong 幀能夠在未被主動請求的狀況下發送給對方。這被認爲是單向的心跳包。單向心跳包是得不到響應的。

5.6 數據幀

數據幀(好比,非控制幀)是經過操做碼的最高有效位是 0 來肯定的。當前已經定義的數據幀包括 0x1 (文本)0x2 (二進制)。操做碼 0x3-0x7 是爲了未來的非控制幀的使用而保留的。

數據幀承載了 「應用層 application-layer」 或者 「擴展層 extension-layer」 的數據。操做碼決定了數據的表現形式。

  • 文本 Text

    「有效負載數據 Payload data」 是以 UTF-8 編碼的文本。注意,做爲整個文本消息的一部分的部分文本幀可能包含了部分的 UTF-8 序列;可是整個的消息的內容必須是一個有效的 UTF-8 序列。對於無效的 UTF-8 消息的處理在第 8.1 節中描述。

  • 二進制 Binary

    「有效負荷數據 Payload data」 是僅由應用層來決定的任意二進制內容。

5.7 例子

  • 一個單個幀的沒有進行掩碼的文本消息

    • 0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f(消息內容爲 「Hello」)
  • 一個單個幀的掩碼後的消息

    • 0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(消息內容爲 「 Hello」)
  • 一個碎片化的沒有掩碼的文本消息

    • 0x01 0x03 0x48 0x65 0x6c(消息內容爲 「Hel」)
    • 0x80 0x02 0x6c 0x6f(消息內容爲 「lo」)
  • 沒有掩碼的 Ping 請求和其掩碼後的響應

    • 0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f(消息體部分爲 「Hello」,只不過是例子,能夠爲任意內容)
    • x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58(消息體部分也是 「Hello」,和其響應的 Ping 相同)
  • 256 個字節的二進制消息,使用單個未掩碼的幀

    • 0x82 0x7E 0x0100 [256 個字節的二進制數據]
  • 64 Kb 的二進制消息,使用單個未掩碼的幀

    • 0x82 0x7F 0x0000000000010000 [65536 個字節的二進制數據]

5.8 擴展性

協議被設計爲容許擴展,擴展能夠在基礎協議的功能上添加更多的功能。通訊雙方必須在握手期間完成擴展的協商。在這份技術說明中,爲擴展提供使用的部分爲:操做碼 0x3 到 0x七、以及 0xB 到 0xF,「擴展數據 Extension data」 字段,frame-rsv一、frame-rsv二、frame-rsv3 這三個位於幀頭部的數位。關於擴展協商的詳細在第 9.1 節中討論。下面的列表是關於擴展的預期使用形式,不過它既不完整也不規範:

  • 「擴展數據」 能夠放在 「應用數據」 之間,它們共同組成 「有效負荷數據」
  • 保留的數位能夠爲每一幀按需分配
  • 保留的操做碼能夠被定義
  • 若是須要更多的操做碼的話,能夠佔用保留數位覺得操做碼提供更多的數位空間
  • 佔用保留數位,或者在 「有效負荷數據」 以外定義 「擴展」 的操做碼,以此得到更大的操做碼錶示空間,或者更多的區別每一幀的數位

6 發送和接收數據

6.1 發送數據

爲了在 WebSocket 鏈接上發送由 /data/ 組成的 WebSocket 消息,發送端必須按下的步驟去執行:

  1. 發送端必須肯定當前的 WebSocket 鏈接的狀態是 OPEN(見 第 4.1 和 4.2 節)。在任什麼時候間點,若是鏈接的狀態改變了,那麼發送端必須終止下面的步驟。

  2. 發送端必須使用 WebSocket 幀將 /data/ 按第 5.2 節中描述的形式包裹起來。若是數據太大,或者在發送時不能整個地獲取需發送數據,那麼發送端能夠按照第 5.4 節中描述的,將數據分割成一連串的幀進行發送。

  3. 包含數據的第一個幀的操做碼必須設置爲適當的數據類型,以便接收端能夠肯定用文本仍是二進制來解釋其接收到的數據,數據類型定義在第 5.2 節。

  4. 在消息的最後一個包含數據的幀中必須將其 FIN 設置爲 1,相關定義在第 5.2 節。

  5. 若是數據是由客戶端發送的,那麼數據在發送前必須按照第 5.2 中定義的方式進行掩碼。

  6. 若是鏈接中進行了擴展協商,那麼額外的擴展相關的處理將會應用到幀上。

  7. 幀必須經由 WebSocket 底層相關的網絡鏈接發送。

6.2 接收數據

爲了接收 WebSocket 數據,接收端必須監聽底層相關的網絡鏈接。接收到的數據必須按照第 5.2 節中定義的格式進行解析。若是接收到的是一個控制幀,那麼必須按照第 5.5 節中的定義去處理。一旦接收到第 5.6 節中定義的數據幀,接收端必須注意數據幀的類型 /type/,這點根據幀的 opcode,定義在第 5.2 節。「應用數據 Application data」 被定義爲消息的數據 /data/。若是幀是一個沒有被碎片化的幀,定義在第 5.4 節,那麼就說明一個消息已經被徹底接收了,即知道了其類型 /type/ 和數據 /data/。若是幀是碎片化消息的一部分,那麼其隨後的幀的 「應用數據」 鏈接在一塊兒組成消息的數據 /data/。當最後一個碎片化的幀被接收時,也就是幀的 FIN 位爲 1 時,代表一個 WebSocket 消息已經被徹底接收了,其數據 /data/ 就是全部相關碎片化的幀的 「應用數據」 鏈接到一塊兒的值,而 /type/ 就是第一個或者其餘組成消息的碎片化幀的操做碼。以後的幀必須被解釋爲屬於一個新的消息。

擴展(第 9 節)可能會更改數據被讀取的方式,特別是如何界定消息之間的邊界。擴展在有效負荷中的 「應用數據」 以前添加的 「擴展數據」 也可能會修改 「應用數據」 的內容(好比進行了壓縮)。

服務端必須未來自客戶端的幀進行反掩碼,操做定義在第 5.3 節。

7 關閉鏈接

7.1 定義

7.1.1 關閉 WebSocket 鏈接

爲了關閉 WebSocket 鏈接,一端能夠關閉底層的 TCP 鏈接。一端在關閉鏈接的時候必須乾淨的關閉,好比 TLS 會話,儘量的丟棄全部已經接收可是還沒有處理的字節。一端能夠在須要的時候以任意的理由去關閉鏈接,好比在收到攻擊時。

底層的 TCP 鏈接,在通常狀況下應該由服務端先進行關閉,而客戶端則須要在一段時間內等待服務端的 TCP 關閉,若是超過了客戶端的等待時間,客戶端則能夠關閉 TCP 鏈接。

一個以使用 Berkeley sockets 的 C 語言的例子演示如何幹淨的關閉鏈接:首先一端須要調用對 socket 調用 shutdown() 函數,並以 SHUT_WR 爲函數的參數,而後調用 recv() 函數直到其返回值爲 0,最後調用 close() 函數關閉 socket。

7.1.2 開始 WebSocket 關閉握手

爲了關閉開始 WebSocket 關閉握手,須要關閉的一端必須選擇一個狀態碼(第 7.4 節)/code/ 和可選的關閉緣由 (第 7.1.6 節)/reason/,而後按照第 5.5.1 節中的描述發送一個關閉幀,幀的狀態碼以及緣由就是以前選取的 /code/ 和 /reason/。一旦一端發送並接收到了關閉幀,就能夠按照第 7.1.1 節中定義的內容關閉 WebSocket 鏈接。

7.1.3 WebSocket 關閉握手已經開始

一旦任何一端發送或者接收到關閉幀,就代表 WebSocket 關閉握手已經開始,而且 WebSocket 鏈接的狀態變爲 CLOSING。

7.1.4 WebSocket 鏈接已經關閉

當底層的 TCP 鏈接已經關閉時,就代表 WebSocket 鏈接已經關閉,而且 WebSocket 鏈接的狀態變爲 CLOSED。若是 TCP 鏈接在 WebSocket 關閉握手完成以後才進行關閉,就說明關閉是乾淨(cleanly)的。不然的話就說明 WebSocket 鏈接已經關閉,但不是乾淨地(cleanly)。

7.1.5 WebSocket 鏈接關閉代碼

與第 5.5.1 節和第 7.4 節中定義的同樣,一個關閉幀能夠包含一個關閉狀態碼,以此代表關閉的緣由。WebSocket 的關閉能夠由任意一端發起,或者同時發起。返回的關閉幀的狀態碼與接收到的關閉幀的狀態碼相同。若是關閉幀沒有包含狀態碼,那麼就認爲其狀態碼是 1005。若是一端發現 WebSocket 鏈接已經關閉可是沒有收到關閉幀,那麼就認爲此時的狀態碼是 1006。

注意:兩端的關閉幀的狀態碼沒必要相同。好比,若是遠程的一端發送了一個關閉幀,可是本地的應用程序尚未讀取位於接收緩存中的關閉幀,而且應用程序也發送了一個關閉幀,那麼兩端都將會達到 「發送了」 和 「接收到」 關閉幀的狀態。每一端都會看到來自另外一端的具備不一樣狀態碼的關閉幀。所以,兩端能夠沒必要要求發送和接收到的關閉幀的狀態碼是相同的,這樣兩端就能夠大概同時進行 WebSocket 鏈接的關閉了。

7.1.6 WebSocket 鏈接關閉緣由

與第 5.5.1 節和第 7.4 節中定義的相同,關閉幀能夠包含一個狀態碼,而且在狀態碼以後能夠跟隨以 UTF-8 編碼的數據,具體這些數據應該如何被解釋依賴於對端的實現,本協議並無明確的定義。每一端均可以發起 WebSocket 關閉,或者同時發起。WebSocket 鏈接關閉緣由的定義就是跟隨在關閉狀態碼以後的以 UTF-8 編碼的數據,響應的關閉幀中的 /reason/ 內容來自請求的關閉幀中 /reason/,並與之相同。若是沒有定義這些 UTF-8 數據,那麼關閉的緣由就是空字符串。

注意:遵循第 7.1.5 節中描述的邏輯,兩端沒必要要求發送和接收的關閉幀的 /reason/ 是相同的。

7.1.7 將 WebSocket 鏈接標記爲失敗

由於某種算法或者特定的需求使得一端須要將 WebSocket 鏈接表示位失敗。爲了達到這個目的,客戶端必須關閉 WebSocket 鏈接,而且能夠將問題以適當的方式反饋給用戶(對於開發者來講可能很是重要)。一樣的,服務端爲了達到這個目的也必須關閉 WebSocket 鏈接,而且使用日誌記錄下發生的問題。

若是在但願將 WebSocket 鏈接標記爲失敗以前,WebSocket 鏈接已經創建的話,那麼一端在關閉 WebSocket 鏈接以前應該發送關閉幀,並帶上適當的狀態碼(第 7.4 節)。若是一端認爲另外一端不可能有能力去接受和處理關閉幀時,好比 WebSocket 鏈接還沒有創建,那麼能夠省略發送關閉幀的過程。若是一端標記了 WebSocket 鏈接爲失敗的,那麼它不能夠再接受和處理來自遠程的數據(包括響應一個關閉幀)。

除了上面的狀況或者應用層須要(好比,使用了 WebSocket API 的腳本),客戶端不該該關閉鏈接。

7.2 異常關閉

7.2.1 客戶端發起的關閉

由於某種算法或者在開始握手的實際運做過程當中,須要標記 WebSocket 鏈接爲失敗。爲了達到這個目的,客戶端必須按照第 7.1.7 節中描述的內容將 WebSocket 鏈接標記爲失敗。

若是在任意時間點,底層的傳輸層鏈接發送了丟失,那麼客戶端必須將 WebSocket 鏈接標記爲失敗。

除了上面的狀況或者特定的應用層須要(好比,使用了 WebSocket API 的腳本),客戶端不能夠關閉鏈接。

7.2.2 服務端發起的關閉

由於某種算法或者在握手期間終止 WebSocket 鏈接,服務端必須按照第 7.1.1 節的描述去關閉 WebSocket 鏈接。

7.2.3 從異常中恢復

異常關閉可能有不少的緣由引發。好比一個短暫的錯誤致使的異常關閉,在這種狀況下,經過重連可使用一個沒有問題的鏈接,而後繼續正常的操做。然而異常也多是一個由非短暫的問題引發的,若是全部發布的客戶端在經歷了一個異常關閉以後,馬上不斷的試圖向服務器發起重連,若是有大量的客戶端在試圖重連的話,那麼服務器將有可能面對拒絕服務攻擊(denial-of-service attack)。這樣形成的結果就是服務將沒法在短時間內恢復。

爲了防止這個問題出現,客戶端應該在發生了異常關閉以後進行重連時使用一些補償機制。

第一個重連應該延遲,在一個隨機時間後進行。產生用於延遲的隨機時間的參數由客戶端去決定,初始的重連延遲能夠在 0 到 5 秒之間隨機選取。客戶端能夠根據實際應用的狀況去決定具體的隨機值。

若是第一次的重連失敗,那麼接下來的重連應該使用一個更長的延遲,可使用一些已有的方法,好比 truncated binary exponential backoff

7.3 鏈接的通常關閉

服務端能夠在其需求的時候對 WebSocket 鏈接進行關閉。客戶端不該該隨意的關閉 WebSocket 鏈接。當須要進行關閉的時候,須要遵循第 7.1.2 節中定義的過程。

狀態碼

當關閉已經創建的鏈接時(好比在握手完成後發送關閉幀),請求關閉的一端必須代表關閉的緣由。如何解釋緣由,以及對於緣由應該採起什麼動做,都是這份技術說明中沒有定義的。這份技術說明中定義了一組預約義的狀態碼,以及擴展、框架、最終應用程序使用的狀態碼範圍。狀態碼相關的緣由 /reason/ 在關閉幀中是可選的。

7.4.1 已定義的狀態碼

當發送關閉幀的時候,一端能夠採用下面的預約義的狀態碼:

1000

1000 代表這是一個正常的關閉,表示鏈接已經圓滿完成了其工做。
1001

1001 代表一端是即將關閉的,好比服務端將關閉或者瀏覽器跳轉到了其餘頁面。
1002

1002 代表一端正在由於協議錯誤而關閉鏈接。
1003

1003 代表一端由於接收到了沒法受理的數據而關閉鏈接(好比只能處理文本的一端接收到了一個二進制的消息)
1004

保留的。特定的含義會在之後定義。
1005

1005 是一個保留值,而且必須不能夠做爲關閉幀的狀態碼。它的存在乎義就是應用程序可使用其表示幀中沒有包含狀態碼。
1006

1006 這是一個保留值,而且必須不能夠做爲關閉幀的狀態碼。它的存在乎義就是若是鏈接非正常關閉而應用程序須要一個狀態碼時,可使用這個值。
1007

1007 代表一端接收到的消息內容與之標記的類型不符而須要關閉鏈接(好比文本消息中出現了非 UTF-8 的內容)
1008

1008 代表了一端接收到的消息內容違反了其接收消息的策略而須要關閉鏈接。這是一個通用的狀態碼,能夠在找不到其餘合適的狀態碼時使用此狀態碼,或者但願隱藏具體與接收端的哪些策略不符時(好比 10031009)。
1009

1009 代表一端接收了很是大的數據而其沒法處理時須要關閉鏈接。
1010

1010 代表了客戶端但願服務端協商一個或多個擴展,可是服務端在返回的握手信息中包含協商信息。擴展的列表必須出如今其發送給服務端的關閉幀的 /reason/ 中。注意這個狀態碼並不被服務端使用。
1011

1011 代表了一端遇到了異常狀況使得其沒法完成請求而須要關閉鏈接。
1015

1015 是一個保留值,而且它必須不能夠做爲狀態碼在關閉幀中使用,在應用程序須要一個狀態碼去代表執行 TLS 握手失敗時,可使用它(好比服務端的證書沒有經過驗證)。

7.4.2 保留的狀態碼區間

  • 0-999

    在 0-999 之間的狀態碼是不被使用的

  • 1000-2999

    在 1000-2999 之間的狀態碼是本協議保留的,而且擴展能夠在其公開的技術說明中使用。

  • 3000-3999

    在 3000-3999 之間的狀態碼是爲庫、框架、應用程序保留的。這些狀態碼能夠直接經過 IANA 進行註冊。狀態碼的具體表示意義爲在本協議中定義。

  • 4000-4999

    在 4000-4999 之間的狀態碼是爲了私有使用而保留的,所以不能夠被註冊。相應狀態碼的使用及其意義能夠在 WebSocket 應用程序之間事先商議好。這些狀態碼的意義在本協議中未定義。

錯誤處理

8.1 處理編碼錯誤的 UTF-8 數據

當一端在以 UTF-8 編碼解釋接收到的數據,可是發現其實不是有效的 UTF-8 編碼時,一端必須標記 WebSocket 鏈接爲失敗。這個規則適用於握手以及隨後的數據傳輸階段。

9. 擴展

在這份技術說明中,客戶端是能夠請求使用擴展的,而且服務端能夠受理客戶端請求的擴展中的一個或者全部擴展。服務端響應的擴展必須屬於客戶端請求的擴展列表。若是擴展協商中包含了相應的擴展參數,那麼參數的選擇和應用必須按照具體的擴展的技術說明中描述的方式。

9.1 擴展協商

客戶端經過包含 |Sec-WebSocket-Extensions| 去請求擴展,此字段名遵循普通的 HTTP 頭字段的規則 RFC2616], Section 4.2,其內容的形式經由下面的 ABNF RFC2616 表達式給出定義。注意,着一節中的 ABNF 語法規則遵循 RFC2616,包括了 「隱含的 *LWS 規則」。若是一端接收到的值不符合下面的 ABNF,那麼接收端必須馬上標記 WebSocket 鏈接爲失敗。

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 頭字段同樣,這些頭字段也能夠分隔成多行,或者由多行合併。所以下面的兩個是等價的:

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

等價於

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

任何的 extension-token 好比使用已註冊的 token(見第 11.4 節)。爲擴展提供的參數好比遵循相應擴展的定義。注意,客戶端只是提供它但願使用的擴展,除非服務端從中選擇了一個或多個代表其也但願使用,不然客戶端不能夠私自的使用。

注意,擴展的在列表中順序是重要的。多個擴展之間的交互方式,可能在具體定義了擴展的文檔中進行了描述。若是沒有定義描述了多個擴展之間應該如何交互,那麼排在靠前位置的擴展應該最早被考慮使用。在服務端響應中列出的擴展將是鏈接實際將會使用的擴展。擴展之間修改數據或者幀的操做順序,應該假設和擴展在服務端握手響應中的擴展列表中出現的順序相同。

好比,若是有兩個擴展 「foo」 和 「bar」,而且在服務端發送的 |Sec-WebSocket-Extensions| 的值爲 「foo, bar」,那麼對數據的操做總體來看就是 bar(foo(data)),對於數據或者幀的修改過程看起來像是 「棧 stack」。

一個關於受理擴展頭字段的非規範化的例子:

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| 頭字段。不過當面對具備更多功能的客戶端時就不能採用此方法了(檢查 |Origin| 頭字段)。

這份協議能夠適用於運行在 web 頁面中的腳本,也能夠直接被主機所使用。那些主機能夠由於自身的目的發送一個僞造的 |Origin| 頭字段,以此迷惑服務器。服務端所以服務器不該該信息任何的客戶端輸入。

例子:若是服務端使用了客戶端的 SQL 查詢語句,全部的輸入文本在提交到 SQL 服務器以前必須進行跳脫操做(escape),減小服務端被 SQL 注入的風險。

10.2 Origin 的考慮

服務端沒必要接收來自互聯網的全部請求,能夠僅僅受理包含特定源的請求。若是請求的源不符合服務端的接收範圍,那麼服務端應該在對客戶端的握手響應中包含狀態碼 「403 Forbidden」。

|Origin| 的做用是能夠預防來自運行在可信任的客戶端中的 Javascript 的惡意攻擊。客戶端自己能夠鏈接到服務器,經過 |Origin| 的機制決定是否將通訊的權限交給 Javascript 應用。這麼作的目的不是針對非瀏覽器的鏈接,而是杜絕運行在被信任的瀏覽器可能的潛在威脅 - Javascript 腳本僞造 WebSocket 鏈接。

10.3 針對基礎設施的攻擊

除了一端的終節點會收到攻擊以外,基礎設施中的其餘部分,好比代理,也可能會收到攻擊。

針對代理的攻擊其實是針對那些在實現上有缺陷的代理服務器,有缺陷的代理服務器的工做方式相似:

  1. 首先你經過 Socket 的方式和 IP 爲 2.2.2.2 的服務器創建鏈接,鏈接是經由代理的。
  2. 在鏈接創建完成後,你發送了相似下面的文本:

    GET /script.js HTTP/1.1 Host: target.com

    (更多更深刻的描述見 Talking

    這段文本首先是傳到代理服務器的,代理服務器正確的工做方式是應該將此文本直接轉發給 IP 爲 2.2.2.2 的服務器。但是,有缺陷的代理會認爲這是一個 HTTP 請求,須要採用 HTTP 代理的機制,進而訪問了 target.com 並獲取了 /script.js。

這種錯誤的工做方式並非你所指望的。可是不可能一一檢查網絡中全部可能存在此問題的代理,因此最好的方式就是將客戶端發送的內容都進行掩碼操做,這樣就不會出現那種讓有缺陷的代理服務器產生迷惑的內容了。

10.4 特定實現的限制

在協議實現中,可能會有一些客觀的限制,好比特定平臺的限制,這些限制與幀的大小或者全部幀合併後的消息的大小相關(好比,惡意的終節點能夠經過發送單個很大的幀(2**60),或者發送不少很小的幀可是這些幀組成的消息很是大,以此來耗盡另外一方的資源)。所以在實現中,一端應該強制使用一些限制,限制幀的大小,以及許多幀最後組成的消息的大小。

10.5 WebSocket 客戶端認證

這份協議沒有規定任何方式可被用於服務端在握手期間對客戶端進行認證。WebSocket 服務端可使用任何在普通 HTTP 服務端中使用的對客戶端的認證方式,好比 cookie,HTTP 認證,或者 TLS 認證。

10.6 鏈接的保密性和完整性

WebSocket 協議的保密性和完整性是經過將其運行在 TLS 上達到的。WebSocket 實現必須支持 TLS 並在須要的時候使用它。

對於使用 TLS 的鏈接,TLS 提供的大部分好處都是基於 TLS 握手階段協商的算法的強度。好比,一些 TLS 加密算法沒有保證信息的保密性。爲了使安全達到合適的程度,客戶端應該只使用高強度的 TLS 算法。W3C.REC-wsc-ui-20100812 具體討論了什麼是高強度的 TLS 算法,RFC5246 的附錄 A.5 和 附錄 D.3 提供了一些指導意見。

10.7 處理錯誤數據

客戶端和服務端接收的數據都必須通過驗證。若是在任意時間點上,一端接收到了沒法理解的或者違反標準的數據,或者發現了不安全的數據,或者在握手期間接收到了非指望的值(好比錯誤的路徑或者源),則能夠關閉 TCP 鏈接。若是接收到無效數據時 WebSocket 鏈接已經創建,那麼一端在關閉 WebSocket 鏈接以前,應該向另外一端發送一個帶有適當的狀態碼的關閉幀。經過使用具備適當狀態碼的關閉幀,能夠幫助定位問題。若是在握手期間接收到了無效的數據,那麼服務端應該返回適當的 HTTP 狀態碼 RFC2616

一個典型的安全問題就是當發送的數據採用了錯誤的編碼時。這份協議中規定了,文本數據包含的必須是 UTF-8 編碼的數據。應用程序須要經過一個長度去肯定幀序列的傳輸什麼時候結束,可是這個長度每每在事先很差肯定(碎片化的消息)。這就給檢查文本消息是否採用了正確的編碼帶來了困難,由於必須等到消息的全部碎片幀都接受完成了,才能夠檢查它們組成的消息的編碼是否正確。不過若是不檢查編碼的話,就不能確保接收的數據能夠被正確的解釋,並會帶來潛在的安全問題。

10.8 在 WebSocket 握手中採用 SHA-1

這份文檔中描述的 WebSocket 握手並不依賴於 SHA-1 算法的安全屬性,好比抗碰撞性或者在 RFC4270 中描述的 second pre-image attack。

相關文章
相關標籤/搜索