一、前言
?本文將介紹 WebSocket 的封裝,比如:心跳機制,重連和一些問題如何去處理
?
二、背景
之前,錢包相關的查詢,我們是使用的輪詢方案來做的,后來更新了一次需求,需要做一些實時數(shù)據統(tǒng)計的更新,然后順帶給錢包的余額也用長連接來做了,好,那么故事就開始了...
某天,
「老板:」 我錢怎么沒了,但是我這里查賬戶還有。
「我的內心:」 恩?這玩意難道說... 后端沒返?
和后端溝通以后,感覺是返回了的,被擠賬號了?排查了一段時間以后,最終我將問題鎖定在手機息屏的操作上。
因為我們是一個 「H5」 的項目,APP 是嵌套在 webview
中,所以不能操作原生的事件來處理,只能將方案控制在瀏覽器提供的事件來處理。
好了,接下來各位可以看我是如何處理這個問題,如果沒有搞過也是可以有不少收獲,也歡迎大神評論區(qū)交流其他方案。
三、WebSocket
3.1 什么是 WebSocket ?為什么使用他?
以下是百度百科中對 「WebSocket」 的定義:
WebSocket
是一種在單個 TCP 連接上進行 全雙工 通信的協(xié)議。WebSocket
通信協(xié)議于2011年被 IETF 定為標準 RFC 6455,并由 RFC7936 補充規(guī)范。WebSocket API
也被 W3C 定為標準。
WebSocket
使得客戶端和服務器之間的數(shù)據交換變得更加簡單,允許服務端主動向客戶端推送數(shù)據。在 WebSocket API
中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據傳輸。
「WebSocket 的關鍵特點」
- 客戶端和服務器都可以主動發(fā)送數(shù)據,而不是像
HTTP
一樣只能由客戶端發(fā)起請求。
- 消息可以實時傳遞,延遲更低,適合需要實時更新的場景。
- 使用單個
TCP
連接完成多次數(shù)據交互,無需為每次通信重新建立連接。
WebSocket
頭部信息非常小,比傳統(tǒng) HTTP 請求的頭部要輕量。
上述中,是 AI 給我們總結的 WebSocket
的特點,接下來我們要知道我們?yōu)槭裁词褂盟?code style="padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">HTTP 他能不能做,他的局限性又在哪里?
「傳統(tǒng) HTTP 的局限性:」
HTTP
是基于請求-響應模型的,客戶端必須發(fā)起請求,服務器才能返回數(shù)據。- 如果需要實時更新(如股票價格、在線聊天),通常需要使用輪詢(Polling)或長輪詢(Long Polling),這會導致:
- 高網絡流量(每次請求都包含冗長的 HTTP 頭部信息)。
- 更高的延遲(數(shù)據可能需要等待較長時間才能返回)。
其實 HTTP
是可以實現(xiàn)的,如果 HTTP
請求頻繁三次握手和四次揮手的操作會占用大量資源,HTTP/1.1
以后開啟了 「Keep-Alive (長連接)」,可以復用連接,但是實時的情況下,響應模型仍然會導致較高的延遲和資源消耗。
相比之下,WebSocket
通過一次握手建立連接以后,就可以保持雙向通信,服務器可以主動推送數(shù)據,無需客戶端輪詢。解決了 HTTP
帶來的一些痛點。
四、封裝 WebSocket
我們將實現(xiàn)以下幾個功能點:
4.1 Javascript 版本
class ReSocket {
constructor(url, options = {}) {
this.url = url; // WebSocket 服務器地址
this.options = options; // 可選參數(shù)
this.socket = null; // WebSocket 實例
this.maxReconnectTimes = options.maxReconnectTimes || 5; // 最大重連次數(shù)
this.reconnectTimes = 0; // 當前重連次數(shù)
this.reconnectInterval = options.reconnectInterval || 3000; // 重連間隔時間(毫秒)
this.isClosed = false; // 是否已關閉
this.isOpen = false; // 是否已打開
this.isConnect = false; // 是否已連接
this.isReconnecting = false; // 是否正在重連
this.isDestroyed = false; // 是否已銷毀
this.reconnectTimer = null; // 重連定時器
this.heartbeatTimer = null; // 心跳定時器
this.heartbeatInterval = options.heartbeatInterval || 30000; // 心跳間隔時間(默認30秒)
this.heartbeatData = options.heartbeatData || "ping"; // 心跳數(shù)據
this.onMessageCallback = null; // 消息接收回調
this.onOpenCallback = null; // 連接成功回調
this.onCloseCallback = null; // 連接關閉回調
}
// 創(chuàng)建WebSocket實例
createSocket() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.isOpen = true;
this.isConnect = true;
this.reconnectTimes = 0; // 重連次數(shù)歸零
this.startHeartbeat(); // 啟動心跳機制
if (this.onOpenCallback) this.onOpenCallback(); // 調用連接成功回調
};
this.socket.onmessage = event => {
if (this.onMessageCallback) this.onMessageCallback(event.data); // 調用消息接收回調
};
this.socket.onclose = () => {
this.isOpen = false;
this.isConnect = false;
this.stopHeartbeat(); // 停止心跳機制
if (this.onCloseCallback) this.onCloseCallback(); // 調用連接關閉回調
if (!this.isClosed && this.reconnectTimes < this.maxReconnectTimes) {
this.reconnect(); // 嘗試重連
}
};
this.socket.onerror = error => {
console.error("WebSocket 錯誤: ", error); // 錯誤處理
};
}
// 開始連接
connect() {
if (this.isDestroyed) return; // 如果已銷毀,則不再連接
this.createSocket(); // 創(chuàng)建WebSocket實例
}
// 重連
reconnect() {
if (this.isReconnecting || this.reconnectTimes >= this.maxReconnectTimes)
return; // 防止重復重連
this.isReconnecting = true;
this.reconnectTimes++; // 增加重連次數(shù)
this.reconnectTimer = setTimeout(() => {
console.log(`正在重連... (${this.reconnectTimes})`); // 打印重連次數(shù)
this.createSocket(); // 再次創(chuàng)建WebSocket實例
this.isReconnecting = false; // 重連狀態(tài)設置為false
}, this.reconnectInterval); // 按設定時間重連
}
// 發(fā)送消息
send(data) {
if (this.isOpen) {
this.socket.send(data); // 發(fā)送數(shù)據
} else {
console.error("WebSocket 未打開,無法發(fā)送消息。"); // 提示錯誤
}
}
// 設置消息接收回調
onMessage(callback) {
this.onMessageCallback = callback; // 綁定接收消息的回調
}
// 設置連接成功回調
onOpen(callback) {
this.onOpenCallback = callback; // 綁定連接成功的回調
}
// 設置連接關閉回調
onClose(callback) {
this.onCloseCallback = callback; // 綁定連接關閉的回調
}
// 啟動心跳機制
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isOpen) {
this.send(this.heartbeatData); // 發(fā)送心跳數(shù)據
}
}, this.heartbeatInterval); // 按設定的時間間隔發(fā)送
}
// 停止心跳機制
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer); // 清除心跳定時器
this.heartbeatTimer = null;
}
}
// 關閉連接
close() {
this.isClosed = true; // 設置為已關閉
this.isOpen = false;
this.socket.close(); // 關閉WebSocket連接
this.stopHeartbeat(); // 停止心跳機制
clearTimeout(this.reconnectTimer); // 清除重連定時器
}
// 銷毀實例
destroy() {
this.isDestroyed = true; // 設置為已銷毀
this.close(); // 關閉連接
}
}
4.2 Typescript 版本
type ReSocketOptions = {
maxReconnectTimes?: number; // 最大重連次數(shù)
reconnectInterval?: number; // 重連間隔時間(毫秒)
heartbeatInterval?: number; // 心跳間隔時間(毫秒)
heartbeatData?: string; // 心跳數(shù)據
};
class ReSocket {
private url: string;
private socket: WebSocket | null = null;
private maxReconnectTimes: number;
private reconnectTimes: number = 0;
private reconnectInterval: number;
private isClosed: boolean = false;
private isOpen: boolean = false;
private isConnect: boolean = false;
private isReconnecting: boolean = false;
private isDestroyed: boolean = false;
private reconnectTimer: NodeJS.Timeout | null = null;
private heartbeatTimer: NodeJS.Timeout | null = null;
private heartbeatInterval: number;
private heartbeatData: string;
private onMessageCallback: ((message: string) => void) | null = null;
private onOpenCallback: (() => void) | null = null;
private onCloseCallback: (() => void) | null = null;
constructor(url: string, options: ReSocketOptions = {}) {
this.url = url;
this.maxReconnectTimes = options.maxReconnectTimes || 5;
this.reconnectInterval = options.reconnectInterval || 3000;
this.heartbeatInterval = options.heartbeatInterval || 30000;
this.heartbeatData = options.heartbeatData || 'ping';
}
private createSocket(): void {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.isOpen = true;
this.isConnect = true;
this.reconnectTimes = 0;
this.startHeartbeat();
if (this.onOpenCallback) this.onOpenCallback();
};
this.socket.onmessage = (event: MessageEvent) => {
if (this.onMessageCallback) this.onMessageCallback(event.data);
};
this.socket.onclose = () => {
this.isOpen = false;
this.isConnect = false;
this.stopHeartbeat();
if (this.onCloseCallback) this.onCloseCallback();
if (!this.isClosed && this.reconnectTimes < this.maxReconnectTimes) {
this.reconnect();
}
};
this.socket.onerror = (error: Event) => {
console.error("WebSocket 錯誤: ", error);
};
}
public connect(): void {
if (this.isDestroyed) return;
this.createSocket();
}
private reconnect(): void {
if (this.isReconnecting || this.reconnectTimes >= this.maxReconnectTimes) return;
this.isReconnecting = true;
this.reconnectTimes++;
this.reconnectTimer = setTimeout(() => {
console.log(`正在重連... (${this.reconnectTimes})`);
this.createSocket();
this.isReconnecting = false;
}, this.reconnectInterval);
}
public send(data: string): void {
if (this.isOpen && this.socket) {
this.socket.send(data);
} else {
console.error("WebSocket 未打開,無法發(fā)送消息。");
}
}
public onMessage(callback: (message: string) => void): void {
this.onMessageCallback = callback;
}
public onOpen(callback: () => void): void {
this.onOpenCallback = callback;
}
public onClose(callback: () => void): void {
this.onCloseCallback = callback;
}
private startHeartbeat(): void {
this.heartbeatTimer = setInterval(() => {
if (this.isOpen && this.socket) {
this.send(this.heartbeatData);
}
}, this.heartbeatInterval);
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
public close(): void {
this.isClosed = true;
this.isOpen = false;
if (this.socket) {
this.socket.close();
}
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
}
public destroy(): void {
this.isDestroyed = true;
this.close();
}
}
export { ReSocket };
4.3 如何使用?
首先簡單寫個 ws 的服務,我的 「Node」 環(huán)境是 20.18.0
創(chuàng)建一個 Socket
的文件夾 vscode
打開執(zhí)行:
npm init -y
生成完畢 package.json
之后,我們安裝 ws
:
npm i ws
創(chuàng)建 app.js
寫一個簡單服務 :
const WebSocket = require("ws");
// 創(chuàng)建 WebSocket 服務器,監(jiān)聽端口 8080
const wss = new WebSocket.Server({ port: 8080 });
// 監(jiān)聽客戶端連接
wss.on("connection", (ws) => {
console.log("客戶端已連接");
// 監(jiān)聽客戶端發(fā)送的消息
ws.on("message", (message) => {
console.log("收到客戶端消息:", message);
// 向客戶端發(fā)送回復
ws.send(`服務器回復: ${message}`);
});
// 發(fā)送一條歡迎消息給客戶端
ws.send("歡迎連接 WebSocket 服務器");
});
// 打印服務器地址
console.log("WebSocket 服務器已啟動: ws://localhost:8080");
執(zhí)行運行命令 :
node .\app.js
這里可以先用一個 WebSocket 調試工具試試是否創(chuàng)建成功 這里我是用的是 WebSocket在線測試工具 ,效果如下圖:
看到歡迎連接的時候,說明我們這個服務已經成功啟動了,接下來就是 Javascript 中如何使用了,創(chuàng)建一個 index.html
,然后引入我們封裝好的
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ws 調試</title>
</head>
<script src="./socket.js"></script>
<script>
var ws = new ReSocket('ws://localhost:8080');
ws.connect()
ws.onMessage((res) => {
console.log('onMessage', res)
})
</script>
<body>
</body>
</html>
打開瀏覽器之后在控制臺中看日志,如圖:
在網絡中我們需要在這里看:
到這里,如果你跟著做了一遍,你已經掌握了,如果感覺現(xiàn)在沒時間,可以收藏,點贊,留個標記,畢竟收藏等于學會了??????
五、我的痛點如何處理
其實我的封裝對于很多瀏覽器都是可以跑的,如果你復制去跑不了,那就人跑,明白我的意思吧?好了,其實這個封裝,沒有一些特殊兼容,比如:
- 瀏覽器兼容性,某些瀏覽器支持不完整,可能就要降級處理了,具體某些說的是哪個瀏覽器,大家心里都知道
- 代理和網絡環(huán)境問題,某些企業(yè)網絡或代理服務器會攔截或限制
WebSocket
流量 - 網絡狀態(tài)檢測,在網絡斷開但沒有觸發(fā)
onclose
或 onerror
時,可能無法及時重新連接 - 心跳包超時檢測,如果服務器沒有正確響應心跳包,客戶端可能無法及時發(fā)現(xiàn)連接異常
- 大數(shù)據傳輸和分片處理,發(fā)送大數(shù)據包可能導致超時或失敗
- 瀏覽器生命周期事件,在瀏覽器后臺或移動設備息屏時,
WebSocket
可能被掛起或斷開??? 我的問題就是在這里
等等...
所以,需要各位根據自己使用場景,簡單的需求基本上還是可以用的,如果場景涵蓋比較多,這時候就可以優(yōu)先考慮三方庫使用
「瀏覽器生命周期事件:」 當我在移動設備息屏時,我的 WebSocket
確實不會觸發(fā)心跳,然后后端就給我掛了,那么我們?yōu)g覽器其實提供了一個 visibilitychange
給我們使用,可以這樣寫:
// 頁面可見性監(jiān)聽
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log("頁面可見,嘗試恢復 WebSocket 連接...");
if (!socket.isConnect) {
socket.connect(); // 頁面可見時嘗試恢復連接
}
} else {
console.log("頁面隱藏,關閉 WebSocket 連接...");
socket.close(); // 頁面隱藏時關閉連接以節(jié)省資源
}
});
其實,這個我感覺還不太滿足我,所以我又添加了一個定時任務來執(zhí)行檢驗,代碼如下:
// 頁面可見性監(jiān)聽
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
console.log("頁面可見,嘗試恢復 WebSocket 連接...");
if (!socket.isConnect) {
socket.connect();
}
lastActiveTime = Date.now(); // 更新最近活動時間
} else {
console.log("頁面隱藏,關閉 WebSocket 連接...");
socket.close();
}
});
// 定時任務 - 檢測 WebSocket 狀態(tài)及頁面活躍度
const startCheckInterval = () => {
checkInterval = setInterval(() => {
const now = Date.now();
// 檢測 WebSocket 是否斷開,嘗試重連
if (!socket.isConnect) {
console.log("WebSocket 未連接,嘗試重連...");
socket.connect();
}
// 檢測最近活動時間,判斷頁面是否處于非活躍狀態(tài)
if (now - lastActiveTime > 10000) { // 超過10秒未活動
console.log("檢測到頁面可能處于非活躍狀態(tài)!");
// 此處可執(zhí)行其他恢復或提醒操作
}
}, 5000); // 每5秒檢查一次
};
// 清理定時任務
const clearCheckInterval = () => {
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
};
// 初始化定時任務
startCheckInterval();
// 頁面銷毀的時候調用 clearCheckInterval 清理
結語
很久沒有更新了,狠狠的寫了3000多字,希望這篇文章還是對讀者們有幫助。最近也是經歷了裁員,和找工作一系列的事情,小小吐槽以下,就業(yè)環(huán)境不容樂觀,但是基本上看見這篇文章的讀者,都是熱愛技術的,多學點知識基本上儲備量上去了,面試還是很容易通過的。
「最后,看到此刻的你,祝你工作順利,生活愉快!」