去github查看完整源碼

前言 從不認識Websocket到接手H5聊天室項目,接著就是一年多的聊天室功能更新和迭代,總的來說,入門很簡單,要深入還" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網站運營 > 小白Websocket入門,10分鐘搭建一個多人聊天室~

小白Websocket入門,10分鐘搭建一個多人聊天室~

時間:2023-07-06 12:36:01 | 來源:網站運營

時間:2023-07-06 12:36:01 來源:網站運營

小白Websocket入門,10分鐘搭建一個多人聊天室~:

Demo演示

chatroom-demo.gif
體驗一下

去github查看完整源碼

前言

從不認識Websocket到接手H5聊天室項目,接著就是一年多的聊天室功能更新和迭代,總的來說,入門很簡單,要深入還是要花一些時間去學習實踐才可以。網上已經有不少大佬發(fā)表過關于Websocket實現(xiàn)聊天室的文章,這里我自己總結了Websocket的基礎知識,實現(xiàn)了一個簡單的聊天室入門的demo,作為記錄和復習,也分享給需要入門的人。

大綱

了解Websocket

背景

在許多場景下,用戶需要得到實時的消息,比如聊天,醫(yī)療設備讀數(shù)等,舊的解決方案是基于輪詢的方式獲取最新數(shù)據(jù),但是并不會完全實時消息同步,并且大多數(shù)情況下請求都是沒必要的,反而浪費了大量的流量和服務器資源。基于這種背景下,websocket誕生了。

Websocket基本概念

WebSocket 是 HTML5 開始提供的一種在「單個 TCP 連接上」進行「全雙工通訊」「協(xié)議」。WebSocket通信協(xié)議誕生于2008年,2011年成為國際標準. WebSocket使得客戶端和服務器之間的數(shù)據(jù)交換變得更加簡單,「允許服務端主動向客戶端推送數(shù)據(jù)」。在WebSocket API中,瀏覽器和服務器「只需要完成一次握手」,兩者之間就直接可以創(chuàng)建「持久性的連接」,并進行「雙向數(shù)據(jù)傳輸」

兼容性問題(主流瀏覽器都支持)

image

Websocket特點

Websocket初始握手

每個Websocket連接都始于一個HTTP請求,改請求和其他請求類似,但是包含一個特殊的首標 —— 「Upgrade」。Upgrade表示客戶端將「把連接升級到Websocket協(xié)議」。

在握手前,Websocket遵循HTTP/1.1協(xié)議。

客戶端發(fā)送升級為Websocket的請求也稱為初始握手。客戶端發(fā)送HTTP升級請求后,直到服務端響應 101 狀態(tài)碼、Upgrade和Sec-WebSocket-Accept首標才算連接成功,否則不能連接成功。下面是拷貝的websocket握手的請求頭和相應頭:

// 客戶端發(fā)送的請求頭GET wss://www.example.cn/webSocket HTTP/1.1 // 使用的https協(xié)議, 對應的wss請求Host: www.example.cnConnection: Upgrade // 帶upgrade頭的http1.1消息必須含有connection頭,表示任何接受此消息的人都在在轉發(fā)此消息之前處理掉connection中指定的域(即不轉發(fā)upgrade域)Upgrade: websocket // 定義轉換協(xié)議的header域,如果服務器支持,客戶端希望使用已經建立好的http(tcp)連接Sec-WebSocket-Version: 13 // 客戶端支持的WebSocket協(xié)議的版本列表Origin: http://example.cn // Origin為安全使用,防止跨站攻擊,瀏覽器一般會使用這個來標識原始域。Sec-WebSocket-Key: afmbhhBRQuwCLmnWDRWHxw== // 首標 客戶端隨機生成,服務器會使用此字段組裝成另一個key值放在握手返回信息里。用于客戶端到服務器websocket的初始握手,避免夸協(xié)議攻擊。Sec-WebSocket-Protocol: chat, superchat // 首標 告訴客戶端應用程序可使用的協(xié)議Sec-WebSocket-Extensions: permessage-deflate(協(xié)商使用傳輸數(shù)據(jù)壓縮); client_max_window_bits(擦采用LZ77壓縮算法時,滑動窗口相關SIZE大小)// 首標??// 服務器發(fā)出的響應頭HTTP/1.1 101Server: nginx/1.12.2Date: Sat, 11 Aug 2018 13:21:27 GMTConnection: upgradeUpgrade: websocketSec-WebSocket-Accept: sLMyWetYOwus23qJyUD/fa1hztc= // 確認服務器是否理解websocket協(xié)議Sec-WebSocket-Protocol: chatSec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15??/** Sec-WebSocket-Accept的生成步驟:(1)將Sec-WebSocket-Key與協(xié)議中已定義的GUID進行拼接 (2)將(1)中生成的字符串進行SHA1編碼 (3)將2中生成的字符串進行Base64編碼Sec-WebSocket-Accept用來確定:(1)服務端是否理解websocket協(xié)議,如果不理解,就不會返回正確的Sec-WebSocket-Accept (2)返回值是本次請求的,而不是之前的緩存*/

websocket與HTTP的異同點

相同點

  1. 都是基于TCP的應用層協(xié)議。
  2. 都使用Request/Response模型進行連接的建立。
  3. 在連接的建立過程中對錯誤的處理方式相同,在這個階段WebSocket可能返回和HTTP相同的返回碼。

不同點

  1. HTTP協(xié)議基于Request/Response,只能做單向傳輸,是「半雙工通信」,而WebSocket是「全雙工通信」。
「半雙工通信」:單向流動, 服務器不主動推送數(shù)據(jù)給客戶端。 「全雙工通信」:服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發(fā)送信息,是真正的雙向平等對話,屬于服務器推送技術的一種。
  1. 「http是無狀態(tài)的」,所以請求得到響應以后就關閉了,無狀態(tài)的好處是服務器不需要存儲相關會話信息。缺點是每次Http請求和響應都會發(fā)送關于請求的冗余信息;而WebSocket只需要建立一次Request/Response消息對,之后都是TCP連接,避免了需要多次建立Request/Response消息對而產生的冗余頭部信息。節(jié)省了大量流量和服務器資源。
HTTP連接和Websocket對比
  1. WebSocket在建立握手連接時,數(shù)據(jù)是通過HTTP協(xié)議傳輸?shù)?,但在建立連接之后,「真正的數(shù)據(jù)傳輸階段是不需要HTTP協(xié)議參與的。而http需要需要三次握手」
Http三次握手
  1. WebSocket傳輸?shù)臄?shù)據(jù)是二進制流,是以幀為單位的,HTTP傳輸?shù)氖敲魑膫鬏?,是字符串傳輸?/li>

Websocket應用場景

Websocket相關API

強調下,Websocket通過在客戶端和服務端之間的第一次握手時將HTTP協(xié)議升級到Websocket協(xié)議,建立連接后,后面的消息是直接在websocket接口定義的方法上來回傳送。

WebSocket 實例

WebSocket協(xié)議本質上是一個基于 TCP 的協(xié)議。調用WebSocket構造函數(shù)創(chuàng)建一個WebSocket連接,返回WebSocket的對象實例。

websocket協(xié)議定義了兩種URL方案

創(chuàng)建實例

/** * URL: 連接的目標 * protocols(選填):string | string[] 一個或一組協(xié)議名稱*/?const ws = new WebSocket(URL, protocols)為了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發(fā)起一個HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,其中附加頭信息"Upgrade: WebSocket"表明這是一個申請協(xié)議升級的HTTP請求,服務器端解析這些附加的頭信息然后產生應答信息返回給客戶端,客戶端和服務器端的WebSocket連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞信息,「并且這個連接會持續(xù)存在直到客戶端或者服務器端的某一方主動的關閉連接?!?/b>

