設(shè)置
  • 日夜間
    隨系統(tǒng)
    淺色
    深色
  • 主題色

為什么有 HTTP 協(xié)議,還要有 websocket 協(xié)議

小白debug 2022/10/11 11:56:20 責(zé)編:子非

平時我們打開網(wǎng)頁,比如購物網(wǎng)站某寶。都是點(diǎn)一下列表商品,跳轉(zhuǎn)一下網(wǎng)頁就到了商品詳情。

從 HTTP 協(xié)議的角度來看,就是點(diǎn)一下網(wǎng)頁上的某個按鈕,前端發(fā)一次 HTTP 請求,網(wǎng)站返回一次 HTTP 響應(yīng)。

這種由客戶端主動請求,服務(wù)器響應(yīng)的方式也滿足大部分網(wǎng)頁的功能場景。

但有沒有發(fā)現(xiàn),這種情況下,服務(wù)器從來就不會主動給客戶端發(fā)一次消息。

就像你喜歡的女生從來不會主動找你一樣。

但如果現(xiàn)在,你在刷網(wǎng)頁的時候右下角突然彈出一個小廣告,提示你【一個人在家偷偷才能玩哦】。

求知,好學(xué),勤奮,這些刻在你 DNA 里的東西都動起來了。

你點(diǎn)開后發(fā)現(xiàn)。

長相平平無奇的古某提示你 "道士 9 條狗,全服橫著走"。

影帝某輝老師跟你說 "系兄弟就來砍我"。

來都來了,你就選了個角色進(jìn)到了游戲界面里。

創(chuàng)建角色頁面

這時候,上來就是一個小怪,從遠(yuǎn)處走來,然后瘋狂拿木棒子抽你。

你全程沒點(diǎn)任何一次鼠標(biāo)。服務(wù)器就自動將怪物的移動數(shù)據(jù)和攻擊數(shù)據(jù)源源不斷發(fā)給你了。

這….太暖心了。

感動之余,問題就來了,

像這種看起來服務(wù)器主動發(fā)消息給客戶端的場景,是怎么做到的?

在真正回答這個問題之前,我們先來聊下一些相關(guān)的知識背景。

使用 HTTP 不斷輪詢

其實(shí)問題的痛點(diǎn)在于,怎么樣才能在用戶不做任何操作的情況下,網(wǎng)頁能收到消息并發(fā)生變更。

最常見的解決方案是,網(wǎng)頁的前端代碼里不斷定時發(fā) HTTP 請求到服務(wù)器,服務(wù)器收到請求后給客戶端響應(yīng)消息。

這其實(shí)時一種服務(wù)器推的形式。

它其實(shí)并不是服務(wù)器主動發(fā)消息到客戶端,而是客戶端自己不斷偷偷請求服務(wù)器,只是用戶無感知而已。

用這種方式的場景也有很多,最常見的就是掃碼登錄。

比如某信公眾號平臺,登錄頁面二維碼出現(xiàn)之后,前端網(wǎng)頁根本不知道用戶掃沒掃,于是不斷去向后端服務(wù)器詢問,看有沒有人掃過這個碼。而且是以大概 1 到 2 秒的間隔去不斷發(fā)出請求,這樣可以保證用戶在掃碼后能在 1 到 2s 內(nèi)得到及時的反饋,不至于等太久。

使用 HTTP 定時輪詢

但這樣,會有兩個比較明顯的問題

當(dāng)你打開 F12 頁面時,你會發(fā)現(xiàn)滿屏的 HTTP 請求。雖然很小,但這其實(shí)也消耗帶寬,同時也會增加下游服務(wù)器的負(fù)擔(dān)。

最壞情況下,用戶在掃碼后,需要等個 1~2s,正好才觸發(fā)下一次 http 請求,然后才跳轉(zhuǎn)頁面,用戶會感到明顯的卡頓。

使用起來的體驗(yàn)就是,二維碼出現(xiàn)后,手機(jī)掃一掃,然后在手機(jī)上點(diǎn)個確認(rèn),這時候卡頓等個 1~2s,頁面才跳轉(zhuǎn)。

不斷輪詢查看是否有掃碼

那么問題又來了,有沒有更好的解決方案?

有,而且實(shí)現(xiàn)起來成本還非常低。

長輪詢

我們知道,HTTP 請求發(fā)出后,一般會給服務(wù)器留一定的時間做響應(yīng),比如 3s,規(guī)定時間內(nèi)沒返回,就認(rèn)為是超時。

