设计一个即时聊天功能

目录

websocket基本概念
使用nodejs设计一个简单的ws服务器
stomp基本概念和使用
常用的websocket API
踩过的坑

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. 生产者,发送消息到指定地址。
  1. 消费者,通过发送subscribe帧进行消息订阅,当消费者发送消息到该订阅地址后,订阅该地址的其他消费者接收消息。
客户端在这两个角色中互相转换,这种订阅机制使得Websocket可以很方便地实现点对点通信和广播通讯。

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

  1. 客户端建立websocket连接
  1. 客户端获取消息订阅地址。
  1. 客户端作为消费者,通过stomp进行订阅(subsribe)消息地址,并定义接收消息的回调函数
  1. 客户端作为生产者,发送消息到指定地址,在消息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 :当前的链接状态。0:connecting、1:open、2:closing、3:closed
WebSocket.url :WebSocket 的绝对路径
WebSocket.binaryType使用二进制的数据类型连接。
WebSocket.bufferedAmount :只读未发送至服务器的字节数。
WebSocket.extensions 服务器选择的扩展。
 

踩过的坑

异常断线处理

所谓的异常断线,是指由于各种因素影响,导致ws连接异常断开,并且这种异常断开客户端和服务器都无法感知到。
这个问题是在微信小程序中出现,当微信小程序进入后台5s后,微信会将该小程序挂起,这时就会断开ws连接,最重要的是这种断开操作服务器和前端都无法感知,因此导致了消息发不出去的问题。
处理异常断线的方案是心跳机制。
心跳机制前面有讲过,所谓的心跳条件机制就是每隔一段时间就送一个空消息给对方,如果收到确认消息就表示ws连接有效,否则就证明ws连接失效,此时需要重新建立连接。

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

stomp客户端接收到对方消息时,会自动进行应答(ack),这有些类似TCP协议,通过这种应答的方式后端能够判断消息是否正确地传递给了接收方,然后再将这一结果返回给发送方。
具体是思路是后端一旦在一定时间内没有接收到ack确认,就发送一个error事件通知发送方。
 

参考

stomp官网:stomp.github.io