Websocket事件

Websocket是純事件驅動的,可以通過監(jiān)聽Websocket對象上的事件處理數(shù)據(jù)輸入和連接狀態(tài)改變,以下為Websocket對象的4個事件。

  • 「onopen」: 客戶端和服務器建立連接后觸發(fā)。它被稱為客戶端和服務器之間的「初始握手」。如果接收到open, 說明已經連接成功,可以進行通信了。
  • 「onmessage」: 接收到消息時觸發(fā)。服務器發(fā)送給客戶端的消息可以包括純文本消息,二進制數(shù)據(jù)(Blob消息或者ArrayBuffer消息)
  • 「onerror」: 響應意外故障時觸發(fā),在錯誤之后總是會終止連接。
  • 「onclose」:連接關閉時觸發(fā)。一旦連接關閉后,客戶端和服務端將不會再進行消息的收發(fā)。也可主動調用close()方法關閉連接。

Websocket方法

  • 「send()」 : 在連接成功后關閉前,發(fā)送消息(onopen后和onclose前才可發(fā)送消息)。
  • 「close()」 : 關閉連接

Websocket對象屬性

  • 「readyState」: 只讀屬性,表示Websocket的連接狀態(tài)。其值如下:
常量特性取值狀態(tài)
Websocket.CONNECTING0連接進行中,但還未建立成功
Websocket.OPEN1連接已經建立,可以正常發(fā)送消息
Websocket.CLOSING2連接正在進行關閉握手
Websocket.CLOSED3表示連接已經關閉或者連接不能打開。
備注:不同瀏覽器支持不同,親測微信小程序沒有這個屬性。

  • 「bufferedAmount」: 只讀屬性。已被 send() 放入正在隊列中等待傳輸,但是還沒有發(fā)出的 UTF-8 文本字節(jié)數(shù)。
當要檢查發(fā)往服務器的緩沖數(shù)據(jù)量,特別是客戶端向服務器發(fā)送大量數(shù)據(jù)。盡管調用send()連接是立即生效的,但是數(shù)據(jù)在互聯(lián)網上的傳輸卻不是這樣。瀏覽器將為你的客戶端應用程序緩存出棧數(shù)據(jù),從而使你可以隨時調用send(), 但如果你想知道數(shù)據(jù)在網絡上的傳輸速率,Websocket對象可以告訴你緩存的大小。因此可以用bufferedAmount檢查已經進入隊列,但是尚未發(fā)送到服務器的字節(jié)數(shù)。

  • 「Protocol」: 打開握手期間使用的協(xié)議。

用Websocket搭建一個簡易聊天室

以上是websocket的基礎概念,現(xiàn)在咱們就來手動實現(xiàn)一個聊天室demo。




fighting

創(chuàng)建連接

使用ws搭建一個簡單的Websocket連接。ws是一個第三方的Websocket通信模塊,是基于Node.js構建的。

  1. 首先需要安裝下ws
npm i ws
  1. 服務端邏輯:創(chuàng)建一個server.js文件,建立Websocket連接,啟動服務(這里使用的3000端口)。使用ws第三方插件,new一個Websocket實例,接著監(jiān)聽connection連接成功后,在connection內部編寫連接后的邏輯。監(jiān)聽onmessage事件獲取收到的消息,監(jiān)聽onclose事件處理連接斷開的邏輯,使用send方法向客戶端發(fā)送消息。