如果我們的 HTTP 請求將超時設(shè)置的很大,比如 30s,在這 30s 內(nèi)只要服務(wù)器收到了掃碼請求,就立馬返回給客戶端網(wǎng)頁。如果超時,那就立馬發(fā)起下一次請求。

這樣就減少了 HTTP 請求的個數(shù),并且由于大部分情況下,用戶都會在某個 30s 的區(qū)間內(nèi)做掃碼操作,所以響應(yīng)也是及時的。

長輪詢

比如,某度云網(wǎng)盤就是這么干的。所以你會發(fā)現(xiàn)一掃碼,手機(jī)上點(diǎn)個確認(rèn),電腦端網(wǎng)頁就秒跳轉(zhuǎn),體驗(yàn)很好。

長輪詢的方式來替代

真一舉兩得。

像這種發(fā)起一個請求,在較長時間內(nèi)等待服務(wù)器響應(yīng)的機(jī)制,就是所謂的長輪詢機(jī)制。我們常用的消息隊列 RocketMQ 中,消費(fèi)者去取數(shù)據(jù)時,也用到了這種方式。

RocketMQ 的消費(fèi)者通過長輪詢獲取數(shù)據(jù)

像這種,在用戶不感知的情況下,服務(wù)器將數(shù)據(jù)推送給瀏覽器的技術(shù),就是所謂的服務(wù)器推送技術(shù),它還有個毫不沾邊的英文名,comet 技術(shù),大家聽過就好。

上面提到的兩種解決方案,本質(zhì)上,其實(shí)還是客戶端主動去取數(shù)據(jù)。

對于像掃碼登錄這樣的簡單場景還能用用。

但如果是網(wǎng)頁游戲呢,游戲一般會有大量的數(shù)據(jù)需要從服務(wù)器主動推送到客戶端。

這就得說下 websocket 了。

websocket 是什么

我們知道 TCP 連接的兩端,同一時間里雙方都可以主動向?qū)Ψ桨l(fā)送數(shù)據(jù)。這就是所謂的全雙工

而現(xiàn)在使用最廣泛的 HTTP1.1,也是基于 TCP 協(xié)議的,同一時間里,客戶端和服務(wù)器只能有一方主動發(fā)數(shù)據(jù),這就是所謂的半雙工。

也就是說,好好的全雙工 TCP,被 HTTP 用成了半雙工。

為什么?

這是由于 HTTP 協(xié)議設(shè)計之初,考慮的是看看網(wǎng)頁文本的場景,能做到客戶端發(fā)起請求再由服務(wù)器響應(yīng),就夠了,根本就沒考慮網(wǎng)頁游戲這種,客戶端和服務(wù)器之間都要互相主動發(fā)大量數(shù)據(jù)的場景。

所以為了更好的支持這樣的場景,我們需要另外一個基于 TCP 的新協(xié)議

于是新的應(yīng)用層協(xié)議 websocket 就被設(shè)計出來了。

大家別被這個名字給帶偏了。雖然名字帶了個 socket,但其實(shí) socket 和 websocket 之間,就跟雷峰和雷峰塔一樣,二者接近毫無關(guān)系。

websocket 在四層網(wǎng)絡(luò)協(xié)議中的位置

怎么建立 websocket 連接

我們平時刷網(wǎng)頁,一般都是在瀏覽器上刷的,一會刷刷圖文,這時候用的是 HTTP 協(xié)議,一會打開網(wǎng)頁游戲,這時候就得切換成我們新介紹的 websocket 協(xié)議。

為了兼容這些使用場景。瀏覽器在 TCP 三次握手建立連接之后,都統(tǒng)一使用 HTTP 協(xié)議先進(jìn)行一次通信。

如果此時是普通的 HTTP 請求,那后續(xù)雙方就還是老樣子繼續(xù)用普通 HTTP 協(xié)議進(jìn)行交互,這點(diǎn)沒啥疑問。

如果這時候是想建立 websocket 連接,就會在 HTTP 請求里帶上一些特殊的 header 頭

Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n

這些 header 頭的意思是,瀏覽器想升級協(xié)議(Connection: Upgrade),并且想升級成 websocket 協(xié)議(Upgrade: websocket)。

同時帶上一段隨機(jī)生成的 base64 碼(Sec-WebSocket-Key),發(fā)給服務(wù)器。

如果服務(wù)器正好支持升級成 websocket 協(xié)議。就會走 websocket 握手流程,同時根據(jù)客戶端生成的 base64 碼,用某個公開的算法變成另一段字符串,放在 HTTP 響應(yīng)的 Sec-WebSocket-Accept 頭里,同時帶上 101 狀態(tài)碼,發(fā)回給瀏覽器。

