平時我們打開網頁,比如購物網站某寶。都是點一下列表商品,跳轉一下網頁就到了商品詳情。
從HTTP協議的角度來看,就是點一下網頁上的某個按鈕,前端發一次HTTP請求,網站返回一次HTTP響應。
這種由客戶端主動請求,服務器響應的方式也滿足大部分網頁的功能場景。
但有沒有發現,這種情況下,服務器從來就不會主動給客戶端發一次消息。
就像你喜歡的女生從來不會主動找你一樣。
但如果現在,你在刷網頁的時候右下角突然彈出一個小廣告,提示你【一個人在家偷偷才能玩哦】。
求知,好學,勤奮,這些刻在你DNA里的東西都動起來了。
你點開后發現。
長相平平無奇的古某提示你'道士9條狗,全服橫著走'。
影帝某輝老師跟你說'系兄弟就來砍我'。
其實問題的痛點在于,怎么樣才能在用戶不做任何操作的情況下,網頁能收到消息并發生變更。
最常見的解決方案是,網頁的前端代碼里不斷定時發HTTP請求到服務器,服務器收到請求后給客戶端響應消息。
這其實時一種偽服務器推的形式。
它其實并不是服務器主動發消息到客戶端,而是客戶端自己不斷偷偷請求服務器,只是用戶無感知而已。
用這種方式的場景也有很多,最常見的就是掃碼登錄。
比如某信公眾號平臺,登錄頁面二維碼出現之后,前端網頁根本不知道用戶掃沒掃,于是不斷去向后端服務器詢問,看有沒有人掃過這個碼。而且是以大概1到2秒的間隔去不斷發出請求,這樣可以保證用戶在掃碼后能在1到2s內得到及時的反饋,不至于等太久。
但這樣,會有兩個比較明顯的問題
使用起來的體驗就是,二維碼出現后,手機掃一掃,然后在手機上點個確認,這時候卡頓等個1~2s,頁面才跳轉。
我們知道,HTTP請求發出后,一般會給服務器留一定的時間做響應,比如3s,規定時間內沒返回,就認為是超時。
如果我們的HTTP請求將超時設置的很大,比如30s,在這30s內只要服務器收到了掃碼請求,就立馬返回給客戶端網頁。如果超時,那就立馬發起下一次請求。
這樣就減少了HTTP請求的個數,并且由于大部分情況下,用戶都會在某個30s的區間內做掃碼操作,所以響應也是及時的。
像這種發起一個請求,在較長時間內等待服務器響應的機制,就是所謂的長訓輪機制。我們常用的消息隊列RocketMQ中,消費者去取數據時,也用到了這種方式。
像這種,在用戶不感知的情況下,服務器將數據推送給瀏覽器的技術,就是所謂的服務器推送技術,它還有個毫不沾邊的英文名,comet技術,大家聽過就好。
上面提到的兩種解決方案,本質上,其實還是客戶端主動去取數據。
對于像掃碼登錄這樣的簡單場景還能用用。
但如果是網頁游戲呢,游戲一般會有大量的數據需要從服務器主動推送到客戶端。
這就得說下websocket了。
我們知道TCP連接的兩端,同一時間里,雙方都可以主動向對方發送數據。這就是所謂的全雙工。
而現在使用最廣泛的HTTP1.1,也是基于TCP協議的,同一時間里,客戶端和服務器只能有一方主動發數據,這就是所謂的半雙工。
也就是說,好好的全雙工TCP,被HTTP用成了半雙工。
為什么?
這是由于HTTP協議設計之初,考慮的是看看網頁文本的場景,能做到客戶端發起請求再由服務器響應,就夠了,根本就沒考慮網頁游戲這種,客戶端和服務器之間都要互相主動發大量數據的場景。
所以為了更好的支持這樣的場景,我們需要另外一個基于TCP的新協議。
于是新的應用層協議websocket就被設計出來了。
大家別被這個名字給帶偏了。雖然名字帶了個socket,但其實socket和websocket之間,就跟雷峰和雷峰塔一樣,二者接近毫無關系。
我們平時刷網頁,一般都是在瀏覽器上刷的,一會刷刷圖文,這時候用的是HTTP協議,一會打開網頁游戲,這時候就得切換成我們新介紹的websocket協議。
為了兼容這些使用場景。瀏覽器在TCP三次握手建立連接之后,都統一使用HTTP協議先進行一次通信。
Connection: UpgradeUpgrade: websocketSec-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\nSec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n復制代碼
http狀態碼=200(正常響應)的情況,大家見得多了。101確實不常見,它其實是指協議切換。
之后,瀏覽器也用同樣的公開算法將base64碼轉成另一段字符串,如果這段字符串跟服務器傳回來的字符串一致,那驗證通過。
就這樣經歷了一來一回兩次HTTP握手,websocket就建立完成了,后續雙方就可以使用webscoket的數據格式進行通信了。
我們可以用wireshark抓個包,實際看下數據包的情況。
上面這張圖,注意畫了紅框的第2445行報文,是websocket的第一次握手,意思是發起了一次帶有特殊Header的HTTP請求。
上面這個圖里畫了紅框的4714行報文,就是服務器在得到第一次握手后,響應的第二次握手,可以看到這也是個HTTP類型的報文,返回的狀態碼是101。同時可以看到返回的報文header中也帶有各種websocket相關的信息,比如Sec-WebSocket-Accept。
上面這張圖就是全貌了,從截圖上的注釋可以看出,websocket和HTTP一樣都是基于TCP的協議。經歷了三次TCP握手之后,利用HTTP協議升級為websocket協議。
你在網上可能會看到一種說法:'websocket是基于HTTP的新協議',其實這并不對,因為websocket只有在建立連接時才用到了HTTP,升級完成之后就跟HTTP沒有任何關系了。
這就好像你喜歡的女生通過你要到了你大學室友的微信,然后他們自己就聊起來了。你能說這個女生是通過你去跟你室友溝通的嗎?不能。你跟HTTP一樣,都只是個工具人。
這就有點'借殼生蛋'的那意思。
上面提到在完成協議升級之后,兩端就會用webscoket的數據格式進行通信。
數據包在websocket中被叫做幀。
我們來看下它的數據格式長什么樣子。
這里面字段很多,但我們只需要關注下面這幾個。
opcode字段:這個是用來標志這是個什么類型的數據幀。比如。
payload字段:存放的是我們真正想要傳輸的數據的長度,單位是字節。比如你要發送的數據是字符串'111',那它的長度就是3。
另外,可以看到,我們存放payload長度的字段有好幾個,我們既可以用最前面的7bit, 也可以用后面的7+16bit或7+64bit。
那么問題就來了。
我們知道,在數據層面,大家都是01二進制流。我怎么知道什么情況下應該讀7bit,什么情況下應該讀7+16bit呢?
websocket會用最開始的7bit做標志位。不管接下來的數據有多大,都先讀最先的7個bit,根據它的取值決定還要不要再讀個16bit或64bit。
payload data字段:這里存放的就是真正要傳輸的數據,在知道了上面的payload長度后,就可以根據這個值去截取對應的數據。
大家有沒有發現一個小細節,websocket的數據格式也是 數據頭(內含payload長度) + payload data 的形式。
之前寫的《既然有HTTP協議,為什么還要有RPC》提到過,TCP協議本身就是全雙工,但直接使用純裸TCP去傳輸數據,會有粘包的'問題'。為了解決這個問題,上層協議一般會用消息頭+消息體的格式去重新包裝要發的數據。
而消息頭里一般含有消息體的長度,通過這個長度可以去截取真正的消息體。
HTTP協議和大部分RPC協議,以及我們今天介紹的websocket協議,都是這樣設計的。
websocket完美繼承了TCP協議的全雙工能力,并且還貼心的提供了解決粘包的方案。它適用于需要服務器和客戶端(瀏覽器)頻繁交互的大部分場景。比如網頁/小程序游戲,網頁聊天室,以及一些類似飛書這樣的網頁協同辦公軟件。
回到文章開頭的問題,在使用websocket協議的網頁游戲里,怪物移動以及攻擊玩家的行為是服務器邏輯產生的,對玩家產生的傷害等數據,都需要由服務器主動發送給客戶端,客戶端獲得數據后展示對應的效果。
最近原創更文的閱讀量穩步下跌,思前想后,夜里輾轉反側。
我有個不成熟的請求。
離開廣東好長時間了,好久沒人叫我靚仔了。
大家可以在評論區里,叫我一靚仔嗎?
我這么善良質樸的愿望,能被滿足嗎?
如果實在叫不出口的話,能幫我點下關注和右下角的點贊+收藏嗎?