刨根問底HTTP和WebSocket協議(二)

Alchemist發表於2016-08-22

HTTP vs WebSocket

上篇介紹了HTTP1.1協議的基本內容,這篇文章將繼續分析WebSocket協議,然後對這兩個進行簡單的比較。

WebSocket

WebSocket協議還很年輕,RFC文件相比HTTP的釋出時間也很短,它的誕生是為了建立一種「雙向通訊」的協議,來作為HTTP協議的一個替代者。那麼首先看一下它和HTTP(或者HTTP的長連線)的區別。

為什麼要用WebSocket來替代HTTP

上一篇中提到WebSocket的目的就是解決網路傳輸中的雙向通訊的問題,HTTP1.1預設使用持久連線(persistent connection),在一個TCP連線上也可以傳輸多個Request/Response訊息對,但是HTTP的基本模型還是一個Request對應一個Response。這在雙向通訊(客戶端要向伺服器傳送資料,同時伺服器也需要實時的向客戶端傳送資訊,一個聊天系統就是典型的雙向通訊)時一般會使用這樣幾種解決方案:

  1. 輪訓,輪詢就會造成對網路和通訊雙方的資源的浪費,且非實時。
  2. 長輪訓,客戶端傳送一個超時時間很長的Request,伺服器hold住這個連線,在有新資料到達時返回Response,相比#1,佔用的網路頻寬少了,其他類似。
  3. 長連線,其實有些人對長連線的概念是模糊不清的,我這裡講的其實是HTTP的長連線(1)。如果你使用Socket來建立TCP的長連線(2),那麼,這個長連線(2)跟我們這裡要討論的WebSocket是一樣的,實際上TCP長連線就是WebSocket的基礎,但是如果是HTTP的長連線,本質上還是Request/Response訊息對,仍然會造成資源的浪費、實時性不強等問題。

HTTP的長連線模型

協議基礎

WebSocket的目的是取代HTTP在雙向通訊場景下的使用,而且它的實現方式有些也是基於HTTP的(WS的預設埠是80和443)。現有的網路環境(客戶端、伺服器、網路中間人、代理等)對HTTP都有很好的支援,所以這樣做可以充分利用現有的HTTP的基礎設施,有點向下相容的意味。

簡單來講,WS協議有兩部分組成:握手和資料傳輸。

握手(handshake)

出於相容性的考慮,WS的握手使用HTTP來實現(此文件中提到未來有可能會使用專用的埠和方法來實現握手),客戶端的握手訊息就是一個「普通的,帶有Upgrade頭的,HTTP Request訊息」。所以這一個小節到內容大部分都來自於RFC2616,這裡只是它的一種應用形式,下面是RFC6455文件中給出的一個客戶端握手訊息示例:

    GET /chat HTTP/1.1            //1
    Host: server.example.com   //2
    Upgrade: websocket            //3
    Connection: Upgrade            //4
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==            //5
    Origin: http://example.com            //6
    Sec-WebSocket-Protocol: chat, superchat            //7
    Sec-WebSocket-Version: 13            //8複製程式碼

可以看到,前兩行跟HTTP的Request的起始行一模一樣,而真正在WS的握手過程中起到作用的是下面幾個header域。

  1. Upgrade:upgrade是HTTP1.1中用於定義轉換協議的header域。它表示,如果伺服器支援的話,客戶端希望使用現有的「網路層」已經建立好的這個「連線(此處是TCP連線)」,切換到另外一個「應用層」(此處是WebSocket)協議。

  2. Connection:HTTP1.1中規定Upgrade只能應用在「直接連線」中,所以帶有Upgrade頭的HTTP1.1訊息必須含有Connection頭,因為Connection頭的意義就是,任何接收到此訊息的人(往往是代理伺服器)都要在轉發此訊息之前處理掉Connection中指定的域(不轉發Upgrade域)。 如果客戶端和伺服器之間是通過代理連線的,那麼在傳送這個握手訊息之前首先要傳送CONNECT訊息來建立直接連線。

  3. Sec-WebSocket-*:第7行標識了客戶端支援的子協議的列表(關於子協議會在下面介紹),第8行標識了客戶端支援的WS協議的版本列表,第5行用來傳送給伺服器使用(伺服器會使用此欄位組裝成另一個key值放在握手返回資訊裡傳送客戶端)。

  4. Origin:作安全使用,防止跨站攻擊,瀏覽器一般會使用這個來標識原始域。

如果伺服器接受了這個請求,可能會傳送如下這樣的返回資訊,這是一個標準的HTTP的Response訊息。101表示伺服器收到了客戶端切換協議的請求,並且同意切換到此協議。RFC2616規定只有切換到的協議「比HTTP1.1更好」的時候才能同意切換。

    HTTP/1.1 101 Switching Protocols //1
    Upgrade: websocket. //2
    Connection: Upgrade. //3
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=  //4
    Sec-WebSocket-Protocol: chat. //5複製程式碼

