【譯】WebSocket協議第四章——連線握手(Opening Handshake)

黃Java發表於2018-06-21

概述

本文為WebSocket協議的第四章,本文翻譯的主要內容為WebSocket建立連線開始握手的內容,主要包含了客戶端和服務端握手的內容,以及雙方如何處理相關欄位和邏輯。

4 開始握手(協議正文)

4.1 客戶端要求

為了建立一個WebSocket連線,客戶端需要建立一個連線並且傳送一個在本節中定義的握手協議。連線最初狀態為CONNECTING。客戶端需要提供一個第三章討論過的主機(host)、埠(port)、資源名稱(resource name)和安全標記(secure)欄位以及可被使用的一個協議(protocol)和擴充套件(extensions)列表。另外,如果客戶端是一個Web瀏覽器,還需要提供源(origin)欄位。

客戶端在一個受控制的環境內執行,如使用特定運營商的手機瀏覽器,可能會斷開連線切換到其他的運營商。在這種情況下,我們需要考慮包括手機軟體和相關運營商在內的指定客戶端。

當客戶端通過一系列的配置欄位(主機(host)、埠(port)、資源名稱(resource name)和安全標記(secure))以及一個可被使用的協議(protocol)和擴充套件(extensions)列表來建立一個WebSocket連線,它一定會通過傳送一個握手協議,並且受到一個服務端的握手響應來建立一條連線。建立連線具體需要哪些東西,在開始握手的時候會傳送哪些欄位,如何處理解讀服務端的的響應都會在這一部分得到解答。在下面的內容中,我們會使用到第三章定義的一些術語如主機(host)和安全(secure)欄位。

  1. WebSocket的URI部分傳遞的欄位(主機(host)、埠(port)、資源名稱(resource name)和安全標記(secure))必須是在第三章WebSocket URIs部分指定過的有效欄位,如果任意部分是無效欄位,那麼客戶端一定會在接下來的步驟中關閉連線。

  2. 如果客戶端有一條通過遠端主機(IP地址)定義的主機和埠定義的已經建立連線的WebSocket連線,即使這個遠端主機被定義為了其他的名字,這個客戶端也必須等到當前的這條連線建立成功或者失敗才能建立連線。客戶端最多有一條連線可以處於CONNECTING狀態。如果多個連線嘗試同時與一個相同的IP地址建立連線,客戶端必須把他們進行排序,所以只能有一個連線執行下面的步驟。

    如果客戶端不能夠確定遠端主機的IP地址(例如所有的請求都通過一個自己執行DNS查詢的代理),那麼客戶端必須基於此假設每一個主機名都對應著不同的遠端主機,因此客戶端應該限制同時連線的總數目在一個比較合理的小數目上(例如:客戶端可能允許同時跟a.example.comb.example.com這兩個地址建立連線,但是如果同時和主機建立三十個連線,這可能是不允許的)。例如:在Web瀏覽器環境下,客戶端需要考慮在使用者開啟的多個tab頁中設定一個同時建立連線的數目限制。

    注意:這個限制使得指令碼僅僅通過建立大量的WebSocket連線來進行拒絕服務攻擊變得更難了。服務端可以在關閉連線前就停止攻擊,從而進一步減小負載,這樣會減少客戶端的重連率。

    注意:客戶端可以與單個主機建立的WebSocket連線數量是沒有限制的。當建立的連線過多時,服務端可以拒絕和主機/IP地址建立的連線,同時服務端在負載過高時也可以主動斷開佔用資源的連線。

  3. 使用代理:如果客戶端在使用WebSocket協議來連線特定的主機和埠時使用了配置的代理,那麼客戶端應該連線到那個代理並且通過這個代理去和指定的主機和埠建立一個TCP連線。

    例如:如果客戶端使用了全域性的HTTP代理,那麼如果嘗試和example.com的80埠建立連線,那麼久可能會傳送下面的欄位給代理伺服器:

    CONNECT example.com:80 HTTP/1.1Host: example.com複製程式碼
     如果有密碼欄位的話,那麼可能如下所示: 複製程式碼
    CONNECT example.com:80 HTTP/1.1Host: example.comProxy-authorization: Basic ZWRuYW1vZGU6bm9jYXBlcyE=複製程式碼

    如果客戶端沒有配置代理,那麼就應該會和給定的主機和埠直接建立一條TCP連線。

    注意:如果可以,實現不暴露明顯介面的來給WebSocket選擇與其他代理分開的代理推薦使用SOCKS5(RFC1928)代理供WebSocket連線,如果不行的話,使用配置了HTTPS連線的代理優於使用HTTP連線的代理。

    為了自動配置指令碼,傳遞引數的URI必須包含定義在第三節WebSocket URI中的主機(host)、埠(port)、資源名稱(resource name)和安全(secure)欄位。

    注意:WebSocket協議可以根據定義的規範配置到代理自動配置指令碼(”ws”代表非加密連線,”wss”代表加密連線)。

  4. 如果連線沒有被開啟,或者由於直連失敗或者代理返回了一個錯誤,那麼客戶端必須斷開WebSocket連線,並且停止重試連線。

  5. 如果安全(secure)欄位存在,客戶端必須在連線建立以後、傳送握手資料之前進行TLS握手。如果TLS握手失敗(比如服務端正數沒有驗證通過),那麼客戶端必須斷開WebSocket連線。否則,所有後續的在此頻道上面的資料通訊都必須在加密的通道中傳輸。

    客戶端在TLS握手時必須使用伺服器名稱指示擴充套件(SNI,Server Name Indication)。

