爲什麼有 HTTP 協議,還要有 websocket 協議

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

從 HTTP 協議的角度來看,就是點一下網頁上的某個按鈕,前端發一次 HTTP 請求,網站返回一次 HTTP 響應

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

但有沒有發現,這種情況下,服務器從來就不會主動給客戶端發一次消息。

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

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

求知,好學,勤奮,這些刻在你 DNA 裏的東西都動起來了。

你點開後發現。

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

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

來都來了,你就選了個角色進到了遊戲界面裏。

創建角色頁面

這時候,上來就是一個小怪,從遠處走來,然後瘋狂拿木棒子抽你。

你全程沒點任何一次鼠標。服務器就自動將怪物的移動數據和攻擊數據源源不斷髮給你了。

這…. 太暖心了。

感動之餘,問題就來了,

像這種看起來服務器主動發消息給客戶端的場景,是怎麼做到的?

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

使用 HTTP 不斷輪詢

其實問題的痛點在於,怎麼樣才能在用戶不做任何操作的情況下,網頁能收到消息併發生變更。

最常見的解決方案是,網頁的前端代碼裏不斷定時發 HTTP 請求到服務器,服務器收到請求後給客戶端響應消息。

這其實時一種服務器推的形式。

它其實並不是服務器主動發消息到客戶端,而是客戶端自己不斷偷偷請求服務器,只是用戶無感知而已。

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

比如某信公衆號平臺,登錄頁面二維碼出現之後,前端網頁根本不知道用戶掃沒掃,於是不斷去向後端服務器詢問,看有沒有人掃過這個碼。而且是以大概 1 到 2 秒的間隔去不斷髮出請求,這樣可以保證用戶在掃碼後能在 1 到 2s 內得到及時的反饋,不至於等太久

使用 HTTP 定時輪詢

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

使用起來的體驗就是,二維碼出現後,手機掃一掃,然後在手機上點個確認,這時候卡頓等個 1~2s,頁面才跳轉。

不斷輪詢查看是否有掃碼

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

有,而且實現起來成本還非常低。

長輪詢

我們知道,HTTP 請求發出後,一般會給服務器留一定的時間做響應,比如 3s,規定時間內沒返回,就認爲是超時。

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

這樣就減少了 HTTP 請求的個數,並且由於大部分情況下,用戶都會在某個 30s 的區間內做掃碼操作,所以響應也是及時的。

長輪詢

比如,某度雲網盤就是這麼幹的。所以你會發現一掃碼,手機上點個確認,電腦端網頁就秒跳轉,體驗很好。

長輪詢的方式來替代

真一舉兩得。

像這種發起一個請求,在較長時間內等待服務器響應的機制,就是所謂的長訓輪機制。我們常用的消息隊列 RocketMQ 中,消費者去取數據時,也用到了這種方式。

RocketMQ 的消費者通過長輪詢獲取數據

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

上面提到的兩種解決方案,本質上,其實還是客戶端主動去取數據。

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

但如果是網頁遊戲呢,遊戲一般會有大量的數據需要從服務器主動推送到客戶端。

這就得說下 websocket 了。

websocket 是什麼

我們知道 TCP 連接的兩端,同一時間裏雙方都可以主動向對方發送數據。這就是所謂的全雙工

而現在使用最廣泛的HTTP1.1,也是基於 TCP 協議的,同一時間裏,客戶端和服務器只能有一方主動發數據,這就是所謂的半雙工

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

爲什麼?

這是由於 HTTP 協議設計之初,考慮的是看看網頁文本的場景,能做到客戶端發起請求再由服務器響應,就夠了,根本就沒考慮網頁遊戲這種,客戶端和服務器之間都要互相主動發大量數據的場景。

所以爲了更好的支持這樣的場景,我們需要另外一個基於 TCP 的新協議

於是新的應用層協議 websocket 就被設計出來了。

大家別被這個名字給帶偏了。雖然名字帶了個 socket,但其實 socket 和 websocket 之間,就跟雷峯和雷峯塔一樣,二者接近毫無關係

websocket 在四層網絡協議中的位置

