如何用JS WebSocket實現(xiàn)簡單聊天
短輪詢(Polling)
短輪詢的實現(xiàn)思路就是瀏覽器端每隔幾秒鐘向服務(wù)器端發(fā)送 HTTP 請求,服務(wù)端在收到請求后,不論是否有數(shù)據(jù)更新,都直接進行響應(yīng)。在服務(wù)端響應(yīng)完成,就會關(guān)閉這個 TCP 連接,代碼實現(xiàn)也最簡單,就是利用 XHR, 通過 setInterval 定時向后端發(fā)送請求,以獲取最新的數(shù)據(jù)。
setInterval(function() {
fetch(url).then((res) => {
// success code
})
}, 3000);
優(yōu)點:實現(xiàn)簡單。
缺點:會造成數(shù)據(jù)在一小段時間內(nèi)不同步和大量無效的請求,安全性差、浪費資源。
長輪詢(Long-Polling)
客戶端發(fā)送請求后服務(wù)器端不會立即返回數(shù)據(jù),服務(wù)器端會阻塞請求連接不會立即斷開,直到服務(wù)器端有數(shù)據(jù)更新或者是連接超時才返回,客戶端才再次發(fā)出請求新建連接、如此反復(fù)從而獲取最新數(shù)據(jù)。大致效果如下:

客戶端代碼如下:
function async() {
fetch(url).then((res) => {
async();
// success code
}).catch(() => {
// 超時
async();
})
}
優(yōu)點:比 Polling 做了優(yōu)化,有較好的時效性。
缺點:保持連接掛起會消耗資源,服務(wù)器沒有返回有效數(shù)據(jù),程序超時。
WebSocket
前面提到的短輪詢(Polling)和長輪詢(Long-Polling), 都是先由客戶端發(fā)起 Ajax 請求,才能進行通信,走的是 HTTP 協(xié)議,服務(wù)器端無法主動向客戶端推送信息。
當(dāng)出現(xiàn)類似體育賽事、聊天室、實時位置之類的場景時,輪詢就顯得十分低效和浪費資源,因為要不斷發(fā)送請求,連接服務(wù)器。WebSocket 的出現(xiàn),讓服務(wù)器端可以主動向客戶端發(fā)送信息,使得瀏覽器具備了實時雙向通信的能力。
沒用過 WebSocket 的人,可能會以為它是個什么高深的技術(shù)。其實不然,WebSocket 常用的 API 不多也很容易掌握,不過在介紹如何使用之前,讓我們先看看它的通信原理。
通信原理
當(dāng)客戶端要和服務(wù)端建立 WebSocket 連接時,在客戶端和服務(wù)器的握手過程中,客戶端首先會向服務(wù)端發(fā)送一個 HTTP 請求,包含一個 Upgrade 請求頭來告知服務(wù)端客戶端想要建立一個 WebSocket 連接。
在客戶端建立一個 WebSocket 連接非常簡單:
let ws = new WebSocket('ws://localhost:9000');
類似于 HTTP 和 HTTPS,ws 相對應(yīng)的也有 wss 用以建立安全連接,本地已 ws 為例。這時的請求頭如下:
Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: Upgrade // 表示該連接要升級協(xié)議 Cookie: _hjMinimizedPolls=358479; ts_uid=7852621249; CNZZDATA1259303436=1218855313-1548914234-%7C1564625892; csrfToken=DPb4RhmGQfPCZnYzUCCOOade; jsESSIONID=67376239124B4355F75F1FC87C059F8D; _hjid=3f7157b6-1aa0-4d5c-ab9a-45eab1e6941e; acw_tc=76b20ff415689655672128006e178b964c640d5a7952f7cb3c18ddf0064264 Host: localhost:9000 Origin: http://localhost:9000 Pragma: no-cache Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: 5fTJ1LTuh3RKjsJxydyifQ== // 與響應(yīng)頭 Sec-WebSocket-Accept 相對應(yīng) Sec-WebSocket-Version: 13 // 表示 websocket 協(xié)議的版本 Upgrade: websocket // 表示要升級到 websocket 協(xié)議 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (Khtml, like Gecko) Chrome/76.0.3809.132 Safari/537.36
響應(yīng)頭如下:
Connection: Upgrade Sec-WebSocket-Accept: ZUip34t+bCjhkvxxwhmdEOyx9hE= Upgrade: websocket