HTTP/1.1 101 Switching Protocols\r\n
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\n
Upgrade: websocket\r\n
Connection: Upgrade\r\n

http 狀態(tài)碼 = 200(正常響應(yīng))的情況,大家見得多了。101 確實(shí)不常見,它其實(shí)是指協(xié)議切換。

base64 轉(zhuǎn)為新的字符串

之后,瀏覽器也用同樣的公開算法將 base64 碼轉(zhuǎn)成另一段字符串,如果這段字符串跟服務(wù)器傳回來的字符串一致,那驗(yàn)證通過。

對比客戶端和服務(wù)端生成的字符串

就這樣經(jīng)歷了一來一回兩次 HTTP 握手,websocket 就建立完成了,后續(xù)雙方就可以使用 webscoket 的數(shù)據(jù)格式進(jìn)行通信了。

建立 websocket 連接.drawio

websocket 抓包

我們可以用 wireshark 抓個包,實(shí)際看下數(shù)據(jù)包的情況。

客戶端請求升級為 websocket

上面這張圖,注意畫了紅框的第 2445 行報文,是 websocket 的第一次握手,意思是發(fā)起了一次帶有特殊 Header 的 HTTP 請求。

服務(wù)器同意升級為 websocket 協(xié)議

上面這個圖里畫了紅框的 4714 行報文,就是服務(wù)器在得到第一次握手后,響應(yīng)的第二次握手,可以看到這也是個 HTTP 類型的報文,返回的狀態(tài)碼是 101。同時可以看到返回的報文 header 中也帶有各種 websocket 相關(guān)的信息,比如 Sec-WebSocket-Accept。

兩次 HTTP 請求之后正式使用 websocket 通信

上面這張圖就是全貌了,從截圖上的注釋可以看出,websocket 和 HTTP 一樣都是基于 TCP 的協(xié)議。經(jīng)歷了三次 TCP 握手之后,利用 HTTP 協(xié)議升級為 websocket 協(xié)議。

你在網(wǎng)上可能會看到一種說法:"websocket 是基于 HTTP 的新協(xié)議",其實(shí)這并不對,因?yàn)?websocket 只有在建立連接時才用到了 HTTP,升級完成之后就跟 HTTP 沒有任何關(guān)系了。

這就好像你喜歡的女生通過你要到了你大學(xué)室友的微信,然后他們自己就聊起來了。你能說這個女生是通過你去跟你室友溝通的嗎?不能。你跟 HTTP 一樣,都只是個工具人。

這就有點(diǎn) " 借殼生蛋 " 的那意思。

HTTP 和 websocket 的關(guān)系

websocket 的消息格式

上面提到在完成協(xié)議升級之后,兩端就會用 webscoket 的數(shù)據(jù)格式進(jìn)行通信。

數(shù)據(jù)包在 websocket 中被叫做。

我們來看下它的數(shù)據(jù)格式長什么樣子。

websocket 報文格式

這里面字段很多,但我們只需要關(guān)注下面這幾個。

opcode 字段:這個是用來標(biāo)志這是個什么類型的數(shù)據(jù)幀。比如。

等于 1 時是指 text 類型(string)的數(shù)據(jù)包

等于 2 是二進(jìn)制數(shù)據(jù)類型([] byte)的數(shù)據(jù)包

等于 8 是關(guān)閉連接的信號

payload 字段:存放的是我們真正想要傳輸?shù)臄?shù)據(jù)的長度,單位是字節(jié)。比如你要發(fā)送的數(shù)據(jù)是字符串 "111",那它的長度就是 3。

另外,可以看到,我們存放 payload 長度的字段有好幾個,我們既可以用最前面的 7bit, 也可以用后面的 7+16bit 或 7+64bit。

那么問題就來了。

我們知道,在數(shù)據(jù)層面,大家都是 01 二進(jìn)制流。我怎么知道什么情況下應(yīng)該讀 7bit,什么情況下應(yīng)該讀 7+16bit 呢?

websocket 會用最開始的 7bit 做標(biāo)志位。不管接下來的數(shù)據(jù)有多大,都先讀最先的 7 個 bit,根據(jù)它的取值決定還要不要再讀個 16bit 或 64bit。

如果最開始的 7bit 的值是 0~125,那么它就表示了 payload 全部長度,只讀最開始的 7 個 bit 就完事了。

payload 長度在 0 到 125 之間

