WebSocket 協議

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 的方式去響應那些狀態碼。狀態行以後,就是頭字段。

ConnectionUpgrade 頭字段完成了對 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.comb.example.com 是兩臺不一樣的服務器,可是若是每臺服務器都有三十個須要同時發生的鏈接的話,可能就應該不被容許)

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

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

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

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

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

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

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

  2. 若是設置了 /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/ 可能會有查詢參數的,只不過例子中沒有。

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

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

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

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

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

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

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

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

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

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

  4. 請求能夠包含其餘可選的頭字段,好比 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| 頭字段值的方式也生成一個字符串,與服務端回傳的進行對比,若是不一樣就標記鏈接爲失敗的。

  1. 若是服務端回傳的 |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| 是能夠的,表示客戶端和服務端之間將不使用任何擴展。

  1. 若是客戶端在握手請求中包含了子協議頭字段 |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 節中討論

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

  2. 一個狀態行,包含了響應碼 101。好比 HTTP/1.1 101 Switching Protocols

  3. 一個 |Upgrade| 頭字段,值爲 websocket

  4. 一個 |Connection| 頭字段,值爲 Upgrade

  5. 一個 |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| 的字段值。
  1. 可選的,一個 |Sec-WebSocket-Protocol| 頭字段,它的值已經在第 4.2.2 節中的第 4 步定義了

  2. 可選的,一個 |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

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

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
相關文章
相關標籤/搜索