WebSocket協議Uri

ws協議預設使用80埠,wss協議預設使用443埠。

      ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
      wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

      host = 
      port = 
      path = 
      query = 複製程式碼

在客戶端傳送握手之前要做的一些小事

在握手之前,客戶端首先要先建立連線,一個客戶端對於一個相同的目標地址(通常是域名或者IP地址,不是資源地址)同一時刻只能有一個處於CONNECTING狀態(就是正在建立連線)的連線。從建立連線到傳送握手訊息這個過程大致是這樣的:

  1. 客戶端檢查輸入的Uri是否合法。
  2. 客戶端判斷,如果當前已有指向此目標地址(IP地址)的連線(A)仍處於CONNECTING狀態,需要等待這個連線(A)建立成功,或者建立失敗之後才能繼續建立新的連線。
    PS:如果當前連線是處於代理的網路環境中,無法判斷IP地址是否相同,則認為每一個Host地址為一個單獨的目標地址,同時客戶端應當限制同時處於CONNECTING狀態的連線數。 PPS:這樣可以防止一部分的DDOS攻擊。 PPPS:客戶端並不限制同時處於「已成功」狀態的連線數,但是如果一個客戶端「持有大量已成功狀態的連線的」,伺服器或許會拒絕此客戶端請求的新連線。
  3. 如果客戶端處於一個代理環境中,它首先要請求它的代理來建立一個到達目標地址的TCP連線。 例如,如果客戶端處於代理環境中,它想要連線某目標地址的80埠,它可能要收現傳送以下訊息:

           CONNECT example.com:80 HTTP/1.1
           Host: example.com複製程式碼

    如果客戶端沒有處於代理環境中,它就要首先建立一個到達目標地址的直接的TCP連線。

  4. 如果上一步中的TCP連線建立失敗,則此WebSocket連線失敗。
  5. 如果協議是wss,則在上一步建立的TCP連線之上,使用TSL傳送握手資訊。如果失敗,則此WebSocket連線失敗;如果成功,則以後的所有資料都要通過此TSL通道進行傳送。

對於客戶端握手資訊的一些小要求

  1. 握手必須是RFC2616中定義的Request訊息
  2. 此Request訊息的方法必須是GET,HTTP版本必須大於1.1 。 以下是某WS的Uri對應的Request訊息:
     ws://example.com/chat
     GET /chat HTTP/1.1複製程式碼
  3. 此Request訊息中Request-URI部分(RFC2616中的概念)所定義的資型必須和WS協議的Uri中定義的資源相同。
  4. 此Request訊息中必須含有Host頭域,其內容必須和WS的Uri中定義的相同。
  5. 此Request訊息必須包含Upgrade頭域,其內容必須包含websocket關鍵字。
  6. 此Request訊息必須包含Connection頭域,其內容必須包含Upgrade指令。
  7. 此Request訊息必須包含Sec-WebSocket-Key頭域,其內容是一個Base64編碼的16位隨機字元。
  8. 如果客戶端是瀏覽器,此Request訊息必須包含Origin頭域,其內容是參考RFC6454
  9. 此Request訊息必須包含Sec-WebSocket-Version頭域,在此協議中定義的版本號是13。
  10. 此Request訊息可能包含Sec-WebSocket-Protocol頭域,其意義如上文中所述。
  11. 此Request訊息可能包含Sec-WebSocket-Extensions頭域,客戶端和伺服器可以使用此header來進行一些功能的擴充套件。
  12. 此Request訊息可能包含任何合法的頭域。如RFC2616中定義的那些。

在客戶端接收到Response握手訊息之後要做的一些事情

  1. 如果返回的返回碼不是101,則按照RFC2616進行處理。如果是101,進行下一步,開始解析header域,所有header域的值不區分大小寫。
  2. 判斷是否含有Upgrade頭,且內容包含websocket。
  3. 判斷是否含有Connection頭,且內容包含Upgrade
  4. 判斷是否含有Sec-WebSocket-Accept頭,其內容在下面介紹。
  5. 如果含有Sec-WebSocket-Extensions頭,要判斷是否之前的Request握手帶有此內容,如果沒有,則連線失敗。
  6. 如果含有Sec-WebSocket-Protocol頭,要判斷是否之前的Request握手帶有此協議,如果沒有,則連線失敗。

服務端的概念

服務端指的是所有參與處理WebSocket訊息的基礎設施,比如如果某伺服器使用Nginx(A)來處理WebSocket,然後把處理後的訊息傳給響應的伺服器(B),那麼A和B都是這裡要討論的服務端的範疇。

接受了客戶端的連線請求,服務端要做的一些事情

如果請求是HTTPS,則首先要使用TLS進行握手,如果失敗,則關閉連線,如果成功,則之後的資料都通過此通道進行傳送。

