img

WebSocket搭建简易聊天室

Deeruby 1年前 ⋅ 649 阅读

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五随笔系列WebSocket搭建简易聊天室.

前言

本文新手向, 意在熟悉 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;
/* 样式适配 */
@media
not 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.1
Host: api.chat.deeruby.com
Connection: Upgrade
Upgrade: websocket
Origin: https://chat.deeruby.com
Sec-WebSocket-Version: 13
Sec-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:5分钟从入门到精通

[廖雪峰] WebSocket

[阮一峰] Server-Sent Events 教程


全部评论: 0

    我有话说: