websocket的使用

673 阅读1分钟

前端代码

class Socket {
  constructor({ url = '', token = '', roomId = '', callback = () => {} }) {
    this.url = url;
    this.callback = callback;
    this.token = token;
    this.roomId = roomId;
    this.ws = {};
    this.init();
  }

  init() {
    this.ws = new WebSocket(`wss://${this.url}`);
    // WebSocket连接成功时触发
    this.ws.onopen = () => this.onOpen(this.token, this.roomId);
    // WebSocket接受到信息时触发
    this.ws.onmessage = e => this.onMessage(e, this.callback);
    // WebSocket连接信息错误时触发
    this.ws.onerror = this.onError;
    // WebSocket关闭时触发
    this.ws.onclose = this.onClose;
  }

  onOpen(token, roomId) {
    // 连接时,根据身份校验事件,传参给服务端设置对应的连接信息
    this.send({
      event: 'auth',
      message: {
        token,
        roomId
      }
    });
  }

  send(data) {
    this.ws.send(JSON.stringify(data));
  }

  onMessage(e, callback) {
    const data = JSON.parse(e.data);
    switch (data.event) {
      case 'noAuth':
        // 验证失败,返回登录页
        location.href = '/login';
        break;
      case 'heartbeat':
        // 心跳检测,当连接成功,服务端就会定时发起ping请求,需要回复,让服务端知道客户端在线,
        this.send({
          event: 'heartbeat',
          message: 'pong'
        });
        clearTimeout(this.pingTimeout);
        this.pingTimeout = setTimeout(() => {
          this.close();
          this.onError();
        }, 30000 + 1000);
        break;
      default:
        callback(data);
    }
  }

  onError() {
    console.log('websocket连接错误');
    // 连接出错了,1S后重新连接
    setTimeout(() => {
      this.init();
    }, 1000);
  }

  onClose() {
    console.log('websocket连接断开');
  }

  close() {
    // 关闭websocket连接需要关闭心跳检测定时器,防止断开后原来的定时器还在运行连接
    clearTimeout(this.pingTimeout);
    this.ws.close();
  }
}

后端代码

import WebSocket from "ws";
import fs from "fs";
import https from "https";
import jwt from "jsonwebtoken";

class Socket {
  constructor(config) {
    this.config = {
      port: 8080,
      ...config,
    };
    this.wss = {};
    this.init();
  }

  init() {
    const server = https.createServer({
      cert: fs.readFileSync(
        __dirname + "/httpsConfig/5163307_www.pengjianming.top.pem"
      ),
      key: fs.readFileSync(
        __dirname + "/httpsConfig/5163307_www.pengjianming.top.key"
      ),
    });
     // 由于使用的是https,根据官方的用法,这样设置,如果不是使用https直接传入this.config定义端口就可以
    this.wss = new WebSocket.Server({ server });
    this.wss.on("connection", (ws) => {
      // 连接成功,开启心跳检测
      ws.isAlive = true;
      this.heartbeat(ws);

      ws.on("message", (msg) => this.message(ws, msg));
      ws.on("close", () => this.close(ws, this.wss));
    });
    server.listen(8080);
  }

  message(ws, msg) {
    const data = JSON.parse(msg);
    const event = {
      // 身份验证完后添加返回验证信息,并添加对应用户信息到ws上
      auth: (params) => {
        const auth = jwt.verify(params.token, "shared-secret");
        if (auth) {
          ws.user = auth;
          ws.roomId = params.roomId;
        } else {
          ws.send(
            JSON.stringify({
              event: "noAuth",
            })
          );
        }
      },
      // 收到心跳,证明还活着
      heartbeat: () => {
        ws.isAlive = true;
      },
    };
    event[data.event](data.message);
  }

  send(user_name, message) {
    // 排除掉是多人的客户端,根据对应用户名发送消息
    [...this.wss.clients]
      .filter((client) => !client.roomId)
      .forEach((client) => {
        if (
          client.readyState === WebSocket.OPEN &&
          client.user.user_name === user_name
        ) {
          client.send(JSON.stringify({ event: "tip", message }));
        }
      });
  }

  broadcast(roomId, user_id) {
    // 广播对应房间号的,排除自己
    [...this.wss.clients]
      .filter((client) => client.roomId)
      .forEach((client) => {
        if (
          client.readyState === WebSocket.OPEN &&
          client.roomId === roomId &&
          user_id !== client.user.id
        ) {
          client.send(JSON.stringify({ event: "chat" }));
        }
      });
  }

  close(ws) {
    // 关闭连接时关闭对应的定时器
    clearInterval(ws.interval);
  }

  heartbeat(ws) {
    // 假设心跳停止,定时发送ping测试心跳,如果没有返回pong,则会关闭该websocket
    ws.interval = setInterval(() => {
      if (ws.isAlive === false) return ws.terminate();
      ws.isAlive = false;
      ws.send(
        JSON.stringify({
          event: "heartbeat",
          message: "ping",
        })
      );
    }, 30000);
  }
}

export default new Socket();

流程说明

初始化

创建对象时,调用init方法,创建websocket实例,连接成功,此时onOpen也会被调用,与后端进行身份校验,与创建连接信息,根据传递的token,可以确定身份,进行单点信息发送,如果传递了roomId,就可以通过广播进行指定roomId的广播,实现多聊天室那样的效果,如果身份校验通过,则设置对应连接信息,如果不通过就会发送event的noAuth事件,来通知客户端身份校验没通过,客户端接收到noAuth事件,变回跳转登录页,进行重新登录

心跳检测

创建websocket实例,连接成功时,后端便会开始心跳检测,连接成功时先设定还活着isAlive = true,当开启心跳检测时,就设定死了,发送heartbeat事件消息给客户端,如果客户端没有在规定时间发送回heartbeat,设定isAlive = true,那么,在第二次心跳检测开始时,就会认为客户端死了直接断开连接了,如果客户端返回对应heartbeat消息,即心跳正常,客户端每次收到心跳,都会清除之前定时器,开启一个新的定时器,如果在规定时间内,服务端没再次进行心跳检测,便会关闭此客户端,重新连接

协议说明

当使用的是http时,协议为ws,如果使用https则协议为wss,并且,需要设定证书文件

广播信息

遍历所有已连接客户端,通过连接时传递的roomId,进行筛选,然后发送信息

单点发送信息

遍历所有已连接客户端,根据之前身份验证的信息,来传递

传递信息注意

传递的信息需要转换为字符换,不转换会拿到传递信息的类型

如何使用

  this.socket = new Socket({
    url: 'www.pengjianming.top:8080',
    token: getToken(),
    roomId: this.$route.query.id,
    callback: data => {
      if (data.event === 'chat') {
        this.getTicket();
      }
    }
  });

创建后,每次服务端返回信息,如果事件不是设定的heartbeat与noauth,都会调用callback,可以在这里进行需要的事件通知