之後服務端可以進行一些客戶端驗證步驟(包括對客戶端header域的驗證),如果需要,則按照RFC2616來進行錯誤碼的返回。

如果一切都成功,則返回成功的Response握手訊息。

服務端傳送的成功的Response握手

此握手訊息是一個標準的HTTP Response訊息,同時它包含了以下幾個部分:

  1. 狀態行(如上一篇RFC2616中所述)
  2. Upgrade頭域,內容為websocket
  3. Connection頭域,內容為Upgrade
  4. Sec-WebSocket-Accept頭域,其內容的生成步驟:
    1. 首先將Sec-WebSocket-Key的內容加上字串258EAFA5-E914-47DA-95CA-C5AB0DC85B11(一個UUID)。
    2. 將#1中生成的字串進行SHA1編碼。
    3. 將#2中生成的字串進行Base64編碼。
  5. Sec-WebSocket-Protocol頭域(可選)
  6. Sec-WebSocket-Extensions頭域(可選)

一旦這個握手發出去,服務端就認為此WebSocket連線已經建立成功,處於OPEN狀態。它就可以開始傳送資料了。

WebSocket的一些擴充套件

Sec-WebSocket-Version可以被通訊雙方用來支援更多的協議的擴充套件,RFC6455中定義的值為13,WebSocket的客戶端和服務端可能回自定義更多的版本號來支援更多的功能。其使用方法如上文所述。

傳送資料

WebSocket中所有傳送的資料使用幀的形式傳送。客戶端傳送的資料幀都要經過掩碼處理,服務端傳送的所有資料幀都不能經過掩碼處理。否則對方需要傳送關閉幀。

一個幀包含一個幀型別的標識碼,一個負載長度,和負載。負載包括擴充套件內容和應用內容。

幀型別

幀型別是由一個4位長的叫Opcode的值表示,任何WebSocket的通訊方收到一個位置的幀型別,都要以連線失敗的方式斷開此連線。 RFC6455中定義的幀型別如下所示:

  1. Opcode == 0 繼續

    表示此幀是一個繼續幀,需要拼接在上一個收到的幀之後,來組成一個完整的訊息。由於這種解析特性,非控制幀的傳送和接收必須是相同的順序。

  2. Opcode == 1 文字幀
  3. Opcode == 2 二進位制幀
  4. Opcode == 3-7 未來使用(非控制幀)
  5. Opcode == 8 關閉連線(控制幀)

    此幀可能會包含內容,以表示關閉連線的原因。

    通訊的某一方傳送此幀來關閉WebSocket連線,收到此幀的一方如果之前沒有傳送此幀,則需要傳送一個同樣的關閉幀以確認關閉。如果雙方同時傳送此幀,則雙方都需要傳送迴應的關閉幀。

    理想情況服務端在確認WebSocket連線關閉後,關閉相應的TCP連線,而客戶端需要等待服務端關閉此TCP連線,但客戶端在某些情況下也可以關閉TCP連線。

  6. Opcode == 9 Ping

    類似於心跳,一方收到Ping,應當立即傳送Pong作為響應。

  7. Opcode == 10 Pong

    如果通訊一方並沒有傳送Ping,但是收到了Pong,並不要求它返回任何資訊。Pong幀的內容應當和收到的Ping相同。可能會出現一方收到很多的Ping,但是隻需要響應最近的那一次就可以了。

  8. Opcode == 11-15 未來使用(控制幀)

幀的格式

具體的每一項代表什麼意思在這裡就不做詳細的闡述了。

  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 ...                |
 +---------------------------------------------------------------+複製程式碼

與HTTP比較

同樣作為應用層的協議,WebSocket在現代的軟體開發中被越來越多的實踐,和HTTP有很多相似的地方,這裡將它們簡單的做一個純個人、非權威的比較:

相同點

  1. 都是基於TCP的應用層協議。
  2. 都使用Request/Response模型進行連線的建立。
  3. 在連線的建立過程中對錯誤的處理方式相同,在這個階段WS可能返回和HTTP相同的返回碼。
  4. 都可以在網路中傳輸資料。

不同點

  1. WS使用HTTP來建立連線,但是定義了一系列新的header域,這些域在HTTP中並不會使用。
  2. WS的連線不能通過中間人來轉發,它必須是一個直接連線。
  3. WS連線建立之後,通訊雙方都可以在任何時刻向另一方傳送資料。
  4. WS連線建立之後,資料的傳輸使用幀來傳遞,不再需要Request訊息。
  5. WS的資料幀有序。

待續

這一篇簡單地將WebSocket協議介紹了一遍,篇幅有點長了,資料幀也沒有來得及詳述。下篇會繼續深扒WebSocket幀傳輸,另外將通過例項探討一些WebSocket協議實際使用中的問題。

相關文章