WebSocket協議還很年輕,RFC文檔相比HTTP的發佈時間也很短,它的誕生是爲了建立一種「雙向通訊」的協議,來做爲HTTP協議的一個替代者。那麼首先看一下它和HTTP(或者HTTP的長鏈接)的區別。html
上一篇中提到WebSocket的目的就是解決網絡傳輸中的雙向通訊的問題,HTTP1.1默認使用持久鏈接(persistent connection),在一個TCP鏈接上也能夠傳輸多個Request/Response消息對,可是HTTP的基本模型仍是一個Request對應一個Response。這在雙向通訊(客戶端要向服務器傳送數據,同時服務器也須要實時的向客戶端傳送信息,一個聊天系統就是典型的雙向通訊)時通常會使用這樣幾種解決方案:web
輪詢(polling),輪詢就會形成對網絡和通訊雙方的資源的浪費,且非實時。瀏覽器
長輪詢,客戶端發送一個超時時間很長的Request,服務器hold住這個鏈接,在有新數據到達時返回Response,相比#1,佔用的網絡帶寬少了,其餘相似。安全
長鏈接,其實有些人對長鏈接的概念是模糊不清的,我這裏講的實際上是HTTP的長鏈接(1)。若是你使用Socket來創建TCP的長鏈接(2),那麼,這個長鏈接(2)跟咱們這裏要討論的WebSocket是同樣的,實際上TCP長鏈接就是WebSocket的基礎,可是若是是HTTP的長鏈接,本質上仍是Request/Response消息對,仍然會形成資源的浪費、實時性不強等問題。ruby
HTTP的長鏈接模型服務器
WebSocket的目的是取代HTTP在雙向通訊場景下的使用,並且它的實現方式有些也是基於HTTP的(WS的默認端口是80
和443
)。現有的網絡環境(客戶端、服務器、網絡中間人、代理等)對HTTP都有很好的支持,因此這樣作能夠充分利用現有的HTTP的基礎設施,有點向下兼容的意味。websocket
簡單來說,WS協議有兩部分組成:握手和數據傳輸。網絡
出於兼容性的考慮,WS的握手使用HTTP來實現(此文檔中提到將來有可能會使用專用的端口和方法來實現握手),客戶端的握手消息就是一個「普通的,帶有Upgrade頭的,HTTP Request消息」。因此這一個小節到內容大部分都來自於RFC2616,這裏只是它的一種應用形式,下面是RFC6455文檔中給出的一個客戶端握手消息示例:socket
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域。ide
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更好」的時候才能贊成切換。
HTTP/1.1 101 Switching Protocols //1 Upgrade: websocket. //2 Connection: Upgrade. //3 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= //4 Sec-WebSocket-Protocol: chat. //5
ws協議默認使用80
端口,wss協議默認使用443
端口。
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
端口,它可能要收現發送如下消息:
CONNECT example.com:80 HTTP/1.1 Host: example.com
若是客戶端沒有處於代理環境中,它就要首先創建一個到達目標地址的直接的TCP鏈接。
若是上一步中的TCP鏈接創建失敗,則此WebSocket鏈接失敗。
若是協議是wss,則在上一步創建的TCP鏈接之上,使用TSL發送握手信息。若是失敗,則此WebSocket鏈接失敗;若是成功,則之後的全部數據都要經過此TSL通道進行發送。
握手必須是RFC2616中定義的Request消息
此Request消息的方法必須是GET,HTTP版本必須大於1.1 。
如下是某WS的Uri對應的Request消息:
ws://example.com/chat GET /chat HTTP/1.1
此Request消息中Request-URI部分(RFC2616中的概念)所定義的資型必須和WS協議的Uri中定義的資源相同。
此Request消息中必須含有Host頭域,其內容必須和WS的Uri中定義的相同。
此Request消息必須包含Upgrade頭域,其內容必須包含websocket關鍵字。
此Request消息必須包含Connection頭域,其內容必須包含Upgrade指令。
此Request消息必須包含Sec-WebSocket-Key頭域,其內容是一個Base64編碼的16位隨機字符。
若是客戶端是瀏覽器,此Request消息必須包含Origin頭域,其內容是參考RFC6454。
此Request消息必須包含Sec-WebSocket-Version頭域,在此協議中定義的版本號是13。
此Request消息可能包含Sec-WebSocket-Protocol頭域,其意義如上文中所述。
此Request消息可能包含Sec-WebSocket-Extensions頭域,客戶端和服務器可使用此header來進行一些功能的擴展。
此Request消息可能包含任何合法的頭域。如RFC2616中定義的那些。
若是返回的返回碼不是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握手消息。
此握手消息是一個標準的HTTP Response消息,同時它包含了如下幾個部分:
狀態行(如上一篇RFC2616中所述)
Upgrade頭域,內容爲websocket
Connection頭域,內容爲Upgrade
Sec-WebSocket-Accept頭域,其內容的生成步驟:
首先將Sec-WebSocket-Key的內容加上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
(一個UUID)。
將#1中生成的字符串進行SHA1編碼。
將#2中生成的字符串進行Base64編碼。
Sec-WebSocket-Protocol頭域(可選)
Sec-WebSocket-Extensions頭域(可選)
一旦這個握手發出去,服務端就認爲此WebSocket鏈接已經創建成功,處於OPEN狀態。它就能夠開始發送數據了。
Sec-WebSocket-Version能夠被通訊雙方用來支持更多的協議的擴展,RFC6455中定義的值爲13
,WebSocket的客戶端和服務端可能回自定義更多的版本號來支持更多的功能。其使用方法如上文所述。
WebSocket中全部發送的數據使用幀的形式發送。客戶端發送的數據幀都要通過掩碼處理,服務端發送的全部數據幀都不能通過掩碼處理。不然對方須要發送關閉幀。
一個幀包含一個幀類型的標識碼,一個負載長度,和負載。負載包括擴展內容和應用內容。
幀類型是由一個4位長的叫Opcode的值表示,任何WebSocket的通訊方收到一個位置的幀類型,都要以鏈接失敗的方式斷開此鏈接。
RFC6455中定義的幀類型以下所示:
Opcode == 0 繼續
表示此幀是一個繼續幀,須要拼接在上一個收到的幀以後,來組成一個完整的消息。因爲這種解析特性,非控制幀的發送和接收必須是相同的順序。
Opcode == 1 文本幀
Opcode == 2 二進制幀
Opcode == 3 - 7 將來使用(非控制幀)
Opcode == 8 關閉鏈接(控制幀)
此幀可能會包含內容,以表示關閉鏈接的緣由。
通訊的某一方發送此幀來關閉WebSocket鏈接,收到此幀的一方若是以前沒有發送此幀,則須要發送一個一樣的關閉幀以確認關閉。若是雙方同時發送此幀,則雙方都須要發送迴應的關閉幀。
理想狀況服務端在確認WebSocket鏈接關閉後,關閉相應的TCP鏈接,而客戶端須要等待服務端關閉此TCP鏈接,但客戶端在某些狀況下也能夠關閉TCP鏈接。
Opcode == 9 Ping
相似於心跳,一方收到Ping,應當當即發送Pong做爲響應。
Opcode == 10 Pong
若是通訊一方並無發送Ping,可是收到了Pong,並不要求它返回任何信息。Pong幀的內容應當和收到的Ping相同。可能會出現一方收到不少的Ping,可是隻須要響應最近的那一次就能夠了。
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 ... | +---------------------------------------------------------------+
一樣做爲應用層的協議,WebSocket在現代的軟件開發中被愈來愈多的實踐,和HTTP有不少類似的地方,這裏將它們簡單的作一個純我的、非權威的比較:
都是基於TCP的應用層協議。
都使用Request/Response模型進行鏈接的創建。
在鏈接的創建過程當中對錯誤的處理方式相同,在這個階段WS可能返回和HTTP相同的返回碼。
均可以在網絡中傳輸數據。
WS使用HTTP來創建鏈接,可是定義了一系列新的header域,這些域在HTTP中並不會使用。
WS的鏈接不能經過中間人來轉發,它必須是一個直接鏈接。
WS鏈接創建以後,通訊雙方均可以在任什麼時候刻向另外一方發送數據。
WS鏈接創建以後,數據的傳輸使用幀來傳遞,再也不須要Request消息。
WS的數據幀有序。
這一篇簡單地將WebSocket協議介紹了一遍,篇幅有點長了,數據幀也沒有來得及詳述。下篇會繼續深扒WebSocket幀傳輸,另外將經過實例探討一些WebSocket協議實際使用中的問題。
刨根問底HTTP和WebSocket協議(一)
WebSocket和Socket的區別(WebSocket外傳)
文/TheAlchemist(簡書做者)原文連接:http://www.jianshu.com/p/f666da1b1835著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。