此時響應(yīng)行(General)中可以看到狀態(tài)碼 status code 是 101 Switching Protocols, 表示該連接已經(jīng)從 HTTP 協(xié)議轉(zhuǎn)換為 WebSocket 通信協(xié)議。 轉(zhuǎn)換成功之后,該連接并沒有中斷,而是建立了一個全雙工通信,后續(xù)發(fā)送和接收消息都會走這個連接通道。
注意,請求頭中有個 Sec-WebSocket-Key 字段,和相應(yīng)頭中的 Sec-WebSocket-Accept 是配套對應(yīng)的,它的作用是提供了基本的防護,比如惡意的連接或者無效的連接。Sec-WebSocket-Key 是客戶端隨機生成的一個 base64 編碼,服務(wù)器會使用這個編碼,并根據(jù)一個固定的算法:
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一個固定的字符串 accept = base64(sha1(key + GUID)); // key 就是 Sec-WebSocket-Key 值,accept 就是 Sec-WebSocket-Accept 值
其中 GUID 字符串是RFC6455官方定義的一個固定字符串,不得修改。
客戶端拿到服務(wù)端響應(yīng)的 Sec-WebSocket-Accept 后,會拿自己之前生成的 Sec-WebSocket-Key 用相同算法算一次,如果匹配,則握手成功。然后判斷 HTTP Response 狀態(tài)碼是否為 101(切換協(xié)議),如果是,則建立連接,大功告成。
實現(xiàn)簡單單聊
下面來實現(xiàn)一個純文字消息類型的一對一聊天(單聊)功能,廢話不多說,直接上代碼,注意看注釋。
客戶端:
function connectWebsocket() {
ws = new WebSocket('ws://localhost:9000');
// 監(jiān)聽連接成功
ws.onopen = () => {
console.log('連接服務(wù)端WebSocket成功');
ws.send(JSON.stringify(msgData)); // send 方法給服務(wù)端發(fā)送消息
};
// 監(jiān)聽服務(wù)端消息(接收消息)
ws.onmessage = (msg) => {
let message = JSON.parse(msg.data);
console.log('收到的消息:', message)
elUl.innerhtml += `<li>小秋:${message.content}</li>`;
};
// 監(jiān)聽連接失敗
ws.onerror = () => {
console.log('連接失敗,正在重連...');
connectWebsocket();
};
// 監(jiān)聽連接關(guān)閉
ws.onclose = () => {
console.log('連接關(guān)閉');
};
};
connectWebsocket();
從上面可以看到 WebSocket 實例的 API 很容易理解,簡單好用,通過 send() 方法可以發(fā)送消息,onmessage 事件用來接收消息,然后對消息進行處理顯示在頁面上。 當(dāng) onerror 事件(監(jiān)聽連接失敗)觸發(fā)時,最好進行執(zhí)行重連,以保持連接不中斷。
服務(wù)端 Node: (這里使用ws庫)
const path = require('path');
const express = require('express');
const app = express();
const server = require('http').Server(app);
const WebSocket = require('ws');
const wss = new WebSocket.Server({ server: server });
wss.on('connection', (ws) => {
// 監(jiān)聽客戶端發(fā)來的消息
ws.on('message', (message) => {
console.log(wss.clients.size);
let msgData = JSON.parse(message);
if (msgData.type === 'open') {
// 初始連接時標識會話
ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`;
} else {
let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`;
wss.clients.forEach(client => {
if (client.sessionId === sessionId) {
client.send(message); // 給對應(yīng)的客戶端連接發(fā)送消息
}
})
}
})
// 連接關(guān)閉
ws.on('close', () => {
console.log('連接關(guān)閉');
});
});
同理,服務(wù)端也有對應(yīng)的發(fā)送和接收的方法。完整示例代碼見這里
這樣瀏覽器和服務(wù)端就可以愉快的發(fā)送消息了,效果如下:

