摘要:幀是發送數據的基本單位,下邊是它的報文格式報文內容中規定了數據標示操作代碼掩碼數據數據長度等格式。首先我們明白了客戶端和服務端進行消息傳遞是這樣的客戶端將消息切割成多個幀,并發送給服務端。服務端接收消息幀,并將關聯的幀重新組裝成完整的消息。
本文概述
Web Sockets的目標是在一個多帶帶的持久連接上提供全雙工、雙向通信。在Javascript創建了Web Socket之后,會有一個HTTP請求發送到瀏覽器以發起連接。在取得服務器響應后,建立的連接會將HTTP升級從HTTP協議交換為WebSocket協議。
由于WebSocket使用自定義的協議,所以URL模式也略有不同。未加密的連接不再是http://,而是ws://;加密的連接也不是https://,而是wss://。在使用WebSocket URL時,必須帶著這個模式,因為將來還有可能支持其他的模式。
使用自定義協議而非HTTP協議的好處是,能夠在客戶端和服務器之間發送非常少量的數據,而不必擔心HTTP那樣字節級的開銷。由于傳遞的數據包很小,所以WebSocket非常適合移動應用。
上文中只是對Web Sockets進行了籠統的描述,接下來的篇幅會對Web Sockets的細節實現進行深入的探索,本文接下來的四個小節不會涉及到大量的代碼片段,但是會對相關的API和技術原理進行分析,相信大家讀完下文之后再來看這段描述,會有一種豁然開朗的感覺。
“握手通道”是HTTP協議中客戶端和服務端通過"TCP三次握手"建立的連接通道。客戶端和服務端使用HTTP協議進行的每次交互都需要先建立這樣一條“通道”,然后通過這條通道進行通信。我們熟悉的ajax交互就是在這樣一個通道上完成數據傳輸的,下面是HTTP協議中建立“握手通道”的過程示意圖:
上文中我們提到:在Javascript創建了WebSocket之后,會有一個HTTP請求發送到瀏覽器以發起連接,然后服務端響應,這就是“握手“的過程,在這個握手的過程當中,客戶端和服務端主要做了兩件事情:
建立了一條連接“握手通道”用于通信(這點和HTTP協議相同,不同的是HTTP協議完成數據交互后就釋放了這條握手通道,這就是所謂的“短連接”,它的生命周期是一次數據交互的時間,通常是毫秒級別的。)
將HTTP協議升級到WebSocket協議,并復用HTTP協議的握手通道,從而建立一條持久連接。
說到這里可能有人會問:HTTP協議為什么不復用自己的“握手通道”,而非要在每次進行數據交互的時候都通過TCP三次握手重新建立“握手通道”呢?答案是這樣的:雖然“長連接”在客戶端和服務端交互的過程中省去了每次都建立“握手通道”的麻煩步驟,但是維持這樣一條“長連接”是需要消耗服務器資源的,而在大多數情況下,這種資源的消耗又是不必要的,可以說HTTP標準的制定經過了深思熟慮的考量。到我們后邊說到WebSocket協議數據幀時,大家可能就會明白,維持一條“持久連接”服務端和客戶端需要做的事情太多了。
說完了握手通道,我們再來看HTTP協議如何升級到WebSocket協議的。
二、HTTP協議升級為WebSocket協議升級協議需要客戶端和服務端交流,服務端怎么知道要將HTTP協議升級到WebSocket協議呢?它一定是接收到了客戶端發送過來的某種信號。下面是我從谷歌瀏覽器中截取的“客戶端發起協議升級請求的報文”,通過分析這段報文,我們能夠得到有關WebSocket中協議升級的更多細節。
首先,客戶端發起協議升級請求。采用的是標準的HTTP報文格式,且只支持GET方法。下面是重點請求的首部的意義:
Connection:Upgrade:表示要升級的協議
Upgrade: websocket:表示要升級到websocket協議
Sec-WebSocket-Version: 13:表示websocket的版本
Sec-WebSocket-Key:UdTUf90CC561cQXn4n5XRg== :與Response Header中的響應首部Sec-WebSocket-Accept: GZk41FJZSYY0CmsrZPGpUGRQzkY=是配套的,提供基本的防護,比如惡意的連接或者無意的連接。
其中Connection就是我們前邊提到的,客戶端發送給服務端的信號,服務端接受到信號之后,才會對HTTP協議進行升級。那么服務端怎樣確認客戶端發送過來的請求是否是合法的呢?在客戶端每次發起協議升級請求的時候都會產生一個唯一碼:Sec-WebSocket-Key。服務端拿到這個碼后,通過一個算法進行校驗,然后通過Sec-WebSocket-Accept響應給客戶端,客戶端再對Sec-WebSocket-Accept進行校驗來完成驗證。這個算法很簡單:
1.將Sec-WebSocket-Key跟全局唯一的(GUID,[RFC4122])標識:258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
2.通過SHA1計算出摘要,并轉成base64字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11這個字符串又叫“魔串",至于為什么要使用它作為Websocket握手計算中使用的字符串,這點我們無需關心,只需要知道它是RFC標準規定就可以了,官方的解析也只是簡單的說此值不大可能被不明白WebSocket協議的網絡終端使用。我們還是用世界上最好的語言來描述一下這個算法吧。
public function dohandshake($sock, $data, $key) { if (preg_match("/Sec-WebSocket-Key: (.*) /", $data, $match)) { $response = base64_encode(sha1($match[1] . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)); $upgrade = "HTTP/1.1 101 Switching Protocol " . "Upgrade: websocket " . "Connection: Upgrade " . "Sec-WebSocket-Accept: " . $response . " "; socket_write($sock, $upgrade, strlen($upgrade)); $this->isHand[$key] = true; } }
服務端響應客戶端的頭部信息和HTTP協議的格式是相同的,所以這里Sec-WebSocket-Accept字段后邊的兩個換行符是少不了的,這和我們使用curl工具模擬get請求是一個道理。這樣展示結果似乎不太直觀,我們使用命令行CLI來根據上圖中的Sec-WebSocket-Key和握手算法來計算一下服務端返回的Sec-WebSocket-Accept是否正確:
從圖中可以看到,通過算法算出來的base64字符串和Sec-WebSocket-Accept是一樣的。那么假如服務端在握手的過程中返回一個錯誤的Sec-WebSocket-Accept字符串會怎么樣呢?當然是客戶端會報錯,連接會建立失敗,大家最好嘗試一下,例如將全局唯一標識符258EAFA5-E914-47DA-95CA-C5AB0DC85B11改為258EAFA5-E914-47DA-95CA-C5AB0DC85B12。
三、WebSocket的幀和數據分片傳輸下圖是我做的一個測試:將小說《飄》的第一章內容復制成文本數據,通過客戶端發送到服務端,然后服務端響應相同的信息完成了一次通信。
可以看到一篇足足有將近15000字節的數據在客戶端和服務端完成通信只用了150ms的時間。我們還可以清晰的看到瀏覽器控制臺中frame欄中顯示的客戶端發送和服務端響應的文本數據,你一定驚訝WebSocket通信強大的數據傳輸能力。數據是否真的像frame中展示的那樣客戶端直接將一大篇文本數據發送到服務端,服務端接收到數據之后,再將一大篇文本數據返回給客戶端呢?這當然是不可能的,我們都知道HTTP協議是基于TCP實現的,HTTP發送數據也是分包轉發的,就是將大數據根據報文形式分割成一小塊一小塊發送到服務端,服務端接收到客戶端發送的報文后,再將小塊的數據拼接組裝。關于HTTP的分包策略,大家可以查看相關資料進行研究,websocket協議也是通過分片打包數據進行轉發的,不過策略上和HTTP的分包不一樣。frame(幀)是websocket發送數據的基本單位,下邊是它的報文格式:
報文內容中規定了數據標示,操作代碼、掩碼、數據、數據長度等格式。不太理解沒關系,下面我通過講解大家只要理解報文中重要標志的作用就可以了。首先我們明白了客戶端和服務端進行Websocket消息傳遞是這樣的:
客戶端:將消息切割成多個幀,并發送給服務端。
服務端:接收消息幀,并將關聯的幀重新組裝成完整的消息。
服務端在接收到客戶端發送的幀消息的時候,將這些幀進行組裝,它怎么知道何時數據組裝完成的呢?這就是報文中左上角FIN(占一個比特)存儲的信息,1表示這是消息的最后一個分片(fragment)如果是0,表示不是消息的最后一個分片。websocket通信中,客戶端發送數據分片是有序的,這一點和HTTP不一樣,HTTP將消息分包之后,是并發無序的發送給服務端的,包信息在數據中的位置則在HTTP報文中存儲,而websocket僅僅需要一個FIN比特位就能保證將數據完整的發送到服務端。
接下來的RSV1,RSV2,RSV3三個比特位的作用又是什么呢?這三個標志位是留給客戶端開發者和服務端開發者開發過程中協商進行拓展的,默認是0。拓展如何使用必須在握手的階段就協商好,其實握手本身也是客戶端和服務端的協商。
Websocket是長連接,為了保持客戶端和服務端的實時雙向通信,需要確保客戶端和服務端之間的TCP通道保持連接沒有斷開。但是對于長時間沒有數據往來的連接,如果依舊保持著,可能會浪費服務端資源。但是不排除有些場景,客戶端和服務端雖然長時間沒有數據往來,仍然需要保持連接,就比如說你幾個月沒有和一個QQ好友聊天了,突然有一天他發QQ消息告訴你他要結婚了,你還是能在第一時間收到。那是因為,客戶端和服務端一直再采用心跳來檢查連接。客戶端和服務端的心跳連接檢測就像打乒乓球一樣:
發送方->接收方:ping
接收方->發送方:pong
等什么時候沒有ping、pong了,那么連接一定是存在問題了。
說了這么多,接下來我使用Go語言來實現一個心跳檢測,Websocket通信實現細節是一件繁瑣的事情,直接使用開源的類庫是比較不錯的選擇,我使用的是:gorilla/websocket。這個類庫已經將websocket的實現細節(握手,數據解碼)封裝的很好啦。下面我就直接貼代碼了:
package main import ( "net/http" "time" "github.com/gorilla/websocket" ) var ( //完成握手操作 upgrade = websocket.Upgrader{ //允許跨域(一般來講,websocket都是獨立部署的) CheckOrigin:func(r *http.Request) bool { return true }, } ) func wsHandler(w http.ResponseWriter, r *http.Request) { var ( conn *websocket.Conn err error data []byte ) //服務端對客戶端的http請求(升級為websocket協議)進行應答,應答之后,協議升級為websocket,http建立連接時的tcp三次握手將保持。 if conn, err = upgrade.Upgrade(w, r, nil); err != nil { return } //啟動一個協程,每隔1s向客戶端發送一次心跳消息 go func() { var ( err error ) for { if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil { return } time.Sleep(1 * time.Second) } }() //得到websocket的長鏈接之后,就可以對客戶端傳遞的數據進行操作了 for { //通過websocket長鏈接讀到的數據可以是text文本數據,也可以是二進制Binary if _, data, err = conn.ReadMessage(); err != nil { goto ERR } if err = conn.WriteMessage(websocket.TextMessage, data); err != nil { goto ERR } } ERR: //出錯之后,關閉socket連接 conn.Close() } func main() { http.HandleFunc("/ws", wsHandler) http.ListenAndServe("0.0.0.0:7777", nil) }
借助go語言很容易搭建協程的特點,我專門開啟了一個協程每秒向客戶端發送一條消息。打開客戶端瀏覽器可以看到,frame中每秒的心跳數據一直在跳動,當長鏈接斷開之后,心跳就沒有了,就像人沒有了心跳一樣:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96879.html
摘要:幀是發送數據的基本單位,下邊是它的報文格式報文內容中規定了數據標示操作代碼掩碼數據數據長度等格式。首先我們明白了客戶端和服務端進行消息傳遞是這樣的客戶端將消息切割成多個幀,并發送給服務端。服務端接收消息幀,并將關聯的幀重新組裝成完整的消息。 本文概述 Web Sockets的目標是在一個單獨的持久連接上提供全雙工、雙向通信。在Javascript創建了Web Socket之后,會有一個...
摘要:數據作為消息通過傳輸,每個消息由一個或多個幀組成,其中包含正在發送的數據有效負載。幀數據如上所述,數據可以被分割成多個幀。但是,規范希望能夠處理交錯的控制幀。 文章底部分享給大家一套 react + socket 實戰教程 這是專門探索 JavaScript 及其所構建的組件的系列文章的第5篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章...
摘要:什么是傳統的通訊模式是客戶端發起請求,服務端接收請求并作出響應。而協議復用了的握手通道,具體指的是,客戶端通過請求與服務端協商升級協議。第二步,交換數據,客戶端與服務端可以使用協議進行雙向通訊。 問題 showImg(https://segmentfault.com/img/bVbqF3z?w=704&h=66);一天更新完主分支后啟動nginx,結果報錯:nginx: [emerg]...
閱讀 1244·2023-04-25 18:57
閱讀 2127·2023-04-25 16:28
閱讀 3926·2021-11-24 09:39
閱讀 3631·2021-11-16 11:45
閱讀 1816·2021-10-13 09:40
閱讀 1260·2019-08-30 15:52
閱讀 1715·2019-08-30 10:57
閱讀 657·2019-08-29 16:55