WebSocket
WebSocket協議還很年輕,RFC文檔相比HTTP的發布時間也很短,它的誕生是為了創建一種「雙向通信」的協議,來作為HTTP協議的一個替代者。那么首先看一下它和HTTP(或者HTTP的長連接)的區別。
為什么要用 WebSocket 來替代 HTTP
上一篇中提到WebSocket的目的就是解決網絡傳輸中的雙向通信的問題,HTTP1.1默認使用持久連接(persistent connection),在一個TCP連接上也可以傳輸多個Request/Response消息對,但是HTTP的基本模型還是一個Request對應一個Response。這在雙向通信(客戶端要向服務器傳送數據,同時服務器也需要實時的向客戶端傳送信息,一個聊天系統就是典型的雙向通信)時一般會使用這樣幾種解決方案:
- 輪詢(polling),輪詢就會造成對網絡和通信雙方的資源的浪費,且非實時。
- 長輪詢,客戶端發送一個超時時間很長的Request,服務器hold住這個連接,在有新數據到達時返回Response,相比#1,占用的網絡帶寬少了,其他類似。
- 長連接,其實有些人對長連接的概念是模糊不清的,我這里講的其實是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文檔中給出的一個客戶端握手消息示例:
1
2
3
4
5
6
7
8
|
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域。
Upgrade:upgrade是HTTP1.1中用於定義轉換協議的header域。它表示,如果服務器支持的話,客戶端希望使用現有的「網絡層」已經建立好的這個「連接(此處是TCP連接)」,切換到另外一個「應用層」(此處是WebSocket)協議。
Connection:HTTP1.1中規定Upgrade只能應用在「直接連接」中,所以帶有Upgrade頭的HTTP1.1消息必須含有Connection頭,因為Connection頭的意義就是,任何接收到此消息的人(往往是代理服務器)都要在轉發此消息之前處理掉Connection中指定的域(不轉發Upgrade域)。
如果客戶端和服務器之間是通過代理連接的,那么在發送這個握手消息之前首先要發送CONNECT消息來建立直接連接。
Sec-WebSocket-*:第7行標識了客戶端支持的子協議的列表(關於子協議會在下面介紹),第8行標識了客戶端支持的WS協議的版本列表,第5行用來發送給服務器使用(服務器會使用此字段組裝成另一個key值放在握手返回信息里發送客戶端)。
Origin:作安全使用,防止跨站攻擊,瀏覽器一般會使用這個來標識原始域。
如果服務器接受了這個請求,可能會發送如下這樣的返回信息,這是一個標准的HTTP的Response消息。101表示服務器收到了客戶端切換協議的請求,並且同意切換到此協議。RFC2616規定只有切換到的協議「比HTTP1.1更好」的時候才能同意切換。
1
2
3
4
5
|
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端口。
1
2
3
4
5
6
7
|
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>
|
在客戶端發送握手之前要做的一些小事
在握手之前,客戶端首先要先建立連接,一個客戶端對於一個相同的目標地址(通常是域名或者IP地址,不是資源地址)同一時刻只能有一個處於CONNECTING狀態(就是正在建立連接)的連接。從建立連接到發送握手消息這個過程大致是這樣的:
- 客戶端檢查輸入的Uri是否合法。
- 客戶端判斷,如果當前已有指向此目標地址(IP地址)的連接(A)仍處於CONNECTING狀態,需要等待這個連接(A)建立成功,或者建立失敗之后才能繼續建立新的連接。
PS:如果當前連接是處於代理的網絡環境中,無法判斷IP地址是否相同,則認為每一個Host地址為一個單獨的目標地址,同時客戶端應當限制同時處於CONNECTING狀態的連接數。
PPS:這樣可以防止一部分的DDOS攻擊。
PPPS:客戶端並不限制同時處於「已成功」狀態的連接數,但是如果一個客戶端「持有大量已成功狀態的連接的」,服務器或許會拒絕此客戶端請求的新連接。 - 如果客戶端處於一個代理環境中,它首先要請求它的代理來建立一個到達目標地址的TCP連接。
例如,如果客戶端處於代理環境中,它想要連接某目標地址的80端口,它可能要收現發送以下消息:
1
2
|
CONNECT example.com:80 HTTP/1.1
Host: example.com
|
如果客戶端沒有處於代理環境中,它就要首先建立一個到達目標地址的直接的TCP連接。
4.如果上一步中的TCP連接建立失敗,則此WebSocket連接失敗。
5.如果協議是wss,則在上一步建立的TCP連接之上,使用TSL發送握手信息。如果失敗,則此WebSocket連接失敗;如果成功,則以后的所有數據都要通過此TSL通道進行發送。
對於客戶端握手信息的一些小要求
- 握手必須是RFC2616中定義的Request消息
- 此Request消息的方法必須是GET,HTTP版本必須大於1.1 。
以下是某WS的Uri對應的Request消息:
1
2
|
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 握手消息之后要做的一些事情
- 如果返回的返回碼不是101,則按照RFC2616進行處理。如果是101,進行下一步,開始解析header域,所有header域的值不區分大小寫。
- 判斷是否含有Upgrade頭,且內容包含websocket。
- 判斷是否含有Connection頭,且內容包含Upgrade
- 判斷是否含有Sec-WebSocket-Accept頭,其內容在下面介紹。
- 如果含有Sec-WebSocket-Extensions頭,要判斷是否之前的Request握手帶有此內容,如果沒有,則連接失敗。
- 如果含有Sec-WebSocket-Protocol頭,要判斷是否之前的Request握手帶有此協議,如果沒有,則連接失敗。
服務端的概念
服務端指的是所有參與處理WebSocket消息的基礎設施,比如如果某服務器使用Nginx(A)來處理WebSocket,然后把處理后的消息傳給響應的服務器(B),那么A和B都是這里要討論的服務端的范疇。
接受了客戶端的連接請求,服務端要做的一些事情
如果請求是HTTPS,則首先要使用TLS進行握手,如果失敗,則關閉連接,如果成功,則之后的數據都通過此通道進行發送。
之后服務端可以進行一些客戶端驗證步驟(包括對客戶端header域的驗證),如果需要,則按照RFC2616來進行錯誤碼的返回。
如果一切都成功,則返回成功的Response握手消息。
服務端發送的成功的 Response 握手
此握手消息是一個標准的HTTP Response消息,同時它包含了以下幾個部分:
- 狀態行(如上一篇RFC2616中所述)
- Upgrade頭域,內容為websocket
- Connection頭域,內容為Upgrade
- Sec-WebSocket-Accept頭域,其內容的生成步驟:
a.首先將Sec-WebSocket-Key的內容加上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11(一個UUID)。
b.將#1中生成的字符串進行SHA1編碼。
c.將#2中生成的字符串進行Base64編碼。 - Sec-WebSocket-Protocol頭域(可選)
- 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 未來使用(控制幀)
幀的格式
具體的每一項代表什么意思在這里就不做詳細的闡述了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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有很多相似的地方,這里將它們簡單的做一個純個人、非權威的比較:
相同點
- 都是基於TCP的應用層協議。
- 都使用Request/Response模型進行連接的建立。
- 在連接的建立過程中對錯誤的處理方式相同,在這個階段WS可能返回和HTTP相同的返回碼。
- 都可以在網絡中傳輸數據。
不同點
- WS使用HTTP來建立連接,但是定義了一系列新的header域,這些域在HTTP中並不會使用。
- WS的連接不能通過中間人來轉發,它必須是一個直接連接。
- WS連接建立之后,通信雙方都可以在任何時刻向另一方發送數據。
- WS連接建立之后,數據的傳輸使用幀來傳遞,不再需要Request消息。
- WS的數據幀有序。