一旦到服務端的連線被建立了(包括通過一個代理或者通過一個TLS加密通道),客戶端必須傳送一個開始握手的資料包給服務端。這個資料包由一個HTTP升級請求構成,包含一系列必須的和可選的header欄位。握手的具體要求如下所示:

  1. 握手必須是一個在RFC2616指定的有效的HTTP請求。

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

    例如:如果WebSocket的URI是”ws://example.com/chat”,那麼傳送的請求頭第一行就應該是”GET /chat HTTP/1.1″。

  3. 請求的”Request-URI”部分必須與第三章中定義的資源名稱(resource name)匹配,或者必須是一個http/https絕對路徑的URI,當解析URI時,有一個資源名稱(resource name)、主機(host)和埠(port)與相對應的ws/wss匹配。

  4. 請求必須包含一個Hostheader欄位,它包含了一個主機(host)欄位加上一個緊跟在”:”之後的埠(port)欄位(如果埠不存在則使用預設埠)。

  5. 這個請求必須包含一個Upgradeheader欄位,它的值必須包含”websocket”。

  6. 請求必須包含一個Connectionheader欄位,它的值必須包含”Upgrade”。

  7. 請求必須包含一個名為Sec-WebSocket-Key的header欄位。這個header欄位的值必須是由一個隨機生成的16位元組的隨機數通過base64(見RFC4648的第四章)編碼得到的。每一個連線都必須隨機的選擇隨機數。

    注意:例如,如果隨機選擇的值的位元組順序為0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10,那麼header欄位的值就應該是”AQIDBAUGBwgJCgsMDQ4PEC==”。

  8. 如果這個請求來自一個瀏覽器,那麼請求必須包含一個Originheader欄位。如果請求是來自一個非瀏覽器客戶端,那麼當該客戶端這個欄位的語義能夠與示例中的匹配時,這個請求也可能包含這個欄位。這個header欄位的值為建立連線的原始碼的源地址ASCII序列化後的結果。通過RFC6454可以知道如何構造這個值。

    例如,如果在www.example.com域下面的程式碼嘗試與ww2.example.com這個地址建立連線,那麼這個header欄位的值就應該是”www.example.com“。

  9. 這個請求必須包含一個名為Sec-WebSocket-Version的欄位。這個header欄位的值必須為13。

    注意:儘管這個文件草案的版本(09,10,11和12)都已經發布(這些協議大部分是編輯上的修改和澄清,而不是對無線協議的修改),9,10,11,12這四個值不被認為是有效的Sec-WebSocket-Version的值。這些值被IANA保留,但是沒有被用到過,以後也不會被使用。

  10. 這個請求可能會包含一個名為Sec-WebSocket-Protocol的header欄位。如果存在這個欄位,那麼這個值包含了一個或者多個客戶端希望使用的用逗號分隔的根據權重排序的子協議。這些子協議的值必須是一個非空字串,字元的範圍是U+0021到U+007E,但是不包含其中的定義在RFC2616中的分隔符,並且每個協議必須是一個唯一的字串。ABNF的這個header欄位的值是在RFC2616定義了構造方法和規則的1#token。

  11. 這個請求可能包含一個名為Sec-WebSocket-Extensions欄位。如果存在這個欄位,這個值表示客戶端期望使用的協議級別的擴充套件。這個header欄位的具體內容和格式具體見9.1節。

  12. 這個請求可能還會包含其他的文件中定義的header欄位,如cookie(RFC6265)或者認證相關的header欄位如Authorization欄位(RFC2616)。