怎麼建立 websocket 連接

我們平時刷網頁,一般都是在瀏覽器上刷的,一會刷刷圖文,這時候用的是 HTTP 協議,一會打開網頁遊戲,這時候就得切換成我們新介紹的 websocket 協議

爲了兼容這些使用場景。瀏覽器在 TCP 三次握手建立連接之後,都統一使用 HTTP 協議先進行一次通信。

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

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

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

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

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

http 狀態碼 = 200(正常響應)的情況,大家見得多了。101 確實不常見,它其實是指協議切換

base64 轉爲新的字符串

之後,瀏覽器也用同樣的公開算法base64碼轉成另一段字符串,如果這段字符串跟服務器傳回來的字符串一致,那驗證通過。

對比客戶端和服務端生成的字符串

就這樣經歷了一來一回兩次 HTTP 握手,websocket 就建立完成了,後續雙方就可以使用 webscoket 的數據格式進行通信了。

建立 websocket 連接. drawio

websocket 抓包

我們可以用 wireshark 抓個包,實際看下數據包的情況。

客戶端請求升級爲 websocket

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

服務器同意升級爲 websocket 協議

上面這個圖裏畫了紅框的4714行報文,就是服務器在得到第一次握手後,響應的第二次握手,可以看到這也是個 HTTP 類型的報文,返回的狀態碼是 101。同時可以看到返回的報文 header 中也帶有各種websocket相關的信息,比如Sec-WebSocket-Accept

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

上面這張圖就是全貌了,從截圖上的註釋可以看出,websocket 和 HTTP 一樣都是基於 TCP 的協議。經歷了三次 TCP 握手之後,利用 HTTP 協議升級爲 websocket 協議。

你在網上可能會看到一種說法:"websocket 是基於 HTTP 的新協議",其實這並不對,因爲 websocket 只有在建立連接時纔用到了 HTTP,升級完成之後就跟 HTTP 沒有任何關係了

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

這就有點 " 借殼生蛋 " 的那意思。

HTTP 和 websocket 的關係

websocket 的消息格式

上面提到在完成協議升級之後,兩端就會用 webscoket 的數據格式進行通信。

數據包在 websocket 中被叫做

我們來看下它的數據格式長什麼樣子。

websocket 報文格式

這裏面字段很多,但我們只需要關注下面這幾個。

opcode 字段:這個是用來標誌這是個什麼類型的數據幀。比如。

payload 字段:存放的是我們真正想要傳輸的數據的長度,單位是字節。比如你要發送的數據是字符串"111",那它的長度就是3

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

那麼問題就來了。

我們知道,在數據層面,大家都是 01 二進制流。我怎麼知道什麼情況下應該讀 7bit,什麼情況下應該讀 7+16bit 呢?

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

payload 長度在 0 到 125 之間

payload 長度在 126 到 65535 之間

payload 長度大於等於 65536 的情況

payload data 字段:這裏存放的就是真正要傳輸的數據,在知道了上面的 payload 長度後,就可以根據這個值去截取對應的數據。

大家有沒有發現一個小細節,websocket 的數據格式也是  數據頭(內含 payload 長度) + payload data 的形式。

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

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

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

消息邊界長度標誌

websocket 的使用場景

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

回到文章開頭的問題,在使用 websocket 協議的網頁遊戲裏,怪物移動以及攻擊玩家的行爲是服務器邏輯產生的,對玩家產生的傷害等數據,都需要由服務器主動發送給客戶端,客戶端獲得數據後展示對應的效果。

websocket 的使用場景

總結

最後

最近原創更文的閱讀量穩步下跌,思前想後,夜裏輾轉反側。

我有個不成熟的請求。

離開廣東好長時間了,好久沒人叫我靚仔了。

大家可以在評論區裏,叫我一靚仔嗎?

我這麼善良質樸的願望,能被滿足嗎?

如果實在叫不出口的話,能幫我點下關注和右下角的點贊 + 在看嗎?

別說了,一起在知識的海洋裏嗆水吧
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/jJNdXMNmXcE8wSE0gbtTAQ