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