一旦客戶端的握手請求傳送出去,那麼客戶端必須在傳送後續資料前等待服務端的響應。客戶端必須通過以下的規則驗證服務端的請求:

  1. 如果客戶端收到的服務端返回狀態碼不是101,客戶端需要處理每個HTTP請求的響應。特別的是,客戶端需要在收到401狀態碼的時候可能需要進行驗證;服務端可能會通過3xx的狀態碼來將客戶端進行重定向(但是客戶端不要求遵守這些)等。否則,遵循下面的步驟。
  2. 如果客戶端收到的響應缺少一個Upgradeheader欄位或者Upgradeheader欄位包含一個不是”websocket”的值(該值不區分大小寫),那麼客戶端必須關閉連線。
  3. 如果客戶端收到的響應缺少一個Connectionheader欄位或者Connectionheader欄位不包含”Upgrade”的值(該值不區分大小寫),那麼客戶端必須關閉連線。
  4. 如果客戶端收到的Sec-WebSocket-Acceptheader欄位或者Sec-WebSocket-Acceptheader欄位不等於通過Sec-WebSocket-Key欄位的值(作為一個字串,而不是base64解碼後)和”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″串聯起來,忽略所有前後空格進行base64 SHA-1編碼的值,那麼客戶端必須關閉連線。
  5. 如果客戶端收到的響應包含一個Sec-WebSocket-Extensionsheader欄位,並且這個欄位使用的extension值在客戶端的握手請求裡面不存在(即服務端使用了一個客戶端請求中不存在的值),那麼客戶端必須關閉連線。(解析這個header欄位來確定使用哪個擴充套件在9.1節中有討論。)
  6. 如果客戶端收到的響應包含一個Sec-WebSocket-Protocolheader欄位,並且這個欄位包含了一個沒有在客戶端握手中出現的子協議(即服務端使用了一個客戶端請求中子協議欄位不存在的值),那麼客戶端必須關閉連線。

如果客服務端的響應沒有符合定義在這一節和4.2.2節中的服務端握手響應定義的要求,那麼客戶端也會斷開連線。

請注意,根據RFC2616,所有的header欄位名稱在HTTP請求和HTTP請求響應中都是不區分大小寫的。

如果服務端的響應通過了上述的驗證過程,那麼WebSocket就已經建立連線了,並且WebSocket的連線狀態也到了OPEN狀態。使用的擴充套件被定義為一個字串(可能為空),它是在服務端響應握手時候提供的Sec-WebSocket-Extensions欄位的值,如果這個header欄位在握手響應中不存在,那麼就是一個空值。使用的子協議值是在服務端響應握手中提供的Sec-WebSocket-protocol欄位的值,如果服務端響應握手時沒有這個header欄位,那麼這個值也為空。另外,如過服務端握手響應是稽核制了任何cookie的header欄位(定義在RFC6265),這些cookie被稱為在服務端響應握手時設定的cookie(Cookies Set During the Server’s Opening Handshake)。

4.2 服務端要求

服務端可以將連線的管理掛載到其他的網路代理賞,如負載均衡器或者反向代理。在這種情況下,這篇規範對於服務端的目標是包含從第一個裝置從建立到斷開連線的TCP連線週期到服務端接受請求,傳送響應的所有服務測的基礎設施部分。

示例:一個資料中心可能有一個響應WebSocket握手請求的伺服器,但是它將收到的資料幀都通過連線傳遞給另一個伺服器來處理。在本文件中,”服務端(server)”包含這兩者。

4.2.1 解析客戶端的握手協議

當客戶端開始一個WebSocket連線時,他會傳送一個開始握手協議。為了獲得必要的資訊來保證服務端的握手響應,服務端必須解析這個客戶端這部分的握手協議。

客戶端的握手協議包含以下幾部分。當服務的收到一個握手請求,發現客戶端並沒有傳送一個符合以下內容的握手協議(注意在RFC2616中的每一項,header欄位的順序是不重要的),包括但不限於在握手協議中有不合法的ANBF語法,服務端必須立即停止處理客戶端的握手請求並且在響應中返回一個表示錯誤的HTTP錯誤碼(如400 Bad Request)。

  1. 一個HTTP/1.1或者跟高版本的GET請求,包含一個在第三章定義的應該被解析為資源名稱(resource name)”Request-URI”欄位(或者包含資源名稱(resource name)的HTTP/HTTPS絕對路徑)。
  2. 包含服務端許可權的Hostheader欄位。
  3. 不區分大小寫的值為”websocket”的Upgradeheader欄位。
  4. 不區分大小寫的值為”Upgrade”的Connectionheader欄位。
  5. 值為base64編碼(見RFC4648的第四章)後長度為16位元組的Sec-WebSocket-Keyheader欄位。
  6. 值為13的Sec-WebSocket-Versionheader值。
  7. 可選的Originheader欄位。所有的瀏覽器都會傳送這個欄位。缺少此欄位的連線不應該認為是來自瀏覽器。
  8. 可選的Sec-WebSocket-Protocolheader欄位,對應的值為客戶端支援的子協議,根據權重進行排序。
  9. 可選的Sec-WebSocket-Extensionsheader欄位,對應的值為客戶端可以使用的擴充套件。這個欄位具體內容會在第9.1節再進行討論。
  10. 可選的其他欄位,如使用cookie或者伺服器請求認證的欄位。不識別的header欄位會依據RFC2616中內容被忽略。

4.2.2 傳送服務端握手響應請求

