📕
余烬的小册
数据结构与算法GitHub
  • 总述
  • 经验记录
    • 经验总结
      • web component
      • 前端性能优化总结与分析
      • 我的长列表优化方案
      • 双向通讯解决方案
      • 🔧基于istanbul实现代码测试覆盖率工具
      • 表单系统(低代码表单)
      • 跨端小程序
      • 设计一个即时聊天功能
      • 跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd
    • 踩坑记录
      • HTML踩坑记录
      • Flutter踩坑记录
      • CSS踩坑记录
  • 源码解析
    • Vue源码解析
      • Vue2源码解析系列-响应式原理
      • Vue2源码解析系列-模板编译
      • Vue2源码解析系列-渲染系统(待更新)
        • Patch
      • Vue2源码解析系列-调度系统(todo)
      • Vue2组件更新流程(todo)
      • 如何学习Vue源码
      • Vue3源码解析系列-响应系统
      • Vue3源码解析系列-渲染系统
      • Vue3源码解析系列-组件化和渲染优化(todo)
      • Vue router源码解析(todo)
    • React源码解析(todo)
    • 微前端
      • qiankun源码解析(todo)
    • Vite源码解析
      • Vite Client源码
      • Vite Server源码(todo)
  • 前端技术
    • javaScript
      • ES6
        • 变量声明
        • 模块化
        • 箭头函数
        • 你不知道的for...of
        • 新的数据结构Set和Map
        • JavaScript异步编程终极解决方案
        • ES6 Class 3a0c0a225a534984aabe9a943c5df975
      • JavaScript Error
      • JavaScript浅拷贝和深拷贝
      • JavaScript闭包
      • JavaScript最佳实践
      • JavaScript设计模式
      • async函数的polyfill
    • 深入理解JavaScript系列
      • JavaScript中的继承
      • JavaScript原始类型和引用类型
      • JavaScript浅拷贝和深拷贝
      • JavaScript手写系列
      • JavaScript之this
      • 词法环境和环境记录
      • JavaScript内存泄漏
      • 执行上下文
      • 从ECMAScript规范中学习this
    • TypeScript
      • TypeScript基础教程
      • Typescript高级操作
      • TypeScript工具类型
      • Typescript手写实现工具类型
      • Typescript总结(思维导图)
    • 浏览器原理
      • 页面渲染原理
      • 浏览器存储
      • JavaScript事件循环
      • 事件循环
      • 跨域
      • DOM事件流
      • 从输入url到页面渲染
      • 判断节点之间的关系及根据节点关系查找节点
      • history API
    • 跨端技术
      • Flutter
        • Flutter布局组件
    • 前端工程化
      • Babel插件开发指南
      • 循环依赖
      • pm2
    • React
      • React 状态管理
      • React组件通讯
      • Redux入门
      • Flux
      • React Hook(todo)
      • Effect
  • 服务器端
    • 计算机网络
      • 应用层
      • 运输层
      • 物理层
      • 数据链路层
      • HTTP缓存
      • HTTPS
      • 网络层
    • NodeJs
      • Node.js
      • nodejs最佳实践
      • 《深入浅出Nodejs》小结
      • mongoose填充(populate)
      • node事件循环
      • Node子进程
      • nestjs从零开始
      • nodejs流
      • Nodejs调试
      • Koa源码解析
    • 服务器
      • 操作系统
      • Linux
      • nginx常用指令
      • nginx常用配置
    • 数据库
      • Mysql常见语法
      • MongoDB Indexes索引
  • 前端安全与性能优化
    • 前端安全
      • 跨站脚本攻击(XSS)
      • 跨站点请求伪造(CSRF)
      • 点击劫持
      • 中间人攻击
      • 越权攻击与JWT
    • 前端性能优化
      • 前端监控系统
      • 前端性能优化总结与分析 7348bba0918645b1899006dc842a64c1
      • 衡量性能的核心指标 0dc15ef127cf4f4a9f1137c377420292
      • 图片懒加载
  • 杂项
    • 其他
      • Git
      • web component框架
      • 实现滚动框的懒加载
      • Stencil指南
    • CSS
      • 定位和层叠上下文
      • BFC
      • 盒模型
      • css选择器
      • css变量
由 GitBook 提供支持
在本页
  • 目录
  • websocket
  • 实现一个websocket server
  • stomp
  • 常用的Websocket API
  • 事件
  • 方法
  • 属性
  • 踩过的坑
  • 异常断线处理
  • 如何确保消息成功发送给了对方
  • 参考
在GitHub上编辑
  1. 经验记录
  2. 经验总结

设计一个即时聊天功能

目录

websocket

要实现一个即时聊天概念,首先需要实现一个全双工的通道,让通信双方能够及时的收到对方所发送的消息。 在过去,这通常是通过http轮询实现的,即设置一个定时器,每隔一段时间就发送一个请求获取最新数据,在这一发一收的过程中就完成了双向通讯。但是这种方法最大的弊端在于及时性,消息可能会无法及时的发送给对方,当然这可以通过缩短定时器间隔时间,但是这样做又会导致性能问题。

为了解决这个问题,HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。Websocket是一种全双工协议,使用 ws 或 wss 的统一资源标志符(URI),其中 wss 表示使用了 TLS 的 Websocket。websocket和http一样位于应用层,并且websocket的建立依赖于http,具体流程是:

  1. 客户端发送Get请求,并在请求头中设置Upgrade: websocket;Connection: Upgrade;来告诉服务器将转换成websocket协议来通讯。

  2. 服务器返回转状态码101的响应,并转换成websocket协议

  3. 双方使用ws协议来通信

实现一个websocket server

在nodejs中实现websocket,最简单的方法是使用相关的websocket库,常用的库有socket.io和ws,socket.io相对来说比较重量级一点,ws相对轻量一些,当然最重要的是socket.io实现的websocket,需要客户端也使用对应的socket-client包,这就显得太笨重了,而ws则可以在客户端中使用原生Websocket API。

首先我们安装ws

npm i ws

简单示例

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });

  ws.send('something');
});

用户权限验证

ws连接的建立依靠http,因此我们可以在http upgrade阶段进行身份鉴定,如果用户权限不够则不转换成ws协议,返回401。

这里要注意,ws的noServer要设置为true,并且host、port和noServer三个参数只能设置一个。“noServer”模式的作用是将 WebSocket 服务器与 HTTP/S 服务器完全分离。例如,这使得在多个 WebSocket 服务器之间共享单个 HTTP/S 服务器成为可能。

import { createServer } from "http";
import { WebSocketServer } from "ws";

const server = createServer();
const wss = new WebSocketServer({ noServer: true });

wss.on("connection", function connection(ws, request, client) {
  ws.send("Hello Client");
  ws.on("message", function message(data) {
    console.log(`Received message ${data} from user ${client}`);
  });
});

server.on("upgrade", function upgrade(request, socket, head) {
  /**
   * 在此进行身份验证
   * 如果验证不通过则不建立ws连接
   */
  const user = request.user; // 获取用户信息
  if (!user) {
    socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
    socket.destroy();
    return;
  }

  // 验证通过,建立ws连接
  wss.handleUpgrade(request, socket, head, function (ws) {
    wss.emit("connection", ws, request, user);
  });
});

server.listen(8080);

wss

wss协议相当于https协议,其实就是使用了 TLS 的 Websocket。使用wss也比较简单,只需要在ws基础上提供证书密钥即可。

const server = createServer({
  cert: readFileSync('/path/to/cert.pem'),
  key: readFileSync('/path/to/key.pem')
});

广播

ws可以将消息广播到所有已经建立ws连接的客户端。

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
	// 将收到的消息广播
    wss.clients.forEach((client)=>{
			// 避免将消息发送给原发送方,当然你也可以选择发送给他
			if (cliend !== ws) {
        client.send(data);
      }
    })
  });
});

异常断线处理——心跳机制

在网络中,由于各种因素的影响,ws连接可能会断开,并且这种断开可能是客户端和服务器无感的,这就导致了在双方不知情的情况下仍然进行通信,这不仅浪费资源,还导致通讯双发无法接收对方的消息。

解决异常断线的方式是使用心跳机制,所谓的心跳机制是每隔一段时间就发送一个空消息(ping)来确认ws连接是否正常,对方收到ping后返回一个空数据的响应(pong),如果能正常收到pong,就说明ws连接正常,否则就证明ws连接异常,需要重新连接。

具体的做法是首先在每条ws上加上isAlive标记,该标记的作用是表明该条ws连接是否存活。然后每次发送ping前将isAlive设置为false,收到pong时将isAlive重新设置为true,假如第二次发送ping时isAlive是false,说明上个周期没有正常接收到pong,说明该条ws连接已经失效,则进行断线处理。

import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 8080 });

wss.on("connection", function connection(ws) {
  ws.on("message", function message(data) {
    console.log(data.toString());
  });

  ws.isAlive = true; // 是否存活

  ws.send("Hello Client");

  ws.on("pong", () => {
    // 收到pong,就表示是存活的
    console.log("收到pong");
    ws.isAlive = true;
  });
});

