就這樣把 Websocket 玩出了多種花樣!
一、首先我們要了解 Websocket 握手的原理
請求頭特徵
-
HTTP 必須是 1.1 GET 請求
-
HTTP Header 中 Connection 字段的值必須爲 Upgrade
-
HTTP Header 中 Upgrade 字段必須爲 websocket
-
Sec-WebSocket-Key 字段的值是採用 base64 編碼的隨機 16 字節字符串
-
Sec-WebSocket-Protocol 字段的值記錄使用的子協議,比如 binary base64
-
Origin 表示請求來源
響應頭特徵
-
狀態碼是 101 表示 Switching Protocols
-
Upgrade / Connection / Sec-WebSocket-Protocol 和請求頭一致
-
Sec-WebSocket-Accept 是通過請求頭的 Sec-WebSocket-Key 生成
二、短連接輪詢、長連接、Websocket 橫向對比
- 短連接輪詢
-
很耗費 TCP 連接
-
而且 Header 重複發送
-
且通過宏任務發起,受限於 Event Loop,無法保證及時性
-
同時無效請求會很多
- 長連接
-
HTTP keep-alive 開啓後雖然 TCP 可以複用,但是 Header 重複的問題並沒有解決
-
同時 HTTP keep-alive 還有一個有效期,有效期結束後服務端會發偵查幀探查 TCP 是否有效
題外話:
HTTP keep-alive 的作用是,告知服務端持久化當前的 TCP 連接,不要立即斷開,以便後續的 HTTP 請求複用它,也就是我們所說的「長連接」
HTTP 的 keep-alive 是爲了讓 TCP 活久一點,而 TCP 本身也有一個 keepalive(注意沒有橫槓哦)機制。這是 TCP 的一種檢測連接狀況的保活機制,keepalive 是 TCP 保活定時器:TCP 建立後,如果閒置沒用,服務器不可能白等下去,閒置一段時間 [可設置] 後,服務器就會嘗試向客戶端發送偵測包,來判斷 TCP 連接狀況,如果沒有收到對方的回答(ACK 包),就會過一會 [可設置] 再偵測一次,如果多次 [可設置] 都沒回答,就會丟棄這個 TCP 連接
(TCP keepalive 保活示意圖)
- Websocket
-
和 HTTP 一樣都是建立在 TCP 協議之上,但只需一次 HTTP 握手,就能建立持久性連接,後續就不走 HTTP 了, 而是 WebSocket 特有的數據幀
-
全雙工通信,雙向數據傳輸
-
數據格式輕量,且支持發送二進制數據,支持 ws 和加密的 wss
三、我在微信小程序中利用 WebSocket 都搗鼓了什麼?
1 驗籤鑑權及對應的容錯策略(登錄態要求、峯值訪問、服務端宕機異常)
背景與目的:
-
websocket 握手後,接口請求即可以放棄 HTTP 改走 weboskcet,但大部分業務接口都要求登錄態,因此握手成功後必須先走一次簽名鑑權,獲取登錄態
-
當出現大流量訪問的場景(如大促、熱點活動等)或服務端出 bug 而導致服務端宕機,前端會做 對應容錯,將位於內存的等待隊列中的待發送請求立即降級成 HTTP 發送出去
僞碼示意:
SocketTask.onOpen(function () {
SocketTask.sendSocketMessage({
msg_type: '驗籤',
token: 'xxx'
}, (response) => {
console.log(response.user_id, response.access_token)
// 通道可用,打個標記
global.isSocketAvaliable = true;
})
})
2 心跳保活(減少 TCP 佔用)
背景與目的:爲了減少 TCP 連接的無效佔用,客戶端定時發送一個空包到服務端,告知服務端不要銷燬這條 socket,如果服務端超過一定時間都沒收到心跳包,則將關閉並銷燬該 socket
僞碼示意:
SocketTask.onOpen(function () {
SocketTask.sendSocketMessage({
msg_type: '驗籤',
token: 'xxx'
}, (response) => {
console.log(response.user_id, response.access_token)
// 通道可用,打個標記
global.isSocketAvaliable = true;
// 驗籤成功,開始定時發送心跳包
setInterval(() => {
SocketTask.sendSocketMessage({
msg_type: '心跳'
});
});
});
})
3 模擬 RTT(用於弱網體驗優化)
背景與目的:在發送心跳包時,可得知一個心跳包的 RTT,以此模擬當前用戶網絡環境的 TCP RTT,並據此計算出平滑 RTO,用於弱網體驗優化
僞碼示意:
SocketTask.onOpen(function () {
SocketTask.sendSocketMessage({
msg_type: '驗籤',
token: 'xxx'
}, (response) => {
console.log(response.user_id, response.access_token)
// 通道可用,打個標記
global.isSocketAvaliable = true;
// 驗籤成功,開始定時發送心跳包
setInterval(() => {
// 計算 RTT
const begin = Date.now();
SocketTask.sendSocketMessage({
msg_type: '心跳'
}, () => {
const end = Date.now();
const RTT = begin - end;
const smoothedRTO = cal(RTT);
global.smoothedRTO = smoothedRTO;
});
});
});
});
4 Snappy 壓縮(橫向對比了 gzip / zip / 7z)
背景與目的:在小程序中引入第三方壓縮包(犧牲小程序包體積),減少 websocket 傳輸的字節數
僞碼示意:
import Snappy from 'snappy';
SocketTask.sendSocketMessage = function (msg) {
const encryptedMsg = Snappy.encode(msg);
wx.send(encryptedMsg);
}
5 重連(階梯式錯位重連,避免擁擠)
背景與目的:用戶的網絡環境不穩定,可能會存在主動 / 被動斷開 socket 的情況,需要進行自動重連
僞碼示意:
SocketTask.onClose(function () {
// 限定最大重連次數
if (retryCount > maxCount) {
return;
}
retryCount++;
setTimeout(() => {
SocketTask.connectSocket();
}, retryCount * 1000 + Math.random() * 1000);
});
6 埋點中間層緩存(重複的用戶信息可以不用每次都上報,支持刷新緩存)
背景與目的:爲減少網絡傳輸的包體積,通過 websocket 上報埋點日誌時,可以把部分重複字段值在第一次上報時緩存在服務端,從第二次上報開始只上報值不重複的字段,然後由服務端做日誌合併
僞碼示意:
SocketTask.sendSocketMessage({
msg_type: '埋點日誌',
logs: {
country: 'China', // 可緩存字段
city: '北京', // 可緩存字段
platform: '安卓', // 可緩存字段
click_some_btn: true // 動態變化的埋點字段
},
cacheFields: ['country', 'city', 'platform'] // 只在第一次上報時攜帶
});
7 啓用 TCP_NODELAY
TCP_NODELAY 是用來禁用 Nagle 算法的。Nagle 算法設計的目的是提高網絡帶寬利用率,其核心思路是「合併小的 TCP 包爲一個大的 TCP 包」,避免過多的小包的 TCP 頭部浪費網絡帶寬
參考資料:https://www.zhihu.com/question/42308970
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Q_QlgMQYnz7BZg3lSvb2dQ