# 引言
## 1.1.背景
本節是非規范的。
過去,創建需要在客戶端和服務之間雙向通信(例如,即時消息和游戲應用)的web應用, 需要一個濫用的HTTP來輪詢服務器進行更新但以不同的HTTP調用發生上行通知[[RFC6202](http://tools.ietf.org/html/rfc6202)]。
這將導致各種各樣的問題:
o 服務器被迫為每個客戶端使用一些不同的底層TCP連接: 一個用于發送信息到客戶端和一個新的用于每個傳入消息。
o 線路層協議有較高的開銷,因為每個客戶端-服務器消息都有一個HTTP頭信息。
o 客戶端腳本被迫維護一個傳出的連接到傳入的連接的映射來跟蹤回復。
一個簡單的辦法是使用單個TCP連接雙向傳輸。這是為什么提供WebSocket 協議。與WebSocket API結合[[WSAPI](http://tools.ietf.org/html/rfc6455#ref-WSAPI)],它提供了一個HTTP輪詢的替代來進行從web 頁面到遠程服務器的雙向通信。 同樣的技術可以用于各種各樣的web應用:
游戲、股票行情、同時編輯的多用戶應用、服務器端服務以實時暴露的用戶接口、等等。
WebSocket協議被設計來取代現有的使用HTTP作為傳輸層的雙向通信技術,并受益于現有的基礎設施(代理、過濾、身份驗證)。這樣的技術被實現來在效率和可靠性之間權衡,因為HTTP最初目的不是用于雙向通信(參見[[RFC6202](http://tools.ietf.org/html/rfc6202)]的進一步討論)。WebSocket協議試圖在現有的HTTP基礎設施上下文中解決現有的雙向HTTP技術目標;同樣,它被設計工作在HTTP端口80和443,也支持HTTP代理和中間件,即使這具體到當前環境意味著一些復雜性。但是,這種設計不限制WebSocket到HTTP,且未來的實現可以在一個專用的端口上使用一個更簡單的握手,且沒有再創造整個協議。最后一點是很重要的,因為交互消息的傳輸模式不精確地匹配標準HTTP傳輸并可能在相同組件上包含不常見的負載。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#12%E5%8D%8F%E8%AE%AE%E6%A6%82%E8%BF%B0)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格式。
Request-Line 和 Status-Line 制品定義在[[RFC2616](http://tools.ietf.org/html/rfc2616)]。
在這兩種情況中一個無序的頭字段集合出現在首行之后。這些頭字段的意思指定在本文檔的第4章。另外的頭字段也可能出現,例如cookies[[RFC6265](http://tools.ietf.org/html/rfc6265)]。頭的格式和解析定義在[[RFC2616](http://tools.ietf.org/html/rfc2616)]。
一旦客戶端和服務器都發送了它們的握手,且如果握手成功,接著開始數據傳輸部分。這是一個每一端都可以的雙向通信信道,彼此獨立,隨意發生數據。
一個成功握手之后,客戶端和服務器來回地傳輸數據,在本規范中提到的概念單位為“消息”。在線路上,一個消息是由一個或多個幀的組成。WebSocket的消息并不一定對應于一個特定的網絡層幀,可以作為一個可以被一個中間件合并或分解的片段消息。
一個幀有一個相應的類型。屬于相同消息的每一幀包含相同類型的數據。從廣義上講,有文本數據類型(它被解釋為UTF-8[[RFC3629](http://tools.ietf.org/html/rfc3629)]文本)、二進制數據類型(它的解釋是留給應用)、和控制幀類型(它是不準備包含用于應用的數據,而是協議級的信號,例如應關閉連接的信號)。這個版本的協議定義了六個幀類型并保留10以備將來使用。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#13%E6%89%93%E5%BC%80%E9%98%B6%E6%AE%B5%E6%8F%A1%E6%89%8B)1.3.打開階段握手
本節是非規范的。
打開階段握手目的是兼容基于HTTP的服務器軟件和中間件,以便單個端口可以用于與服務器交流的HTTP客戶端和與服務器交流的WebSocket客戶端。最后,WebSocket客戶端的握手是一個HTTP Upgrade請求:
~~~
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](http://tools.ietf.org/html/rfc2616)],握手中的頭字段可能由客戶端按照任意順序發送,因此在接收的不同頭字段中的順序是不重要的。
“Request-URI”的GET方法[[RFC2616](http://tools.ietf.org/html/rfc2616)]用于識別WebSocket連接的端點,即允許從一個IP地址服務的多個域名,也允許由單臺服務器服務的多個WebSocket端點。 客戶端按照[[RFC2616](http://tools.ietf.org/html/rfc2616)]在它的握手的|Host|頭字段中包含主機名,以便客戶端和服務器都都能驗證他們同意哪一個正在使用的主機。
在WebSocket協議中另外的頭字段可以用于選擇選項。典型的選項在這個版本中可用的是子協議選擇器(|Sec-WebSocket-Protocol|)、客戶端支持的擴展列表(|Sec-WebSocket-Extensions|)、|Origin|頭字段等。|Sec-WebSocket-Protocol|請求頭字段可以用來表示客戶端接受的子協議(WebSocket協議上的應用級協議層)。服務器選擇一個可接受的協議或不,并在它的握手中回應該值表示它已經選擇了那個協議。
~~~
Sec-WebSocket-Protocol: chat
~~~
|Origin|頭字段[RFC6454]是用于保護防止未授權的被瀏覽器中的使用WebSocket API的腳本跨域使用WebSocket服務器。服務器收到WebSocket連接請求生成的腳本來源。如果服務器不想接受來自此來源的連接,它可以選擇通過發送一個適當的HTTP錯誤碼拒絕該連接。這個頭字段由瀏覽器客戶端發送,對于非瀏覽器客戶端,如果它在這些客戶端上下文中有意義,這個頭字段可以被發送。
最后,服務器要證明收到客戶端WebSocket握手的客戶端,以便服務器不接受不是WebSocket連接的連接。這可以防止一個通過使用XMLHttpRequest [[XMLHttpRequest](http://tools.ietf.org/html/rfc6455#ref-XMLHttpRequest)]或一個表單提交發送它精心制作的包欺騙WebSocket服務器的攻擊者。
為了證明收到的握手,服務器必須攜帶兩條信息并組合他們形成一個響應。
第一條信息源自客戶端握手中的| Sec-WebSocket-Key |頭信息: Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
對于這個頭字段,服務器必須攜帶其值(出現在頭字段上,如,減去開頭和結尾空格的base64-編碼[[RFC4648](http://tools.ietf.org/html/rfc4648)]的版本)并將這個與字符串形式的全局唯一標識符(GUID,[[RFC4122](http://tools.ietf.org/html/rfc4122)])“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”連接起來,其不太可能被不理解WebSocket協議的網絡端點使用。SHA-1散列(160位)[[FIPS.180-3](http://tools.ietf.org/html/rfc6455#ref-FIPS.180-3)]、base-64編碼(參見[[RFC4648](http://tools.ietf.org/html/rfc4648#section-4)]第4章)、用于這個的一系列相關事物接著在服務器握手過程中返回。
具體而言,如果在上面例子中,|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編碼(參見[RFC4648]第4章),產生值“s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”。這個值將接著在|Sec-WebSocket-Accept|頭字段中回應。
來自服務器的握手比客戶端握手更簡單。首行是一個HTTP Status-Line,具有狀態碼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-Accept|不能匹配盼望的值、如果頭字段缺失、或HTTP狀態碼不是101,則連接將不能建立,且WebSocket幀將不發生。
可選的字段也可以被包含在內。在這合格版本的協議中,主要可選字段是|Sec-WebSocket-Protocol|,其表示服務器選擇的子協議。WebSocket客戶端驗證服務器包含的在WebSocket客戶端握手中指定的一個值。聲明多個子協議的服務器必須確保它選擇一個,基于客戶端握手并指定它在其握手中。
~~~
Sec-WebSocket-Protocol: chat
~~~
服務器也可以設置cookie相關的可選字段為_set_cookies,描述在[[RFC6265](http://tools.ietf.org/html/rfc6265)]。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#14%E5%85%B3%E9%97%AD%E9%98%B6%E6%AE%B5%E6%8F%A1%E6%89%8B)1.4.關閉階段握手
本節是非規范的。
關閉階段握手比打開階段握手簡單得多。
兩個節點中的任一個都能發送一個控制幀與包含一個指定控制序列的數據來開始關閉階段握手(詳見5.5.1節)。在收到這樣一個幀時,另一個節點在響應中發送一個Close幀,如果還沒有發送一個。在收到那個控制幀時,第一個節點接著關閉連接,安全地知道沒有更多的數據到來。
發送一個控制幀之后,表示連接將被關閉,一個節點不會發送任何更多的數據;在接收到一個控制幀之后,表示連接將被關閉,一個節點會丟棄收到的任何更多的數據。
對于兩個節點同時地初始化這個握手是安全的。
關閉階段握手目的是完成TCP關閉握手(FIN/ACK),基于TCP關閉階段握手不總是可靠的端到端,尤其在存在攔截代理和中間件。 通過發送一個Close幀并等待響應中的Close幀,某些情況下可避免數據不必要的丟失。例如,在某些平臺上,如果一個socket關閉了,且接收隊列中有數據,一個RST包被發送了,這樣會導致接受RST的一方的recv()失敗,即使有數據等待讀取。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#15%E8%AE%BE%E8%AE%A1%E7%90%86%E5%BF%B5)1.5.設計理念
本節是非規范的。
WebSocket協議應該以最小幀的原則設計(唯一存在的框架是使協議基于幀而不是基于流且支持區分Unicode文本和二進制幀)。期望通過應用層將元數據分層在WebSocket之上,同樣地,通過應用層將元數據分層在TCP之上(例如,HTTP)。
從概念上講,WebSocket只是TCP之上的一層,執行以下操作:
o 為瀏覽器添加一個web 基于來源的安全模型
o 添加一個尋址和協議命名機制來支持在一個IP地址的一個端口的多個主機名的多個服務
o 在TCP之上分一個幀機制層以回到TCP基于的IP包機制,但沒有長度限制
o 包括一個額外的帶內(in-band)關閉階段握手,其被設計來工作在現存的代理和其他中間件。
除此之外,WebSocket沒有添加任何東西。基本上,它的目的是盡可能接近僅暴露原始TCP到腳本,盡可能考慮到Web的約束。它也被設計為它的服務器能與HTTP服務器共享一個端口的這樣一種方式,通過持有它的握手是一個有效的HTTP Upgrade請求。一個可以在概念上使用其他協議來建立客戶端-服務器消息,但WebSocket的意圖是提供一個相對簡單的協議,可以與現有HTTP和部署的HTTP基礎設施(例如代理)同時存在,并盡可能接近TCP,且對于使用考慮到安全考慮的這樣的基礎設施同樣是安全的,有針對性的補充以簡化使用并保持簡單的事情簡單(如增加的消息語義)。
協議的目的是為了可擴展;未來版本將可能引入額外的概念如復用(multiplexing)。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#16%E5%AE%89%E5%85%A8%E6%A8%A1%E5%9E%8B)1.6.安全模型
本節是非規范的。
WebSocket協議使用瀏覽器使用的來源模型限制web頁面可以與WebSocket服務器通信,當WebSocket協議是從一個web頁面使用。當然,當WebSocket協議被一個獨立的客戶端直接使用時(也就是,不是從瀏覽器中的一個web頁面),來源模型不再有用,因為客戶端可以提供任意隨意的來源字符串。
該協議的目的是無法與現有的協議如SMTP[[RFC5321](http://tools.ietf.org/html/rfc5321)]和HTTP建立一個連接,同時允許HTTP服務器來選擇支持該協議如果想要。這是通過具有嚴格的和詳盡的握手和通過限制在握手完成之前能被插入到連接的數據(因此限制多少服務器可以被應用)實現的。
當數據是來自其他協議時,同樣的目的是無法建立連接的,尤其發送到一個WebSocket服務器的HTTP,例如,如果一個HTML“表單”提交到WebScoket服務器可能會發生。這主要通過要求服務器驗證它讀取的握手來實現,它只能做 如果握手包含適當的部分,只能通過一個WebScoket客戶端發送。尤其是,在寫本規范的時侯,|Sec-|開頭的字段不能由web瀏覽器的攻擊者設置,僅能使用HTML和JavaScript API,例如XMLHttpRequest [[XMLHttpRequest](http://tools.ietf.org/html/rfc6455#ref-XMLHttpRequest)]。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#17%E4%B8%8Etcp%E5%92%8Chttp%E7%9A%84%E5%85%B3%E7%B3%BB)1.7.與TCP和HTTP的關系
本節是非規范的。
WebSocket協議是一個獨立的基于TCP的協議。它與HTTP唯一的關系是它的握手是由HTTP服務器解釋為一個Upgrade請求。
默認情況下,WebSocket協議使用端口80用于常規的WebSocket連接和端口443用于WebSocket連接的在傳輸層安全(TLS)[[RFC2818](http://tools.ietf.org/html/rfc2818)]之上的隧道化。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#18%E5%BB%BA%E7%AB%8B%E8%BF%9E%E6%8E%A5)1.8.建立連接
本節是非規范的。
當一個連接到一個HTTP服務器共享的端口時(這種情況是很可能在傳輸信息到端口80和443出現),連接將出現在HTTP服務器,是一個正常的具有一個Upgrade提議的GET請求。在相對簡單的安裝,只用一個IP地址和單臺服務器用于所有數據傳輸到單個主機名,這可能允許一個切實可行的辦法對基于WebSocket協議的系統進行部署。在更復雜的安裝(例如,負載均衡和多服務器),一組獨立的用于WebSocket連接的主機從HTTP服務器分離出來可能更容易管理。在寫該規范的時候,應該指出的是,在端口80和443上的連接有明顯不同的成功率,對于在端口443上的連接是明顯更有可能成功,盡管這可能會隨著時間而改變。
## [](https://github.com/zhangkaitao/websocket-protocol/wiki/1.%E5%BC%95%E8%A8%80#19%E4%BD%BF%E7%94%A8websocket%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%AD%90%E5%8D%8F%E8%AE%AE)1.9.使用WebSocket協議的子協議
本節是非規范的。
客戶端可能通過包含|Sec-WebSocket-Protocol|字段在它的握手中使用一個特定的子協議請求服務器。如果它被指定,服務器需要在它的響應中包含同樣的字段和一個選擇的子協議值用于建立連接。
這些子協議名字應該按照11.5節被注冊。為了避免潛在的碰撞,推薦使用包含ASCII版本的子協議發明人的域名的名字。 例如,如果Example公司要創建一個Chat子協議,由Web上的很多服務器實現,它們可能命名它為“chat.example.com”。如果Example組織命名它們的競爭子協議為“chat.example.org”,那么兩個子協議可能由服務器同時實現,因為服務器根據客戶端發送的值動態地選擇使用哪一個子協議。
通過改變子協議的名字,子協議可以以向后不兼容方式版本化,例如,要從“bookings.example.net”到“v2.bookings.example.net”。就WebSocket客戶端而言,這些子協議將被視為是完全不同的。向后兼容的版本可以通過重用相同的子協議字符串實現,但要仔細設置實際的子協議以支持這種可擴展性。