// 心跳机制
const interval = setInterval(() => {
  wss.clients.forEach((client) => {
    if (client.isAlive === false) {
      // 上个周期没有收到pong,说明连接已经失效
      client.terminate();
    }
    // 先假设所有的client都是未存活,如果收到pong,就表示是存活的
    client.isAlive = false;
    client.ping(() => {
      console.log("发送ping");
    });
  });
}, 6000);

wss.on("close", () => {
  clearInterval(interval);
});

stomp

Stomp全称Simple Text Oriented Messaging Protocol,即简单文本定向消息协议。它提供一种特定的格式,以便Stomp客户端和Stomp服务器进行通讯。stomp支持文本,同时也支持二进制。

stomp帧

stomp发送的消息称为帧,帧由三部分组成: command、headers和body。这和http协议有些类似。

  • command :字符串类型 针的名称。例如“CONNECT”、“SEND”、“SUBSCRIBE”等。

  • headers : JavaScript对象,有content-length、content-type等字段。

  • body: 可以是二进制,也可以是文本。

command和headers总是会被定义,但是headers和body可以为空。body和headers之间通过一个空行来分隔。

stomp客户端

对于stomp客户端来说,它会扮演两种角色的其中一种:

  1. 生产者,发送消息到指定地址。

  2. 消费者,通过发送subscribe帧进行消息订阅,当消费者发送消息到该订阅地址后,订阅该地址的其他消费者接收消息。

客户端在这两个角色中互相转换,这种订阅机制使得Websocket可以很方便地实现点对点通信和广播通讯。

实现点对点通讯和广播通讯的流程

  1. 客户端建立websocket连接

  2. 客户端获取消息订阅地址。

  3. 客户端作为消费者,通过stomp进行订阅(subsribe)消息地址,并定义接收消息的回调函数

  4. 客户端作为生产者,发送消息到指定地址,在消息body中指定消息接收者的id,如果没有指定接收者id,则该地址的所有订阅者都会收到该消息。

// 订阅消息和接收消息的回调函数
ws.subscribe("/user/queue/msg", function acceptMessage(msg){
		/* */
});

// 发送消息
ws.send(
	"/user/queue/msg",  // 目标地址
	{},  // headers
	JSON.stringify({  // body
		userBid: this.params.bid,
		reciver: this.othersBid,
		content: JSON.stringify(msg),
	})
);

调试

可以在debug中查看stomp发送或者接收的是什么

ws.debug = function(str) {
	console.log(str)
}

常用的Websocket API

事件

  • error

  • message

  • close

  • open

方法

  • close

  • send

属性

[WebSocket.readyState](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/readyState) :当前的链接状态。0:connecting、1:open、2:closing、3:closed

[WebSocket.url](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/url) :WebSocket 的绝对路径

[WebSocket.binaryType](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/binaryType):使用二进制的数据类型连接。

[WebSocket.bufferedAmount](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/bufferedAmount) :只读未发送至服务器的字节数。

[WebSocket.extensions](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/extensions) 服务器选择的扩展。

踩过的坑

异常断线处理

所谓的异常断线,是指由于各种因素影响,导致ws连接异常断开,并且这种异常断开客户端和服务器都无法感知到。

这个问题是在微信小程序中出现,当微信小程序进入后台5s后,微信会将该小程序挂起,这时就会断开ws连接,最重要的是这种断开操作服务器和前端都无法感知,因此导致了消息发不出去的问题。

处理异常断线的方案是心跳机制。

心跳机制前面有讲过,所谓的心跳条件机制就是每隔一段时间就送一个空消息给对方,如果收到确认消息就表示ws连接有效,否则就证明ws连接失效,此时需要重新建立连接。

如何确保消息成功发送给了对方

stomp客户端接收到对方消息时,会自动进行应答(ack),这有些类似TCP协议,通过这种应答的方式后端能够判断消息是否正确地传递给了接收方,然后再将这一结果返回给发送方。

具体是思路是后端一旦在一定时间内没有接收到ack确认,就发送一个error事件通知发送方。

参考

上一页跨端小程序下一页跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd

最后更新于1年前

ws文档:

websocket api:

websocket介绍:

stomp介绍:

stomp官网:

stompjs:

ws/ws.md at master · websockets/ws (github.com)
https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
一篇吃透WebSocket:概念、原理、易错常识、动手实践
STOMP Over WebSocket (jmesnil.net)
stomp.github.io
StompJs Family