const Websocket = require('ws')const wss = new Websocket.Server({ port: 3000 })wss.on('connection', function (ws) { // 有客戶端連接進來 ws.on('message', function (message) { console.log('server receive message: ', message.toString()) }) ws.send('msg from server!') ws.on('close', function (message) { console.log('連接斷開', message) })})
  1. 客戶端邏輯:這里用Vue搭建了一個基礎項目,在home.vue創(chuàng)建連接。使用原生的WebSocket創(chuàng)建一個ws實例,通過onopen方法監(jiān)聽WebSocket連接是否成功,此后所有的消息收發(fā)處理都要在連接打開狀態(tài),即readyState為1時處理即可。接著在open種處理用戶登錄聊天室邏輯,使用onmessage接收服務端消息,onclose用于監(jiān)聽連接斷開并進行相應的處理,close方法則可以用戶離開聊天室時手動調用。
home.vue

this.ws = new WebSocket('ws://localhost:3000')console.log('before open', this.ws.readyState) // 0this.ws.onopen = () => { // 監(jiān)聽到連接成功 console.log('onopen', this.ws.readyState) // 1 this.roomOpen = true this.ws.send(JSON.stringify({ userId: this.userName, userName: this.nickname, roomId: item.roomId, roomName: item.name, event: 'login', // 向服務端發(fā)送一條登陸消息,并攜帶對應房間信息和用戶信息 }))}// 收到消息的回調this.ws.onmessage = (message) => { console.log('The client receives the message', message)}// 收到連接斷開通知this.ws.onclose = () => { // 監(jiān)聽websocket關閉的回調 console.log('onclose', this.ws.readyState)}// 手動斷開websocket連接close () { this.ws && this.ws.close()}
  1. 可以在調試面板中查看連接狀態(tài)和消息收發(fā)

統(tǒng)計多個聊天室和用戶在線狀態(tài)

創(chuàng)建多個聊天室的邏輯在服務端進行,這里我們根據(jù)roomId來區(qū)分多個聊天室,先創(chuàng)建一個用來存儲roomId的數(shù)組,每當創(chuàng)建一個新的聊天室就往數(shù)組中加一個roomId。如果進入的是已有房間,則在已有的房間在線人數(shù)的基礎上把人數(shù)加1。而前端則需要負責發(fā)送用戶登陸登出通知及對應用戶信息給服務端,并根據(jù)服務端發(fā)送的消息進行頁面所需信息的渲染,代碼如下:

「home.vue」

this.ws.onopen = () => { this.roomOpen = true this.ws.send(JSON.stringify({ userId: this.userName, userName: this.nickname, roomId: item.roomId, roomName: item.name, event: 'login', })) }, 25000)}this.ws.onmessage = (message) => { const data = JSON.parse(message.data) this.onlineNum = data.num if (data.event === 'login') { // 有其他用戶進入房間消息 this.msgList.push({ content: `歡迎${data.userName}進入${data.roomName}房間~`, }) } else if (data.event === 'logout') { // 有其他用戶離開房間消息 console.log('logout', data) this.msgList.push({ content: `${data.userName}離開房間`, }) } else { // 普通消息 const self = this.userId === data.userId if (self) return this.msgList.push({ name: data.userName, self: false, content: data.content, }) } }「server.js」

ws.on('message', function (message) { console.log('server receive message: ', message.toString()) const data = JSON.parse(message.toString()) if (typeof ws.roomId === 'undefined' && data.roomId) { ws.roomId = data.roomId if (typeof group[ws.roomId] === 'undefined') { group[ws.roomId] = 1 } else { group[ws.roomId]++ } } data.num = group[ws.roomId] wss.clients.forEach(client => { if (client.readyState === Websocket.OPEN && client.roomId === ws.roomId) { client.send(JSON.stringify(data)) } })}) ws.on('close', function (message) { // 監(jiān)聽到聊天室關閉后,將在線人數(shù)減1,并將退出房間的消息推送給其他客戶端,更新頁面的在線人數(shù) group[ws.roomId]-- wss.clients.forEach(function each (client) { if (client !== ws && client.readyState === Websocket.OPEN && client.roomId === ws.roomId) { client.send(JSON.stringify({ ...ws.enterInfo, event: 'logout', num: group[ws.roomId], })) } })})

心跳保活

由于在長連接的場景下,客戶端和服務端并不是一直處于通信狀態(tài),如果雙方長期沒有溝通,就不清楚對方目前是否還在連接中,因此需要發(fā)送一段很小的報文告訴對方“我還活著”,來保證連接正常。

心跳?;?/figcaption>如上圖所示,在應用層通常是由客戶端發(fā)送一個心跳包 ping 到服務端,服務端收到后響應一個 pong 表明雙方都活得好好的。

其他目的:

  • 服務端檢測到某個客戶端遲遲沒有心跳過來可以主動關閉通道,讓它下線。
  • 客戶端檢測到某個服務端遲遲沒有響應心跳也能重連獲取一個新的連接。
舉個例子,當個人計算機用戶使用TCP/IP向一個使用Telnet的主機注冊時。如果在一天結束時,他們僅僅關閉了電源而沒有注銷,那么便會留下一個半開放的連接。如果客戶端已經消失了,使得在服務器上留下一個半開放連接,而服務器又在等待來自客戶的數(shù)據(jù),則服務器將永遠等待下去。「?;罟δ芫褪窃噲D在服務器端檢測到這種半開放的連接?!?/b>

而對于服務器而言,能夠及時獲悉連接可用性也非常重要:一方面服務器需要及時清理無效連接以「減輕負載」,另一方面也是業(yè)務的需求,如游戲副本中服務器需要及時處理玩家掉線帶來的問題。

下面是心跳?;畹拇a實現(xiàn)。

「home.vue」

this.ws.onopen = () => { if (this.heartbeatTimer !== -1) { clearInterval(this.heartbeatTimer) this.heartbeatTimer = -1 } this.heartbeatTimer = setInterval(() => { if (this.heartBeatTimeoutJob !== -1) { clearTimeout(this.heartBeatTimeoutJob) this.heartBeatTimeoutJob = -1 } this.heartBeatTimeoutJob = setTimeout(() => { console.log('心跳超時') }, 10000) this.ws.send(JSON.stringify({ event: 'heartBeat', content: 'ping', })) console.log('send ping') }, 25000)}this.ws.onmessage = (message) => { console.log('onmessage', message) const data = JSON.parse(message.data) console.log('message.data: ', data) if (data.event === 'heartBeat' && data.content === 'pong') { console.log('receive server pong') if (this.heartBeatTimeoutJob !== -1) { clearTimeout(this.heartBeatTimeoutJob) this.heartBeatTimeoutJob = -1 } return }} this.ws.onclose = () => { console.log('onclose', this.ws.readyState) clearInterval(this.heartbeatTimer) this.heartbeatTimer = -1 clearTimeout(this.heartBeatTimeoutJob) this.heartBeatTimeoutJob = -1 }在home.vue中,我們在監(jiān)聽到onopen后,創(chuàng)建了一個定時器heartbeatTimer,來隔段時間向服務器發(fā)送還活著的ping消息。同時在heartbeatTimer內部創(chuàng)建一個定時器heartBeatTimeoutJob來檢查服務器是否有回復pong消息,當沒有在heartBeatTimeoutJob設定的時間內收到服務器的pong消息,就視為心跳超時,在這里監(jiān)聽到心跳超時后,可以進行斷連并重連操作。

heartBeatTimer的時長應比heartBeatTimeoutJob要長,否則不會有heartBeatTimeoutJob超時的情況,具體時長可以根據(jù)自己的需要來定。

當在onmessage回調中收到服務端的pong消息,需要將監(jiān)聽心跳超時定時器heartBeatTimeoutJob清除并重置。這里還要注意的是,在每次新建定時器前需要判斷當前環(huán)境中是否有已經有同名定時器并清除,避免定時器多次運行出現(xiàn)bug。且每次在關閉Websocket連接時,也要及時清除定時器,否則即使用戶已經離開了房間,后臺的定時器也不會停止運行,可能造成內存泄漏和其他未知問題。

「server.js」

ws.on('message', function (message) { // console.log('wss', wss) console.log('server receive message: ', message.toString()) const data = JSON.parse(message.toString()) if (data.event === 'login') { ws.enterInfo = data } if (data.event === 'heartBeat' && data.content === 'ping') { console.log('receive ping message') ws.isAlive = true ws.send(JSON.stringify({ event: 'heartBeat', content: 'pong', })) return } if (typeof ws.roomId === 'undefined' && data.roomId) { ws.roomId = data.roomId if (typeof group[ws.roomId] === 'undefined') { group[ws.roomId] = 1 } else { group[ws.roomId]++ } } console.log('groun', group) data.num = group[ws.roomId] wss.clients.forEach(client => { if (client.readyState === Websocket.OPEN && client.roomId === ws.roomId) { client.send(JSON.stringify(data)) } }) })server.js中服務端應該也要有一個定時器校驗心跳是否超時,如果超時則斷開連接,并重新計算在線人數(shù)等。

消息必達

為什么要有消息必達

消息必達是為了處理長連過程中一些重要消息因為網絡、服務器等原因,導致用戶未收到消息的兼容處理。

消息丟失的幾種情況:

  1. (1)中客戶端發(fā)送給服務端消息的過程中消息丟失。這種情況無法處理。
  2. (2)(3)中如用戶已經成功將消息成功發(fā)送給服務端,但是在服務端通知用戶本人或者其他用戶的過程中因為網絡斷開切換等原因發(fā)送失敗,導致用戶收不到消息。這種情況可通過ack手段使用戶能收到重要消息,來提高消息觸達率。
左(1) 中(2) 右(3)

ack機制處理消息必達

使用ack機制來處理消息必達,即當客戶端收到消息后,需要發(fā)送一條ack回執(zhí),告訴服務端已經收到消息了。如果服務端未接收到客戶端的ack消息,則理解為客戶端未收到消息,將會重發(fā)此消息,以保證用戶能夠接收到消息。當客戶端需要接收消息時,使用ack處理消息必達可能會有以下幾種情況:

  1. 正常情況
  • 用戶收到消息后,發(fā)送ack給服務端,服務端知道客戶端收到消息了,服務端不再推送此消息(如下方左圖)。
  • 用戶未收到消息,因此未發(fā)送ack給服務端,服務端未接收到ack,重發(fā)此條消息,用戶收到了消息,消息必達完成(如下方右圖)。
  1. 故障情況
用戶收到消息后,發(fā)送ack給服務端,在發(fā)送過程中網絡中斷等,導致服務端誤以為客戶端未收到消息,重發(fā)了消息,導致客戶端顯示了多條重復消息。(這只ack處理消息必達導致的問題,需要客戶端配合做消息去重)。

結尾

websocket完善的功能還有房間鑒權,離線消息同步,漫游消息等,有興趣的可以多了解下。

參考文檔

書籍:《HTML5 WebSocket權威指南》

HTTP、socket、 Websocket的聯(lián)系和區(qū)別:https://www.cnblogs.com/aspirant/p/11334957.html

長連接的心跳及重連設計:https://juejin.cn/post/6844903765875621896

websocket教程: https://juejin.cn/post/6844903977457287181

websocket教程:https://juejin.cn/post/6844903977457287181https://www.jmjc.tech/less/114

websocket編寫聊天室:https://www.liaoxuefeng.com/wiki/1022910821149312/1103332447876608

ws github: https://github.com/websockets/ws

websocket握手總結:https://blog.csdn.net/QQ729533020/article/details/99739827

關鍵詞:入門

74
73
25
news
  • 網站
  • 營銷
  • 設計
  • 運營
  • 優(yōu)化
  • 效率
  • 專注
  • 電商
  • 方案
  • 推廣
為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點擊下載Chrome瀏覽器
關閉