WebSocket搭建简易聊天室

辣条 发布于 2022/11/05 频道:技术 阅读:10 评论:0

前言

本文新手向, 意在熟悉 WebSocket 及其应用;全篇由两部分构成:前半部分实现该简易聊天室并将其部署至服务器,后半部分则具体聊一聊 WebSocket 并顺带谈谈 SSE

项目演示地址 https://chat.deeruby.com

双手奉上代码链接 传送门 - ajun568

双脚奉上最终效果图

chat1.png

chat3.png

chat2.png

功能实现

👺 需求描述:可发送表情及图片的群聊功能

👺 项目采用:React Hook + Redux + Nodejs + WebSocket 实现,无数据库

👺 样式参考:嗨信聊天微信网页版

如若各位看官对 Redux 不够了解,可跳转至笔者的另一篇文章 Redux在React Hook中的使用及其原理,欢迎留言指教。

如何使用WebSocket

🦥 客户端

WebSocket - 文档

🦅 以下为本文所需知识点:

  • 创建 WebSocket 连接 -> new WebSocket(url)

  • 常量 CONNECTING -> 0OPEN -> 1CLOSING -> 2CLOSED -> 3

  • WebSocket.onopen -> 连接成功,开始通讯

  • WebSocket.onmessage -> 客户端接收服务端发送的消息

  • WebSocket.onclose -> 连接关闭后的回调函数

  • WebSocket.onerror -> 连接失败后的回调函数

  • WebSocket.readyState -> 当前的连接状态

  • WebSocket.close -> 关闭当前连接

  • WebSocket.send -> 客户端向服务端发送消息

🦅 我们来创建一个 Ws 类,用于处理所有通讯事件

class Ws {
  constructor (url) {    this.ws = new WebSocket(url);
  }

  initWs() {    this.ws.onopen = () => {}    this.ws.onmessage = (event) => {}    this.ws.onclose = (event) => {}    this.ws.onerror = () => {}
  }

  close() {    this.ws.close();
  }

  send(data) {    if (this.ws.readyState === this.ws.OPEN) {      this.ws.send(data);
    }
  }
}

🦥 服务端

这里使用ws与客户端通讯:ws - 文档

🦅 服务端代码如下

import express from "express";import WebSocket, { WebSocketServer } from "ws";const wss = new WebSocketServer({ port: 8080 });const app = express();const send = (ws, data) => { // 向客户端发送消息
  ws.send(JSON.stringify(data));
}const broadcast = (data) => { // 向所有客户端广播消息
  wss.clients.forEach(client => {    if (client.readyState === WebSocket.OPEN) {
      send(client, data);
    }
  });
}

wss.on('open', () => { // 连接成功的回调函数
  console.log('connected');
});

wss.on('close', () => { // 连接关闭的回调函数
  console.log('disconnected');
});

wss.on('connection', (ws) => { 
  ws.on('message', (message) => {    // 接收客户端发的消息
  });
});

app.listen(3000, () => {  console.log('Start Service On 3000');
});

🦥 调试

❓ 如何在浏览器中看到 WebSocket 请求

🦅 我们打开控制台,选择 Network 下的 WS,即可看到通讯情况

chat7.png

登录

因未连接数据库,判断若输入的用户名不在成员列表中,则新建用户,否则报错。头像上随机了50张图片,未考虑人数超过50的情况。

login1.png

我们在 localstorage 中存入用户信息,若下次打开时该用户名未被占用,则直接登录。

登录后服务端回传用户信息及成员列表,回传后的数据用 Redux 存储。

login2.png

聊天

🦥 聊天即一个互相通讯的过程,各种情况前文均已涉及,不再赘述,这里说说我们具体要做些什么。

当有新用户进入时,推送xxx进入群聊。

聊天时,向服务端发送消息及用户信息,服务端将此消息广播;若接收到的消息与当前用户名一致,视为我方消息,以此规则显示消息列表。

服务端存储50条历史消息已备刷新页面展示。

每次接收新消息后,将显示区域滚动至底部。

talk1.png

发送表情

emoji 表情选用了 emoji-mart 插件,基础结构如下所示:

emoji1.png

点击 icon 时显示 emoji 盒子,点击其余区域关闭 emoji 盒子,点击其余区域的 自定义Hook 如下:

emoji2.png

选择表情后,将其拼接至输入框中并聚焦。

emoji3.png

Retina 屏幕下 Chrome 浏览器的 emoji 会与文字重叠,可拼接 span 标签做样式处理。

// 匹配 emoji 的正则表达式export const RetinaRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c[\ude32-\ude3a]|[\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g;
/* 样式适配 */@medianot screen and (-webkit-min-device-pixel-ratio: 2),not screen and (min--moz-device-pixel-ratio: 2),not screen and (-o-min-device-pixel-ratio: 2/1),not screen and (min-device-pixel-ratio: 2),not screen and (min-resolution: 192dpi),not screen and (min-resolution: 2dppx) {  .emoji {    margin: 0 5px 0 0;
  }
}

emoji4.png

发送图片

我们使用 <input type="file"> 进行文件上传,仅接受图片格式,基础结构如下所示:

file1.png

点击 icon 时打开上传框;然后我们简单处理,使用 FileReader 读取计算机中的文件,将选择的文件转换成 base64 格式进行上传;记得清空 input,否则同名图片不可上传。

file2.png

因图片加载有延迟,接收消息滚动至底部时,会出现只显示一部分图片的情况,我们在图片加载完后,在进行一次滚动至底部的操作。

file3.png

断线重连与心跳检测

当刷新浏览器或关闭浏览器时,应断开与服务端的连接。

heart1.png

断线重连

若浏览器与服务器断开连接,则进行重连,我们每 5s 重连一次,重连一定次数依旧不能成功,则断开连接。连接成功后将 limit 限制重置。

heart2.png

心跳检测

心跳检测是客户端与服务端约定一个规则进行通讯,如若在一定时间内收不到对方的消息,则连接断开,需进行重连。由于各浏览器机制不同,触发 onclose 时机也不同,故我们需要心跳检测来补充断线重连的逻辑。

heart3.png

因断线后需更新在线人员列表,删除不在线的成员,笔者在服务端加了个定时任务去与客户端通讯,客户端若回复则该成员在线,否则离线。该处理方式比较生硬,大家可自行优化。

浅谈 WebSocket

什么是 WebSocket ?

是一种协议,用于客户端与服务端相互通讯。协议的标识符为 ws,加密则为 wss

wss://api.chat.deeruby.com

为什么使用 WebSocket ?

在传统的 HTTP 协议中,通讯只能由客户端发起,服务端无法主动向客户端推送消息,需通过轮询方式让客户端自行获取,效率极低。

WebSocket 连接是如何创建的?

🦅 WebSocket 并不是全新协议,而是利用 HTTP 协议来建立连接,故此连接需从浏览器发起,格式如下:

GET wss://api.chat.deeruby.com/ HTTP/1.1Host: api.chat.deeruby.comConnection: UpgradeUpgrade: websocketOrigin: https://chat.deeruby.comSec-WebSocket-Version: 13Sec-WebSocket-Key: dsRxU8oSxU2Jru9hOgf4dg==
  • 请求头Upgrade: websocketConnection: Upgrade表示这个连接将要被转换为 WebSocket 连接。

  • Sec-WebSocket-Version指定了 WebSocket 的协议版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

  • Sec-WebSocket-Key与服务端Sec-WebSocket-Accept配套,用于标识连接。

🦅 随后,服务器若接受该请求,则做如下反应:

HTTP/1.1 101 Switching Protocols 
Connection: upgrade 
Upgrade: websocket 
Sec-WebSocket-Accept: aAO8QyaRJEYUX2yG+pTEwRQK04w=
  • 响应码 101 表示本次连接的 HTTP 协议将被更改,更改为 Upgrade: websocket 指定的 WebSocket 协议。

  • Sec-WebSocket-Accep 是将 Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接,通过 SHA1 计算并转换为 base64 字符串。

浅谈SSE

什么是SSE ?

SSE 全称:Server-Sent Events

SSE 使用 HTTP协议,而 HTTP 协议无法由服务器主动推送消息,但有一种变通方式,即服务端向客户端声明,接下来发送的为流信息。其为一个连续发送的数据流,而不是一个一次性的数据包,故客户端不会关闭连接,而是一直等服务器发送新的数据流。SSE 就是通过这种机制,使用流信息向浏览器推送消息。

什么场景选用SSE ?

只需要服务器给客户端发送消息的场景时,SSE可胜任。

Demo

详细用法还请诸位看官自行查看文档,这里只写一个将服务端与客户端连接起来的小🌰Demo,客户端使用 EventSource 接收, 文档如下:EventSource

sse2.png

如何调试 ?

打开控制台,Network 下的 XHREventStream 部分看数据

sse1.png

参考🔗链接

版权声明:本文出自博客随心小记的文章-WebSocket搭建简易聊天室,转载请附上原文出处链接及本声明。
来源:https://delpast.com/post/POSTTB_3fab4ed8a76843039bb9f5dd3778eb50


免责声明:
本文内容整编自互联网,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现有涉嫌抄袭的内容,请联系本站,一经查实,将立刻删除涉嫌侵权内容。

评论留言

我要留言

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。