其中綠色箭頭表示發(fā)出的消息,紅色箭頭表示收到的消息。
心跳?;?/h3>
在實際使用 WebSocket 中,長時間不通消息可能會出現(xiàn)一些連接不穩(wěn)定的情況,這些未知情況導(dǎo)致的連接中斷會影響客戶端與服務(wù)端之前的通信,
為了防止這種的情況的出現(xiàn),有一種心跳?;畹姆椒ǎ嚎蛻舳司拖裥奶粯用扛艄潭ǖ臅r間發(fā)送一次 ping,來告訴服務(wù)器,我還活著,而服務(wù)器也會返回 pong,來告訴客戶端,服務(wù)器還活著。ping/pong 其實是一條與業(yè)務(wù)無關(guān)的假消息,也稱為心跳包。
可以在連接成功之后,每隔一個固定時間發(fā)送心跳包,比如 60s:
setInterval(() => {
ws.send('這是一條心跳包消息');
}, 60000)
總結(jié)

通過上面的介紹,大家應(yīng)該對 WebSocket 有了一定認識,其實并不神秘,這里對文章內(nèi)容簡單總結(jié)一下。當(dāng)創(chuàng)建 WebSocket 實例的時候,會發(fā)一個 HTTP 請求,請求報文中有個特殊的字段 Upgrade,然后這個連接會由 HTTP 協(xié)議轉(zhuǎn)換為 WebSocket 協(xié)議,這樣客戶端和服務(wù)端建立了全雙工通信,通過 WebSocket 的 send 方法和 onmessage 事件就可以通過這條通信連接交換信息。
以上就是如何用JS WebSocket實現(xiàn)簡單聊天的詳細內(nèi)容,更多關(guān)于WebSocket的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談ES6中箭頭函數(shù)與普通函數(shù)的區(qū)別
箭頭函數(shù)是ES6中一種新的函數(shù)的表達式,本文就來介紹一下ES6中箭頭函數(shù)與普通函數(shù)的區(qū)別,非常具有實用價值,需要的朋友可以參考下2023-05-05
UniApp與WebView雙向通信及數(shù)據(jù)傳輸超詳細講解
這篇文章主要介紹了UniApp與WebView雙向通信及數(shù)據(jù)傳輸?shù)南嚓P(guān)資料,詳細講解了UniApp與WebView的通信原理、方法對比、數(shù)據(jù)傳輸實戰(zhàn)、調(diào)試技巧、性能優(yōu)化策略及技術(shù)風(fēng)險控制,通過合理選型和優(yōu)化,需要的朋友可以參考下2025-04-04
javascript實現(xiàn)倒計時跳轉(zhuǎn)頁面
本文給大家介紹了如何使用javascript實現(xiàn)倒計時跳轉(zhuǎn)到其他頁面的方法以及實現(xiàn)原理,非常的簡單實用,有需要的小伙伴可以參考下。2016-01-01
php實例分享之實現(xiàn)顯示網(wǎng)站運行時間
這篇文章主要介紹了php實現(xiàn)顯示網(wǎng)站運行時間,需要的朋友可以參考下2014-05-05
JavaScript對象數(shù)組排序函數(shù)及六個用法
本文給大家分享一個用于數(shù)組或者對象的排序的函數(shù)。該函數(shù)可以以任意深度的數(shù)組或者對象的值作為排序基數(shù)對數(shù)組或的元素進行排序2015-12-12

