如何用JS WebSocket實(shí)現(xiàn)簡(jiǎn)單聊天
短輪詢(xún)(Polling)
短輪詢(xún)的實(shí)現(xiàn)思路就是瀏覽器端每隔幾秒鐘向服務(wù)器端發(fā)送 HTTP 請(qǐng)求,服務(wù)端在收到請(qǐng)求后,不論是否有數(shù)據(jù)更新,都直接進(jìn)行響應(yīng)。在服務(wù)端響應(yīng)完成,就會(huì)關(guān)閉這個(gè) TCP 連接,代碼實(shí)現(xiàn)也最簡(jiǎn)單,就是利用 XHR, 通過(guò) setInterval 定時(shí)向后端發(fā)送請(qǐng)求,以獲取最新的數(shù)據(jù)。
setInterval(function() { fetch(url).then((res) => { // success code }) }, 3000);
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單。
缺點(diǎn):會(huì)造成數(shù)據(jù)在一小段時(shí)間內(nèi)不同步和大量無(wú)效的請(qǐng)求,安全性差、浪費(fèi)資源。
長(zhǎng)輪詢(xún)(Long-Polling)
客戶(hù)端發(fā)送請(qǐng)求后服務(wù)器端不會(huì)立即返回?cái)?shù)據(jù),服務(wù)器端會(huì)阻塞請(qǐng)求連接不會(huì)立即斷開(kāi),直到服務(wù)器端有數(shù)據(jù)更新或者是連接超時(shí)才返回,客戶(hù)端才再次發(fā)出請(qǐng)求新建連接、如此反復(fù)從而獲取最新數(shù)據(jù)。大致效果如下:
客戶(hù)端代碼如下:
function async() { fetch(url).then((res) => { async(); // success code }).catch(() => { // 超時(shí) async(); }) }
優(yōu)點(diǎn):比 Polling 做了優(yōu)化,有較好的時(shí)效性。
缺點(diǎn):保持連接掛起會(huì)消耗資源,服務(wù)器沒(méi)有返回有效數(shù)據(jù),程序超時(shí)。
WebSocket
前面提到的短輪詢(xún)(Polling)和長(zhǎng)輪詢(xún)(Long-Polling), 都是先由客戶(hù)端發(fā)起 Ajax 請(qǐng)求,才能進(jìn)行通信,走的是 HTTP 協(xié)議,服務(wù)器端無(wú)法主動(dòng)向客戶(hù)端推送信息。
當(dāng)出現(xiàn)類(lèi)似體育賽事、聊天室、實(shí)時(shí)位置之類(lèi)的場(chǎng)景時(shí),輪詢(xún)就顯得十分低效和浪費(fèi)資源,因?yàn)橐粩喟l(fā)送請(qǐng)求,連接服務(wù)器。WebSocket 的出現(xiàn),讓服務(wù)器端可以主動(dòng)向客戶(hù)端發(fā)送信息,使得瀏覽器具備了實(shí)時(shí)雙向通信的能力。
沒(méi)用過(guò) WebSocket 的人,可能會(huì)以為它是個(gè)什么高深的技術(shù)。其實(shí)不然,WebSocket 常用的 API 不多也很容易掌握,不過(guò)在介紹如何使用之前,讓我們先看看它的通信原理。
通信原理
當(dāng)客戶(hù)端要和服務(wù)端建立 WebSocket 連接時(shí),在客戶(hù)端和服務(wù)器的握手過(guò)程中,客戶(hù)端首先會(huì)向服務(wù)端發(fā)送一個(gè) HTTP 請(qǐng)求,包含一個(gè) Upgrade 請(qǐng)求頭來(lái)告知服務(wù)端客戶(hù)端想要建立一個(gè) WebSocket 連接。
在客戶(hù)端建立一個(gè) WebSocket 連接非常簡(jiǎn)單:
let ws = new WebSocket('ws://localhost:9000');
類(lèi)似于 HTTP 和 HTTPS,ws 相對(duì)應(yīng)的也有 wss 用以建立安全連接,本地已 ws 為例。這時(shí)的請(qǐng)求頭如下:
Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: Upgrade // 表示該連接要升級(jí)協(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 相對(duì)應(yīng) Sec-WebSocket-Version: 13 // 表示 websocket 協(xié)議的版本 Upgrade: websocket // 表示要升級(jí)到 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
此時(shí)響應(yīng)行(General)中可以看到狀態(tài)碼 status code 是 101 Switching Protocols, 表示該連接已經(jīng)從 HTTP 協(xié)議轉(zhuǎn)換為 WebSocket 通信協(xié)議。 轉(zhuǎn)換成功之后,該連接并沒(méi)有中斷,而是建立了一個(gè)全雙工通信,后續(xù)發(fā)送和接收消息都會(huì)走這個(gè)連接通道。
注意,請(qǐng)求頭中有個(gè) Sec-WebSocket-Key 字段,和相應(yīng)頭中的 Sec-WebSocket-Accept 是配套對(duì)應(yīng)的,它的作用是提供了基本的防護(hù),比如惡意的連接或者無(wú)效的連接。Sec-WebSocket-Key 是客戶(hù)端隨機(jī)生成的一個(gè) base64 編碼,服務(wù)器會(huì)使用這個(gè)編碼,并根據(jù)一個(gè)固定的算法:
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一個(gè)固定的字符串 accept = base64(sha1(key + GUID)); // key 就是 Sec-WebSocket-Key 值,accept 就是 Sec-WebSocket-Accept 值
其中 GUID 字符串是RFC6455官方定義的一個(gè)固定字符串,不得修改。
客戶(hù)端拿到服務(wù)端響應(yīng)的 Sec-WebSocket-Accept 后,會(huì)拿自己之前生成的 Sec-WebSocket-Key 用相同算法算一次,如果匹配,則握手成功。然后判斷 HTTP Response 狀態(tài)碼是否為 101(切換協(xié)議),如果是,則建立連接,大功告成。
實(shí)現(xiàn)簡(jiǎn)單單聊
下面來(lái)實(shí)現(xiàn)一個(gè)純文字消息類(lèi)型的一對(duì)一聊天(單聊)功能,廢話不多說(shuō),直接上代碼,注意看注釋。
客戶(hù)端:
function connectWebsocket() { ws = new WebSocket('ws://localhost:9000'); // 監(jiān)聽(tīng)連接成功 ws.onopen = () => { console.log('連接服務(wù)端WebSocket成功'); ws.send(JSON.stringify(msgData)); // send 方法給服務(wù)端發(fā)送消息 }; // 監(jiān)聽(tīng)服務(wù)端消息(接收消息) ws.onmessage = (msg) => { let message = JSON.parse(msg.data); console.log('收到的消息:', message) elUl.innerhtml += `<li>小秋:${message.content}</li>`; }; // 監(jiān)聽(tīng)連接失敗 ws.onerror = () => { console.log('連接失敗,正在重連...'); connectWebsocket(); }; // 監(jiān)聽(tīng)連接關(guān)閉 ws.onclose = () => { console.log('連接關(guān)閉'); }; }; connectWebsocket();
從上面可以看到 WebSocket 實(shí)例的 API 很容易理解,簡(jiǎn)單好用,通過(guò) send() 方法可以發(fā)送消息,onmessage 事件用來(lái)接收消息,然后對(duì)消息進(jìn)行處理顯示在頁(yè)面上。 當(dāng) onerror 事件(監(jiān)聽(tīng)連接失?。┯|發(fā)時(shí),最好進(jìn)行執(zhí)行重連,以保持連接不中斷。
服務(wù)端 Node: (這里使用ws庫(kù))
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)聽(tīng)客戶(hù)端發(fā)來(lái)的消息 ws.on('message', (message) => { console.log(wss.clients.size); let msgData = JSON.parse(message); if (msgData.type === 'open') { // 初始連接時(shí)標(biāo)識(shí)會(huì)話 ws.sessionId = `${msgData.fromUserId}-${msgData.toUserId}`; } else { let sessionId = `${msgData.toUserId}-${msgData.fromUserId}`; wss.clients.forEach(client => { if (client.sessionId === sessionId) { client.send(message); // 給對(duì)應(yīng)的客戶(hù)端連接發(fā)送消息 } }) } }) // 連接關(guān)閉 ws.on('close', () => { console.log('連接關(guān)閉'); }); });
同理,服務(wù)端也有對(duì)應(yīng)的發(fā)送和接收的方法。完整示例代碼見(jiàn)這里
這樣瀏覽器和服務(wù)端就可以愉快的發(fā)送消息了,效果如下:
其中綠色箭頭表示發(fā)出的消息,紅色箭頭表示收到的消息。
心跳?;?/h3>
在實(shí)際使用 WebSocket 中,長(zhǎng)時(shí)間不通消息可能會(huì)出現(xiàn)一些連接不穩(wěn)定的情況,這些未知情況導(dǎo)致的連接中斷會(huì)影響客戶(hù)端與服務(wù)端之前的通信,
為了防止這種的情況的出現(xiàn),有一種心跳?;畹姆椒ǎ嚎蛻?hù)端就像心跳一樣每隔固定的時(shí)間發(fā)送一次 ping,來(lái)告訴服務(wù)器,我還活著,而服務(wù)器也會(huì)返回 pong,來(lái)告訴客戶(hù)端,服務(wù)器還活著。ping/pong 其實(shí)是一條與業(yè)務(wù)無(wú)關(guān)的假消息,也稱(chēng)為心跳包。
可以在連接成功之后,每隔一個(gè)固定時(shí)間發(fā)送心跳包,比如 60s:
setInterval(() => { ws.send('這是一條心跳包消息'); }, 60000)
總結(jié)
通過(guò)上面的介紹,大家應(yīng)該對(duì) WebSocket 有了一定認(rèn)識(shí),其實(shí)并不神秘,這里對(duì)文章內(nèi)容簡(jiǎn)單總結(jié)一下。當(dāng)創(chuàng)建 WebSocket 實(shí)例的時(shí)候,會(huì)發(fā)一個(gè) HTTP 請(qǐng)求,請(qǐng)求報(bào)文中有個(gè)特殊的字段 Upgrade,然后這個(gè)連接會(huì)由 HTTP 協(xié)議轉(zhuǎn)換為 WebSocket 協(xié)議,這樣客戶(hù)端和服務(wù)端建立了全雙工通信,通過(guò) WebSocket 的 send 方法和 onmessage 事件就可以通過(guò)這條通信連接交換信息。
以上就是如何用JS WebSocket實(shí)現(xiàn)簡(jiǎn)單聊天的詳細(xì)內(nèi)容,更多關(guān)于WebSocket的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js getBoundingClientRect使用方法詳解
這篇文章主要介紹了js getBoundingClientRect使用方法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07javascript實(shí)現(xiàn)的textarea運(yùn)行框效果代碼 不用指定id批量指定
今天在寫(xiě)一個(gè)網(wǎng)頁(yè)的時(shí)候用到了N多嵌套在textarea標(biāo)簽里的代碼,定義雙擊運(yùn)行其內(nèi)的代碼段。但是每次創(chuàng)建一個(gè)這樣的可運(yùn)行的實(shí)例都要給textarea元素自定義一個(gè)id值和寫(xiě)入雙擊事件,好不麻煩。2009-12-12for循環(huán) + setTimeout 結(jié)合一些示例(前端面試題)
最近在學(xué)習(xí)node.js開(kāi)發(fā)資料,正好碰到了for循環(huán)+settimeout的經(jīng)典例子,下面小編給大家分享for循環(huán) + setTimeout 結(jié)合一些示例代碼,需要的朋友參考下吧2017-08-08js實(shí)現(xiàn)select跳轉(zhuǎn)菜單新窗口效果代碼分享
這篇文章主要介紹了js實(shí)現(xiàn)select跳轉(zhuǎn)菜單新窗口效果代碼分享,實(shí)現(xiàn)很簡(jiǎn)單,推薦給大家,有需要的小伙伴可以參考下2015-08-08Textarea根據(jù)內(nèi)容自適應(yīng)高度
使用JAVASCRIPT控制Textarea內(nèi)容自適應(yīng)高度,實(shí)施起來(lái)很簡(jiǎn)單,很平滑。2013-10-10JavaScript實(shí)現(xiàn)圖片懶加載的方法分析
這篇文章主要介紹了JavaScript實(shí)現(xiàn)圖片懶加載的方法,結(jié)合實(shí)例形式分析了javascript圖片懶加載的原理、操作步驟與相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-07-07