如果是 126(0x7E)。那它表示 payload 的長度范圍在 126~65535 之間,接下來還需要再讀 16bit。這 16bit 會包含 payload 的真實(shí)長度。

payload 長度在 126 到 65535 之間

如果是 127(0x7F)。那它表示 payload 的長度范圍 & gt;=65536,接下來還需要再讀 64bit。這 64bit 會包含 payload 的長度。這能放 2 的 64 次方 byte 的數(shù)據(jù),換算一下好多個 TB,肯定夠用了。

payload 長度大于等于 65536 的情況

payload data 字段:這里存放的就是真正要傳輸?shù)臄?shù)據(jù),在知道了上面的 payload 長度后,就可以根據(jù)這個值去截取對應(yīng)的數(shù)據(jù)。

大家有沒有發(fā)現(xiàn)一個小細(xì)節(jié),websocket 的數(shù)據(jù)格式也是  數(shù)據(jù)頭(內(nèi)含 payload 長度) + payload data 的形式。

之前寫的《既然有 HTTP 協(xié)議,為什么還要有 RPC》提到過,TCP 協(xié)議本身就是全雙工,但直接使用純裸 TCP 去傳輸數(shù)據(jù),會有粘包的 "問題"。為了解決這個問題,上層協(xié)議一般會用消息頭 + 消息體的格式去重新包裝要發(fā)的數(shù)據(jù)。

消息頭里一般含有消息體的長度,通過這個長度可以去截取真正的消息體。

HTTP 協(xié)議和大部分 RPC 協(xié)議,以及我們今天介紹的 websocket 協(xié)議,都是這樣設(shè)計的。

消息邊界長度標(biāo)志

websocket 的使用場景

websocket 完美繼承了 TCP 協(xié)議的全雙工能力,并且還貼心的提供了解決粘包的方案。它適用于需要服務(wù)器和客戶端(瀏覽器)頻繁交互的大部分場景。比如網(wǎng)頁 / 小程序游戲,網(wǎng)頁聊天室,以及一些類似飛書這樣的網(wǎng)頁協(xié)同辦公軟件。

回到文章開頭的問題,在使用 websocket 協(xié)議的網(wǎng)頁游戲里,怪物移動以及攻擊玩家的行為是服務(wù)器邏輯產(chǎn)生的,對玩家產(chǎn)生的傷害等數(shù)據(jù),都需要由服務(wù)器主動發(fā)送給客戶端,客戶端獲得數(shù)據(jù)后展示對應(yīng)的效果。

websocket 的使用場景

總結(jié)

  • TCP 協(xié)議本身是全雙工的,但我們最常用的 HTTP1.1,雖然是基于 TCP 的協(xié)議,但它是半雙工的,對于大部分需要服務(wù)器主動推送數(shù)據(jù)到客戶端的場景,都不太友好,因此我們需要使用支持全雙工的 websocket 協(xié)議。

  • 在 HTTP1.1 里。只要客戶端不問,服務(wù)端就不答?;谶@樣的特點(diǎn),對于登錄頁面這樣的簡單場景,可以使用定時輪詢或者長輪詢的方式實(shí)現(xiàn)服務(wù)器推送 (comet) 的效果。

  • 對于客戶端和服務(wù)端之間需要頻繁交互的復(fù)雜場景,比如網(wǎng)頁游戲,都可以考慮使用 websocket 協(xié)議。

  • websocket 和 socket 幾乎沒有任何關(guān)系,只是叫法相似。

  • 正因?yàn)楦鱾€瀏覽器都支持 HTTP 協(xié)議,所以 websocket 會先利用 HTTP 協(xié)議加上一些特殊的 header 頭進(jìn)行握手升級操作,升級成功后就跟 HTTP 沒有任何關(guān)系了,之后就用 websocket 的數(shù)據(jù)格式進(jìn)行收發(fā)數(shù)據(jù)。

本文來自微信公眾號:小白 debug (ID:xiaobaidebug),作者:小白

廣告聲明:文內(nèi)含有的對外跳轉(zhuǎn)鏈接(包括不限于超鏈接、二維碼、口令等形式),用于傳遞更多信息,節(jié)省甄選時間,結(jié)果僅供參考,IT之家所有文章均包含本聲明。

相關(guān)文章

關(guān)鍵詞:HTTP,websocket

軟媒旗下網(wǎng)站: IT之家 最會買 - 返利返現(xiàn)優(yōu)惠券 iPhone之家 Win7之家 Win10之家 Win11之家

軟媒旗下軟件: 軟媒手機(jī)APP應(yīng)用 魔方 最會買 要知