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和技術原理進行分析,相信你們讀完下文以後再來看這段描述,會有一種豁然開朗的感受。git
「握手通道」是HTTP協議中客戶端和服務端經過"TCP三次握手"創建的鏈接通道。客戶端和服務端使用HTTP協議進行的每次交互都須要先創建這樣一條「通道」,而後經過這條通道進行通訊。咱們熟悉的ajax交互就是在這樣一個通道上完成數據傳輸的,下面是HTTP協議中創建「握手通道」的過程示意圖:github
上文中咱們提到:在Javascript建立了WebSocket以後,會有一個HTTP請求發送到瀏覽器以發起鏈接,而後服務端響應,這就是「握手「的過程,在這個握手的過程中,客戶端和服務端主要作了兩件事情:web
說到這裏可能有人會問:HTTP協議爲何不復用本身的「握手通道」,而非要在每次進行數據交互的時候都經過TCP三次握手從新創建「握手通道」呢?答案是這樣的:雖然「長鏈接」在客戶端和服務端交互的過程當中省去了每次都創建「握手通道」的麻煩步驟,可是維持這樣一條「長鏈接」是須要消耗服務器資源的,而在大多數狀況下,這種資源的消耗又是沒必要要的,能夠說HTTP標準的制定通過了深思熟慮的考量。到咱們後邊說到WebSocket協議數據幀時,你們可能就會明白,維持一條「持久鏈接」服務端和客戶端須要作的事情太多了。ajax
說完了握手通道,咱們再來看HTTP協議如何升級到WebSocket協議的。算法
升級協議須要客戶端和服務端交流,服務端怎麼知道要將HTTP協議升級到WebSocket協議呢?它必定是接收到了客戶端發送過來的某種信號。下面是我從谷歌瀏覽器中截取的「客戶端發起協議升級請求的報文」,經過分析這段報文,咱們可以獲得有關WebSocket中協議升級的更多細節。
首先,客戶端發起協議升級請求。採用的是標準的HTTP報文格式,且只支持GET方法。下面是重點請求的首部的意義:
其中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協議的網絡終端使用。咱們仍是用世界上最好的語言來描述一下這個算法吧。websocket
public function dohandshake($sock, $data, $key) { if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) { $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $response . "\r\n\r\n"; 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。
下圖是我作的一個測試:將小說《飄》的第一章內容複製成文本數據,經過客戶端發送到服務端,而後服務端響應相同的信息完成了一次通訊。
能夠看到一篇足足有將近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了,那麼鏈接必定是存在問題了。
說了這麼多,接下來我使用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中每秒的心跳數據一直在跳動,當長連接斷開以後,心跳就沒有了,就像人沒有了心跳同樣: