WebSocket 客户端实践

我正在参与掘金创作者训练营第5期,点击了解活动详情

前言

WebSocket 是一种双工通讯协议,主要用于客户端—服务器通信。当建立连接之后,客户端和服务器都可以主动向对方发送或接收数据。

WebSocket是建立在TCP之上,跟HTTP一样通过TCP传递数据。

使用 WebSocket 创建的连接是长时间存在的通信通道,但是只要其中一方断开连接,那么将无法通信。

WSS 用于需要加密通道(TCP+TLS)。WS一般默认端口号80,WSS默认端口号443。在https下不能使用 ws:// 。且wss下不支持ip地址的写法(必须写成域名形式)。

实现一个WebSocket连接

关于 WebSocket Api 基础语法, 是非常简单的。有不懂的小伙伴先可以去读一下阮一峰博客那一篇 **Websocket教程**

我们先通过 new WebSocket 创建连接并获取到 WebSocket 实例。这里的 ws 接口,是我在晚上找的测试接口,有需要可以 戳一下

const ws = new WebSocket('ws://127.0.0.1:8800');
ws.onopen = function () {
  console.log('连接成功');
  ws.send('shenjilin');
  console.log('客户端向服务器发送的消息:shenjilin');
};
ws.onmessage = function (e) {
  console.log('客户端接收服务器发送的消息:' + e.data);
};
复制代码

在浏览器上查看ws请求

以谷歌为例,启动开发者工具,具体操作如下图所示

Untitled.jpeg

WebSocket 建立连接之后,会在浏览器上显示一个连接。如果同一个连接创建多次,查看最近一次连接信息。

Message 这一栏就是客户端和服务器传输的信息,信息格式一般是用 字符串或JSON字符串

扫描二维码关注公众号,回复: 14441996 查看本文章

项目中使用websocket, 如何封装

在实际项目开发过程中,一般不会这么简单的使用,而且使用起来也不是很方便。 所以这时候要将我们 ws 请求进行封装。这里使用 vite+vue3+pinia 来写,还用到的一个 依赖 mitt(是一个体积极小的第三方消息发布/订阅式JavaScript库)

完整代码代码地址: github.com/shenjilina/… 如果以上对您有帮助,欢迎star

1. 实现一个基础ws类

我们现在src目录下创建一个 utils/socket.js 文件

先使用 websocket api 封装一个class类

我们这个类实现创建 ws 实例、创建四个监听事件(onopen, onerror, onclose, onmessage)、重新连接、关闭、销毁等方法。

class Ws {
  // websocket 接口地址
  url;

  // WebSocket 实例
  ws;

  // 重连中
  isReconnectionLoading = false;

  // 延时重连的 id
  timeId = null;

  // 用户手动关闭连接
  isCustomClose = false;

  // 错误消息队列
  errorStack = [];

  constructor(url) {
    this.url = url;
    this.createWebSocket();
  }

  // 创建一个 webSocket 连接
  createWebSocket() {
    if ("WebSocket" in window) {
      if (flag) return;
      flag = true;
      // 实例化WebSocket
      this.ws = new WebSocket(this.url);
      // 监听事件
      this.onopen();
      this.onerror();
      this.onclose();
      this.onmessage();
    } else {
      console.log("你的浏览器不支持 WebSocket");
    }
  }

  // 监听成功
  onopen() {
    this.ws.onopen = () => {
      console.log("onopen: 连接成功了");
      // 连接成功。 检查之前发送失败的消息,如果有就直接再次发送
      this.errorStack.forEach((message) => {
        this.send(message);
      });
      this.errorStack = [];
      this.isReconnectionLoading = false;
    };
  }

  // 监听错误
  onerror() {
    this.ws.onerror = (err) => {
      this.reconnection();
      this.isReconnectionLoading = false;
    };
  }

  // 监听关闭
  onclose() {
    this.ws.onclose = () => {
      // 如果是用户手动关闭的,直接返回
      if (this.isCustomClose) return;
      // 重新连接
      this.reconnection();
      this.isReconnectionLoading = false;
    };
  }

  // 接收 WebSocket 消息
  onmessage() {
    this.ws.onmessage = (event) => {
      try {
        const data = event.data;
				console.log("接收到消息:",data)
      } catch (error) {
        console.log(error, "error");
      }
    };
  }

  // 重新链接
  reconnection() {
    // 防止重复重新链接
    if (this.isReconnectionLoading) return;
    this.isReconnectionLoading = true;
    flag = null;
    clearTimeout(this.timeId);
    this.timeId = setTimeout(() => {
      this.createWebSocket();
    }, 3000);
  }

  // 发送消息
  send(message) {
    // 连接失败时的处理
    if (this.ws.readyState !== 1) {
      this.errorStack.push(message);
      return;
    }

    this.ws.send(message);
  }

  // 手动关闭
  close() {
    flag = null;
    this.isCustomClose = true;
    this.ws.close();
  }

  // 手动开启
  start() {
    this.isCustomClose = false;
    this.reconnection();
  }

  // 销毁
  destroy() {
    this.close();
    this.ws = null;
    this.errorStack = null;
  }
}
export default Ws;
复制代码

2. 在ws这个类上添加mitt(发布/订阅)

首先安装依赖 pnpm i mitt

  1. 创建文件 utils/mitt.js
import mitt from 'mitt';

export const emitter = mitt();
复制代码
  1. 我们再改造一下上面 ws 类

先在 utils/socket.js 中导入 emitter

// 消息管理中心
import { emitter } from "./mitt";
复制代码
  1. 然后在ws类中创建两个方法。 分别是订阅和取消订阅,订阅方法是在我们创建实例后订阅服务器的消息,取消订阅在我们关闭或者销毁这个连接时调用
// 订阅
subscribe(eventName, cb) {
  emitter.on(eventName, cb);
}

// 取消订阅
unsubscribe(eventName, cb) {
  emitter.off(eventName, cb);
}
复制代码
  1. onmessage 是我们接收服务器发送给客户端的消息监听事件,这里的参数就是服务器发送过来的消息体。我们针对不同的消息类型有不同的处理方式,这里我们使用mitt(事件发布订阅库)这个依赖,将我们服务器发送过来的消息体根据不同类型发布。如果我们想在某个组件中使用,提前订阅这个类型就好
onmessage() {
  this.ws.onmessage = (event) => {
    try {
      const data = event.data;
      // 发布消息到消息中心
      emitter.emit(data.type, data);
    } catch (error) {
      console.log(error, "error");
    }
  };
}
复制代码
  1. 当我们连接销毁的时候也要关闭我们所有的订阅消息。在销毁方法添加移除订阅
// 销毁
destroy() {
  this.close();
  this.ws = null;
  this.errorStack = null;
  // 移除所有订阅
  emitter.all.clear();
}
复制代码

在pinia中使用 WS 类

安装依赖 pnpm i pinia

创建文件 store/socket.js

我们通过 pinia 二次封装,这里我们写了 初始化、订阅、发送消息以及销毁方法 。初始化的时候会调用消息订阅方法

import Ws from '@/utils/socket';

export const useSocket = defineStore('socket', () => {
  // socket 实例
  const instance = ref(null);
  // socket 消息
  const socketData = ref(null);

  /**
   * @Author: shenjilin
   * @Date: 2022-07-29 15:55:38
   * @description: socket 初始化
   * @param {*} url
   * @return {*} socket 实例
   */  
  const wsInit = async (url) => {
    if (!instance.value) {
      const ws = new Ws(url);
      instance.value = ws;
			// 订阅type = notify 的消息
      wsSubscribe("notify");
    }
    return instance.value;
  };

  /**
   * @Author: shenjilin
   * @Date: 2022-07-29 15:43:45
   * @description: 订阅websocket消息
   * @param {string} type  消息类型
   * @return {*}
   */
  const wsSubscribe = (type) => {
    instance.value.subscribe(type, (data) => {
      console.log('接收服务器消息: ', data);
      // 每次接收到消息都会重置 socketData
      socketData.value = data;
    });
  };

  /**
   * @Author: shenjilin
   * @Date: 2022-07-29 15:46:14
   * @description: 发送消息
   * @param {*} data 消息体
   * @return {*}
   */  
  const sendScoket = (data) => {
    instance.value.send(JSON.stringify(data));
  },

  /**
   * @Author: shenjilin
   * @Date: 2022-07-29 15:49:03
   * @description: 销毁
   * @return {*}
   */  
  const destroyScoket = () => {
    if (instance.value) {
      // 销毁socket
      instance.value.destroy();
      instance.value = null;
    }
  },

  return {
    instance,
    socketData,
    wsInit,
    wsSubscribe,
    sendScoket,
    destroyScoket
  };
});
复制代码

在组件中使用

最后就可以在组件中使用,通过实例化 pinia useSocket 获取 socketData, wsInit, sendScoket, 因为我们在初始化

import { watch } from "vue";
import { useSocket } from "@/store/socket";
const { socketData, wsInit, sendScoket } = useSocket();
// 初始化ws
wsInit("ws://127.0.0.1:8800");

// 获取服务器发给客户端的最新数据
watch(
  () => socketData,
  (data) => {
    console.log("ws: ", data);
  },
  {
    immediate: true,
  }
);
// 主动向服务器发送数据
const onsend = () => {
  sendScoket("shenjilin");
};
复制代码

websocket 心跳

我们封装 ws 这个类的时候已经添加 监听错误事件,并在监听到错误时开始重新连接ws。但是导致websocket 断开连接有很多种,比如断网以及客户端和服务器长时间未通信(nginx 代理服务器在一定事件内没有传输任何数据,连接将被关闭, 可以配置nginx的 proxy_read_timeout 读取超时配置,proxy_send_timeout 写入超时配置)。

这里我们可以使用心跳的方式解决以上出现的问题, 先来封装一个方法

utils/heartCheck.js

//心跳检测
export default {
  timeout: 1000 * 6, //6秒
  timeoutObj: null,
  serverTimeoutObj: null,
  reset: function () {
    this.timeoutObj && clearTimeout(this.timeoutObj);
    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
    return this;
  },
  start: function (socket) {
    var self = this;
    this.timeoutObj = setTimeout(function () {
      //这里发送一个心跳,后端收到后,返回一个心跳消息,
      //onmessage拿到返回的心跳就说明连接正常
      socket.send("ping");
      //如果超过一定时间还没重置,说明后端主动断开了
      self.serverTimeoutObj = setTimeout(function () {
        //如果onclose会执行reconnect,我们执行ws.close()就行了.
        //如果直接执行reconnect 会触发onclose导致重连两次
        socket.close();
      }, self.timeout);
    }, this.timeout);
  },
};
复制代码

心跳开启是在我们监听 ws 连接创建成功的时候。心跳方法肯定是每过一段时间就会执行一次,所以我们这里是和后端协商。当后端接收到前端发送的心跳消息(ping),后端返回一个pong。然后 onmessage 这个事件中判断接收到消息(任何类型的消息)的时候,会重置心跳消息的时间。

在 ws 类中的两个方法 onopen 和 onmessage,添加心跳

onopen() {
  this.ws.onopen = () => {
    console.log("onopen: 连接成功了");
    // 连接成功。 检查之前发送失败的消息,如果有就直接再次发送
    this.errorStack.forEach((message) => {
      this.send(message);
    });
    this.errorStack = [];
    this.isReconnectionLoading = false;
    // 重置心跳时间, 开启心跳
    heartCheck.reset().start(this.ws);
  };
}

onmessage() {
  this.ws.onmessage = (event) => {
    try {
      // const data = JSON.parse(event.data);
      const data = event.data;
      // 接到消息重置心跳时间, 开启新的心跳
      heartCheck.reset().start(this.ws);
      if (data.data === "pong") return;
      // 发布消息到消息中心
      emitter.emit("notify", data);
      // emitter.emit(data.type, data);
    } catch (error) {
      console.log(error, "error");
    }
  };
}
复制代码

最后

至此我们客户端websocket封装完成,以上实现思路以及代码上的问题,还望指正!

代码地址: github.com/shenjilina/… 如果以上对您有帮助,欢迎star

猜你喜欢

转载自juejin.im/post/7128030988479037447