當客戶端和服務端建立了一個WebSocket連線,服務端也必須完成接受連線的下面說明的步驟,並且傳送一個服務端握手響應。

  1. 如果是一條建立在HTTPS(HTTPS+TLS)埠的連線,通過這個連結完成TLS握手過程。如果這次握手失敗(例如,客戶端在”server_name”擴充套件中制定了主機名,但是服務端沒有這個主機),那麼關閉這條連線;否則,後續這個連線的所有的資料傳遞(包括服務端握手響應)都必須使用一個加密的通道。

  2. 服務端可以選擇而外面的客戶端認證,例如,通過返回401狀態碼和在RFC2616說明的相對應的WWW-Authenticateheader欄位。

  3. 服務端可能通過使用3xx的狀態碼(見RFC2616)來重定向客戶端。注意這個步驟可以發生在上面說到的認證之前、之後或者和認證一起。

  4. 構造以下資訊:

    源(origin

    Originheader欄位在客戶端的握手請求中表示建立連線的指令碼屬於哪一個源。這個源資訊被序列化為ASCII,並且轉換為小寫。服務端可以使用這個資訊來作為判斷是否接受這個連結的部分參考內容。如果服務端沒有過濾源,那麼他會接受任意源的連線。如果服務端沒有接受這個連線,那麼它必須返回一個對應的HTTP錯誤碼(如403 Forbidden)並且終端這一節描述的WebSocket握手過程。更多詳情可以閱讀第十章。

    關鍵值(key

    Sec-WebSocket-Keyheader欄位在客戶端的握手請求中表示一個長度為16位元組的base64編碼的值。這個編碼後的值是用於服務端握手的建立過程,用來表示接受了這個連線。服務端沒有必要對Sec-WebSocket-Key值進行解碼。版本(versionSec-WebSocket-Versionheader欄位在客戶端握手請求中表示了客戶端建立連線使用的WebSocket協議版本。如果這個版本和服務端的版本沒有匹配上,那麼服務端必須中斷本章說的WebSocket連線,並且傳送一個對應的HTTP錯誤碼(例如426 Upgrade Required),同時返回一個Sec-WebSocket-Versionheader欄位用來標識服務端能夠識別的版本號。

    資源名稱(resource name

    服務端提供的服務識別符號。如果這個服務端提供多種服務,那麼這個值應該是來自客戶端握手請求中的GET方法中的”Request-URI”欄位。如果請求的服務支援,那麼服務端必須傳送一個相對應的HTTP錯誤碼(例如404 Not Found)並且終端WebSocket連線。子協議(subprotocol)服務端準備使用的代表子協議的單個值或者為空。這個值必須選擇客戶端握手協議中由Sec-WebSocket-Protocol欄位中提供的值,服務端會在這個連線中使用此值(任意)。如果客戶端握手協議中沒有包含這個欄位或者服務端不支援客戶端請求中提供的任意一個子協議,那麼這個值只能為空。沒有此header值就表明該值為空(這意味著服務端可以不選擇客戶端傳遞的任意一個子協議,禁止在響應請求中新增一個Sec-WebSocket-Protocol欄位)。空字串與空值不同,並且空值對於此欄位來說是一個不合法值。ABNF對於整個欄位的定義和構造規則可以見RFC2616

    擴充套件(extensions

    表示一個服務端準備使用的協議級擴充套件列表(可能為空)。如果服務端支援多種擴充套件,那麼這個值必須是客戶端握手中已有的數值,是從Sec-WebSocket-Extensions欄位中取一到多個值。該欄位不存在時則表示此值為空。空字串與空值不同。客戶端沒有列舉的擴充套件靜止被使用。應該選擇哪些值和如何進行解析可以見9.1節。

  5. 如果服務端選擇接受一條連線,他必須傳送一個如下說明的有效的HTTP請求來進行相應。

    1. RFC2616中說明的一樣,狀態碼為101的狀態行。比如看上去像這種的:”HTTP/1.1 101 Switching Protocols”。
    2. RFC2616中說明的一樣,值為”websocket”的Upgradeheader欄位。
    3. 值為”Upgrade”的Connectionheader欄位。
    4. 一個Sec-WebSocket-Acceptheader欄位。這個值由第4.2.2節的第4步提到的key來進行構造,通過和字串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″拼接在一起進行SHA-1雜湊運算,得到一個20位元組的值,然後對這20位元組進行base64編碼。ABNF對這個欄位定義如下:
    Sec-WebSocket-Accept = base64-value-non-emptybase64-value-non-empty = (1*base64-data [ base64-padding ]) | base64-paddingbase64-data = 4base64-characterbase64-padding = (2base64-character "==") | (3base64-character "=")base64-character = ALPHA | DIGIT | "+" | "/"複製程式碼

    注意:作為示例,如果客戶端握手時傳送的Sec-WebSocket-Keyheader欄位的值為”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欄位返回這個結果。5. 可選的Sec-WebSocket-Protocol欄位,值為定義在第4.2.2節第4點中的子協議中。6. 可選的Sec-WebSocket-Extensions欄位,值為定義在4.2.2節第4點中的擴充套件中。

這樣服務端握手響應就完成了。如果服務端完成了上述步驟時也沒有關閉中斷WebSocket連線,那麼服務端回考慮建立這個WebSocket連結並且將WebSocket連線狀態置為OPEN。在此刻,服務端就可以開始傳送(和接收)資料了。

4.3 收集握手中使用的新的ABNF的header欄位

這一節使用在RFC2616第2.1節定義的ABNF語法和規則,包括隱含的*LWS規則(implied *LWS rule)。

請注意本節中使用了一下ABNF規定。一些規則名稱對應一些header欄位。這樣的規則表示對應的header欄位的值,例如Sec-WebSocket-Key的ABNF描述了Sec-WebSocket-Keyheader欄位的值的語法。在名字中帶有”-Client”字尾的ABNF規則只適用於客戶端傳送給服務端的請求;而名字中帶有”-Server”字尾的ABNF規則則只適用於服務端給客戶端傳送的請求響應。例如ABNF規則Sec-WebSocket-Protocol-Client表示客戶端傳送給服務端的請求中的Sec-WebSocket-Protocol欄位的值。

以下的新的header欄位可以在客戶端向服務端傳送握手請求時使用:

Sec-WebSocket-Key = base64-value-non-emptySec-WebSocket-Extensions = extension-listSec-WebSocket-Protocol-Client = 1#tokenSec-WebSocket-Version-Client = versionbase64-value-non-empty = (1*base64-data [ base64-padding ]) | base64-paddingbase64-data = 4base64-characterbase64-padding = (2base64-character "==") | (3base64-character "=")base64-character = ALPHA | DIGIT | "+" | "/"extension-list = 1#extensionextension = extension-token *( ";
"
extension-param )extension-token = registered-tokenregistered-token = tokenextension-param = token [ "=" (token | quoted-string) ] ;
當使用帶引號的字串語法變體時,在引號轉義後面的值必須和ABNF"標記(token)"一致。NZDIGIT = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"version = DIGIT | (NZDIGIT DIGIT) | ("1" DIGIT DIGIT) | ("2" DIGIT DIGIT) ;
範圍是從0-255,沒有前導0複製程式碼

以下的新的header欄位可以在服務端向客戶端傳送握手響應請求時使用:

Sec-WebSocket-Extensions = extension-listSec-WebSocket-Accept = base64-value-non-emptySec-WebSocket-Protocol-Server = tokenSec-WebSocket-Version-Server = 1#version複製程式碼

4.4 支援多版本WebSocket協議

這一節提供了一些關於在客戶端和服務端間支援多版本的WebSocket的協議的指導。

使用WebSocket版本標記欄位(Sec-WebSocket-Versionheader欄位),客戶端可以在最初請求時選擇WebSocket協議的版本號(客戶端不必要支援最新的版本)。如果服務端支援請求的版本並且我收到訊息是有效的,那麼服務端會接受這個版本。如果服務端不支援客戶端請求的版本,那麼服務端必須返回一個Sec-WebSocket-Versionheader欄位(或者多個Sec-WebSocket-Versionheader欄位)包含服務端支援的所有版本。在這種情況下,如果客戶端支援其中任意一個版本,它可以選擇一個新的版本值重新發起握手請求。

下面的示例演示瞭如何進行上面所述的版本協商:

GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: 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: 13Sec-WebSocket-Version: 8, 7複製程式碼

客戶端選擇了版本13,重新進行握手:

GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: Upgrade...Sec-WebSocket-Version: 13複製程式碼

來源:https://juejin.im/post/5b2b9850518825748e545d23

相關文章