一.WebSocket 基本概念
1.WebSocket是什么?
WebSocket 是基于 TCP 的一種新的應(yīng)用層網(wǎng)絡(luò)協(xié)議。它提供了一個(gè)全雙工的通道,允許服務(wù)器和客戶端之間實(shí)時(shí)雙向通信。因此,在 WebSocket 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸,客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡(jiǎn)單。WebSocket
2.與 HTTP 協(xié)議的區(qū)別
與 HTTP 協(xié)議相比,WebSocket 具有以下優(yōu)點(diǎn):
- 更高的實(shí)時(shí)性能:WebSocket 允許服務(wù)器和客戶端之間實(shí)時(shí)雙向通信,從而提高了實(shí)時(shí)通信場(chǎng)景中的性能。
- 更少的網(wǎng)絡(luò)開銷:HTTP 請(qǐng)求和響應(yīng)之間需要額外的數(shù)據(jù)傳輸,而 WebSocket 通過(guò)在同一個(gè)連接上雙向通信,減少了網(wǎng)絡(luò)開銷。
- 更靈活的通信方式:HTTP 請(qǐng)求和響應(yīng)通常是一一對(duì)應(yīng)的,而 WebSocket 允許服務(wù)器和客戶端之間以多種方式進(jìn)行通信,例如消息 Push、事件推送等。
- 更簡(jiǎn)潔的 API:WebSocket 提供了簡(jiǎn)潔的 API,使得客戶端開發(fā)人員可以更輕松地進(jìn)行實(shí)時(shí)通信。
當(dāng)然肯定有缺點(diǎn)的:
- 不支持無(wú)連接: WebSocket 是一種持久化的協(xié)議,這意味著連接不會(huì)在一次請(qǐng)求之后立即斷開。這是有利的,因?yàn)樗私⑦B接的開銷,但是也可能導(dǎo)致一些資源泄漏的問(wèn)題。
- 不支持廣泛: WebSocket 是 HTML5 中的一種標(biāo)準(zhǔn)協(xié)議,雖然現(xiàn)代瀏覽器都支持,但是一些舊的瀏覽器可能不支持 WebSocket。
- 需要特殊的服務(wù)器支持: WebSocket 需要服務(wù)端支持,只有特定的服務(wù)器才能夠?qū)崿F(xiàn) WebSocket 協(xié)議。這可能會(huì)增加系統(tǒng)的復(fù)雜性和部署的難度。
- 數(shù)據(jù)流不兼容: WebSocket 的數(shù)據(jù)流格式與 HTTP 不同,這意味著在不同的網(wǎng)絡(luò)環(huán)境下,WebSocket 的表現(xiàn)可能會(huì)有所不同。
3.WebSocket工作原理
1. 握手階段
WebSocket在建立連接時(shí)需要進(jìn)行握手階段。握手階段包括以下幾個(gè)步驟:
- 客戶端向服務(wù)端發(fā)送請(qǐng)求,請(qǐng)求建立WebSocket連接。請(qǐng)求中包含一個(gè)Sec-WebSocket-Key參數(shù),用于生成WebSocket的隨機(jī)密鑰。
- 服務(wù)端接收到請(qǐng)求后,生成一個(gè)隨機(jī)密鑰,并使用隨機(jī)密鑰生成一個(gè)新的Sec-WebSocket-Accept參數(shù)。
- 客戶端接收到服務(wù)端發(fā)送的新的Sec-WebSocket-Accept參數(shù)后,使用原來(lái)的隨機(jī)密鑰和新的Sec-WebSocket-Accept參數(shù)共同生成一個(gè)新的Sec-WebSocket-Key參數(shù),用于加密數(shù)據(jù)傳輸。
- 客戶端將新的Sec-WebSocket-Key參數(shù)發(fā)送給服務(wù)端,服務(wù)端接收到后,使用該參數(shù)加密數(shù)據(jù)傳輸。
2. 數(shù)據(jù)傳輸階段
建立連接后,客戶端和服務(wù)端就可以通過(guò)WebSocket進(jìn)行實(shí)時(shí)雙向通信。數(shù)據(jù)傳輸階段包括以下幾個(gè)步驟:
- 客戶端向服務(wù)端發(fā)送數(shù)據(jù),服務(wù)端收到數(shù)據(jù)后將其轉(zhuǎn)發(fā)給其他客戶端。
- 服務(wù)端向客戶端發(fā)送數(shù)據(jù),客戶端收到數(shù)據(jù)后進(jìn)行處理。
雙方如何進(jìn)行相互傳輸數(shù)據(jù)的 具體的數(shù)據(jù)格式是怎么樣的呢?WebSocket 的每條消息可能會(huì)被切分成多個(gè)數(shù)據(jù)幀(最小單位)。發(fā)送端會(huì)將消息切割成多個(gè)幀發(fā)送給接收端,接收端接收消息幀,并將關(guān)聯(lián)的幀重新組裝成完整的消息。
發(fā)送方 -> 接收方:ping。
接收方 -> 發(fā)送方:pong。
ping 、pong 的操作,對(duì)應(yīng)的是 WebSocket 的兩個(gè)控制幀
3. 關(guān)閉階段
當(dāng)不再需要WebSocket連接時(shí),需要進(jìn)行關(guān)閉階段。關(guān)閉階段包括以下幾個(gè)步驟:
- 客戶端向服務(wù)端發(fā)送關(guān)閉請(qǐng)求,請(qǐng)求中包含一個(gè)WebSocket的隨機(jī)密鑰。
- 服務(wù)端接收到關(guān)閉請(qǐng)求后,向客戶端發(fā)送關(guān)閉響應(yīng),關(guān)閉響應(yīng)中包含服務(wù)端生成的隨機(jī)密鑰。
- 客戶端收到關(guān)閉響應(yīng)后,關(guān)閉WebSocket連接。
總的來(lái)說(shuō),WebSocket通過(guò)握手階段、數(shù)據(jù)傳輸階段和關(guān)閉階段實(shí)現(xiàn)了服務(wù)器和客戶端之間的實(shí)時(shí)雙向通信。
二.WebSocket 數(shù)據(jù)幀結(jié)構(gòu)和控制幀結(jié)構(gòu)。
1. 數(shù)據(jù)幀結(jié)構(gòu)
WebSocket 數(shù)據(jù)幀主要包括兩個(gè)部分:幀頭和有效載荷。以下是 WebSocket 數(shù)據(jù)幀結(jié)構(gòu)的簡(jiǎn)要介紹:
- 幀頭:幀頭包括四個(gè)部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,fin 表示數(shù)據(jù)幀的結(jié)束標(biāo)志,rsv1、rsv2、rsv3 表示保留字段,opcode 表示數(shù)據(jù)幀的類型,masked 表示是否進(jìn)行掩碼處理,payload_length 表示有效載荷的長(zhǎng)度。
- 有效載荷:有效載荷是數(shù)據(jù)幀中實(shí)際的數(shù)據(jù)部分,它由客戶端和服務(wù)端進(jìn)行數(shù)據(jù)傳輸。
2. 控制幀結(jié)構(gòu)
除了數(shù)據(jù)幀之外,WebSocket 協(xié)議還包括一些控制幀,主要包括 Ping、Pong 和 Close 幀。以下是 WebSocket 控制幀結(jié)構(gòu)的簡(jiǎn)要介紹:
- Ping 幀:Ping 幀用于測(cè)試客戶端和服務(wù)端之間的連接狀態(tài),客戶端向服務(wù)端發(fā)送 Ping 幀,服務(wù)端收到后需要向客戶端發(fā)送 Pong 幀進(jìn)行響應(yīng)。
- Pong 幀:Pong 幀用于響應(yīng)客戶端的 Ping 幀,它用于測(cè)試客戶端和服務(wù)端之間的連接狀態(tài)。
- Close 幀:Close 幀用于關(guān)閉客戶端和服務(wù)端之間的連接,它包括四個(gè)部分:fin、rsv1、rsv2、rsv3、opcode、masked 和 payload_length。其中,opcode 的值為 8,表示 Close 幀。
三. JavaScript 中 WebSocket 對(duì)象的屬性和方法,以及如何創(chuàng)建和連接 WebSocket。
WebSocket 對(duì)象的屬性和方法:
WebSocket
對(duì)象:WebSocket 對(duì)象表示一個(gè)新的 WebSocket 連接。WebSocket.onopen
事件處理程序:當(dāng) WebSocket 連接打開時(shí)觸發(fā)。WebSocket.onmessage
事件處理程序:當(dāng)接收到來(lái)自 WebSocket 的消息時(shí)觸發(fā)。WebSocket.onerror
事件處理程序:當(dāng) WebSocket 發(fā)生錯(cuò)誤時(shí)觸發(fā)。WebSocket.onclose
事件處理程序:當(dāng) WebSocket 連接關(guān)閉時(shí)觸發(fā)。WebSocket.send
方法:向 WebSocket 發(fā)送數(shù)據(jù)。WebSocket.close
方法:關(guān)閉 WebSocket 連接。
創(chuàng)建和連接 WebSocket:
- 創(chuàng)建 WebSocket 對(duì)象:
var socket = new WebSocket('ws://example.com');
其中,ws://example.com
是 WebSocket 的 URL,表示要連接的服務(wù)器。
使用 WebSocket.onopen
事件處理程序檢查 WebSocket 是否成功連接。
socket.onopen = function() {
console.log('WebSocket connected');
};
使用 WebSocket.onmessage
事件處理程序接收來(lái)自 WebSocket 的消息。
socket.onmessage = function(event) {
console.log('WebSocket message:', event.data);
};
使用 WebSocket.send
方法向 WebSocket 發(fā)送消息。
socket.send('Hello, WebSocket!');
當(dāng)需要關(guān)閉 WebSocket 時(shí),使用 WebSocket.close
方法。
socket.close();
注意:在 WebSocket 連接成功打開和關(guān)閉時(shí),會(huì)分別觸發(fā) WebSocket.onopen
和 WebSocket.onclose
事件。在接收到來(lái)自 WebSocket 的消息時(shí),會(huì)觸發(fā) WebSocket.onmessage
事件。當(dāng) WebSocket 發(fā)生錯(cuò)誤時(shí),會(huì)觸發(fā) WebSocket.onerror
事件。
四.webSocket簡(jiǎn)單示例
以下是一個(gè)簡(jiǎn)單的 WebSocket 編程示例,通過(guò) WebSocket 向服務(wù)器發(fā)送數(shù)據(jù),并接收服務(wù)器返回的數(shù)據(jù):
- 首先,創(chuàng)建一個(gè) HTML 文件,添加一個(gè)按鈕和一個(gè)用于顯示消息的文本框:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket 示例</title>
</head>
<body>
<button id="sendBtn">發(fā)送消息</button>
<textarea id="messageBox" readonly></textarea>
<script src="main.js"></script>
</body>
</html>
2. 接下來(lái),創(chuàng)建一個(gè) JavaScript 文件(例如 main.js),并在其中編寫以下代碼:// 獲取按鈕和文本框元素
const sendBtn = document.getElementById('sendBtn');
const messageBox = document.getElementById('messageBox');
// 創(chuàng)建 WebSocket 對(duì)象
const socket = new WebSocket('ws://echo.websocket.org'); // 使用一個(gè) WebSocket 服務(wù)器進(jìn)行測(cè)試
// 設(shè)置 WebSocket 連接打開時(shí)的回調(diào)函數(shù)
socket.onopen = function() {
console.log('WebSocket 連接已打開');
};
// 設(shè)置 WebSocket 接收到消息時(shí)的回調(diào)函數(shù)
socket.onmessage = function(event) {
console.log('WebSocket 接收到消息:', event.data);
messageBox.value += event.data + '\n';
};
// 設(shè)置 WebSocket 發(fā)生錯(cuò)誤時(shí)的回調(diào)函數(shù)
socket.onerror = function() {
console.log('WebSocket 發(fā)生錯(cuò)誤');
};
// 設(shè)置 WebSocket 連接關(guān)閉時(shí)的回調(diào)函數(shù)
socket.onclose = function() {
console.log('WebSocket 連接已關(guān)閉');
};
// 點(diǎn)擊按鈕時(shí)發(fā)送消息
sendBtn.onclick = function() {
const message = 'Hello, WebSocket!';
socket.send(message);
messageBox.value += '發(fā)送消息: ' + message + '\n';
};
五.webSocket應(yīng)用場(chǎng)景
- 實(shí)時(shí)通信:WebSocket 非常適合實(shí)時(shí)通信場(chǎng)景,例如聊天室、在線游戲、實(shí)時(shí)數(shù)據(jù)傳輸?shù)?。通過(guò) WebSocket,客戶端和服務(wù)器之間可以實(shí)時(shí)通信,無(wú)需依賴輪詢,從而提高通信效率和減少網(wǎng)絡(luò)延遲。
- 監(jiān)控?cái)?shù)據(jù)傳輸:WebSocket 可以在監(jiān)控系統(tǒng)中實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)傳輸,例如通過(guò) WebSocket,客戶端可以實(shí)時(shí)接收和處理監(jiān)控?cái)?shù)據(jù),而無(wú)需等待輪詢數(shù)據(jù)。
- 自動(dòng)化控制:WebSocket 可以在自動(dòng)化系統(tǒng)中實(shí)現(xiàn)遠(yuǎn)程控制,例如通過(guò) WebSocket,客戶端可以遠(yuǎn)程控制設(shè)備或系統(tǒng),而無(wú)需直接操作。
- 數(shù)據(jù)分析:WebSocket 可以在數(shù)據(jù)分析場(chǎng)景中實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)傳輸和處理,例如通過(guò) WebSocket,客戶端可以實(shí)時(shí)接收和處理數(shù)據(jù),而無(wú)需等待數(shù)據(jù)存儲(chǔ)和分析。
- 人工智能:WebSocket 可以在人工智能場(chǎng)景中實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)傳輸和處理,例如通過(guò) WebSocket,客戶端可以實(shí)時(shí)接收和處理數(shù)據(jù),而無(wú)需等待數(shù)據(jù)處理和分析。
六.WebSocket 錯(cuò)誤處理
WebSocket 的錯(cuò)誤處理
WebSocket is not supported
:當(dāng)瀏覽器不支持 WebSocket 時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在瀏覽器兼容性列表中檢查是否支持 WebSocket。WebSocket connection closed
:當(dāng) WebSocket 連接被關(guān)閉時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onclose
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket error
:當(dāng) WebSocket 發(fā)生錯(cuò)誤時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onerror
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket timeout
:當(dāng) WebSocket 連接超時(shí)時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.ontimeout
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket handshake error
:當(dāng) WebSocket 握手失敗時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onerror
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket closed by server
:當(dāng) WebSocket 連接被服務(wù)器關(guān)閉時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onclose
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket closed by protocol
:當(dāng) WebSocket 連接被協(xié)議錯(cuò)誤關(guān)閉時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onclose
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket closed by network
:當(dāng) WebSocket 連接被網(wǎng)絡(luò)錯(cuò)誤關(guān)閉時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onclose
事件處理程序中進(jìn)行錯(cuò)誤處理。WebSocket closed by server
:當(dāng) WebSocket 連接被服務(wù)器錯(cuò)誤關(guān)閉時(shí),會(huì)出現(xiàn)此錯(cuò)誤。解決方法是在 WebSocket.onclose
事件處理程序中進(jìn)行錯(cuò)誤處理。
通過(guò)為 WebSocket
對(duì)象的 onclose
、onerror
和 ontimeout
事件添加處理程序,可以及時(shí)捕獲和處理 WebSocket 錯(cuò)誤,從而確保程序的穩(wěn)定性和可靠性。
七.利用單例模式創(chuàng)建完整的wesocket連接
class webSocketClass {
constructor(thatVue) {
this.lockReconnect = false;
this.localUrl = process.env.NODE_ENV === 'production' ? 你的websocket生產(chǎn)地址' : '測(cè)試地址';
this.globalCallback = null;
this.userClose = false;
this.createWebSocket();
this.webSocketState = false
this.thatVue = thatVue
}
createWebSocket() {
let that = this;
// console.log('開始創(chuàng)建websocket新的實(shí)例', new Date().toLocaleString())
if( typeof(WebSocket) != "function" ) {
alert("您的瀏覽器不支持Websocket通信協(xié)議,請(qǐng)更換瀏覽器為Chrome或者Firefox再次使用!")
}
try {
that.ws = new WebSocket(that.localUrl);
that.initEventHandle();
that.startHeartBeat()
} catch (e) {
that.reconnect();
}
}
//初始化
initEventHandle() {
let that = this;
// //連接成功建立后響應(yīng)
that.ws.onopen = function() {
console.log("連接成功");
};
//連接關(guān)閉后響應(yīng)
that.ws.onclose = function() {
// console.log('websocket連接斷開', new Date().toLocaleString())
if (!that.userClose) {
that.reconnect(); //重連
}
};
that.ws.onerror = function() {
// console.log('websocket連接發(fā)生錯(cuò)誤', new Date().toLocaleString())
if (!that.userClose) {
that.reconnect(); //重連
}
};
that.ws.onmessage = function(event) {
that.getWebSocketMsg(that.globalCallback);
// console.log('socket server return '+ event.data);
};
}
startHeartBeat () {
// console.log('心跳開始建立', new Date().toLocaleString())
setTimeout(() => {
let params = {
request: 'ping',
}
this.webSocketSendMsg(JSON.stringify(params))
this.waitingServer()
}, 30000)
}
//延時(shí)等待服務(wù)端響應(yīng),通過(guò)webSocketState判斷是否連線成功
waitingServer () {
this.webSocketState = false//在線狀態(tài)
setTimeout(() => {
if(this.webSocketState) {
this.startHeartBeat()
return
}
// console.log('心跳無(wú)響應(yīng),已斷線', new Date().toLocaleString())
try {
this.closeSocket()
} catch(e) {
console.log('連接已關(guān)閉,無(wú)需關(guān)閉', new Date().toLocaleString())
}
this.reconnect()
//重連操作
}, 5000)
}
reconnect() {
let that = this;
if (that.lockReconnect) return;
that.lockReconnect = true; //沒(méi)連接上會(huì)一直重連,設(shè)置延遲避免請(qǐng)求過(guò)多
setTimeout(function() {
that.createWebSocket();
that.thatVue.openSuccess(that) //重連之后做一些事情
that.thatVue.getSocketMsg(that)
that.lockReconnect = false;
}, 15000);
}
webSocketSendMsg(msg) {
this.ws.send(msg);
}
getWebSocketMsg(callback) {
this.ws.onmessage = ev => {
callback && callback(ev);
};
}
onopenSuccess(callback) {
this.ws.onopen = () => {
// console.log("連接成功", new Date().toLocaleString())
callback && callback()
}
}
closeSocket() {
let that = this;
if (that.ws) {
that.userClose = true;
that.ws.close();
}
}
}
export default webSocketClass;