您现在的位置是:首页 >技术杂谈 >WebSocket详解-02网站首页技术杂谈

WebSocket详解-02

空城里的往日时光 2023-05-21 00:00:02
简介WebSocket详解-02

一、背景

        Websocket是html5提出的一个协议规范,是为解决客户端与服务端实时通信。
        WebSocket在连接关闭时会触发onclose事件, 在连接异常时会触发onerror事件。但在弱网环境下, 它们的触发灵敏度不高, 往往延迟很久才触发, 前端再去进行重连操作, 这样很不友好。

        本文将介绍使用心跳重连机制来改善这一问题。

二、原理

        心跳重连机制:前端在WS连接成功的情况下,开始执行心跳函数,首先向服务器端发送ping信息, 服务器内若收到信息则会返回pong信息。在一定时间内, 前端收到服务器返回的信息, 则表示此连接是正常的, 便重置心跳函数; 若前端在一定时间内没有收到心跳函数, 则表明没有连接成功, 此时前端关闭ws, 再执行重连操作。

三、代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>

    <style>
      #container {
        display: grid;
        /*三列, 每列宽50px*/
        grid-template-columns: repeat(4, 50px);
        /*三行, 每行高50px*/
        grid-template-rows: 50px 50px 50px;
        margin-top: 20px;
      }

      .item {
        border: 1px solid #e5e4e9;
        cursor: pointer;
        font-size: 12px;
        color: #ffffff;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .item-1 {
        background-color: #ef342a;
      }
    </style>
  </head>

  <body>
    <h1>Websocket心跳检测</h1>
    <div id="container">
      <div id="btn" class="item item-1">手动停止</div>
    </div>
    <script>
      class Socket {
        constructor(options) {
          // Websocket地址
          this.url = options.url;
          // 监听服务器返回数据
          this.handleMessage = options.handleMessage;
          // Websocket实例对象
          this.ws = null;
          // 心跳函数定时器1:定时发送指令
          this.pingTimer = null;
          // 心跳函数定时器2:定时关闭重连
          this.serverTimer = null;
          this.timeout = options.timeout;
        }

        /**
         * 建立连接
         */
        connect() {
          this.ws = new WebSocket(this.url);

          this.ws.onopen = (e) => {
            console.log("连接成功", e);
            // 启动心跳检测
            this.handleHeartCheck();
          };

          this.ws.onmessage = (e) => {
            this.handleMessage(e.data);
            // 接收到消息则重置心跳函数
            this.handleHeartCheck();
          };

          this.ws.onclose = (e) => {
            console.log("连接关闭", e);
            this.handleClose(e);
          };

          this.ws.onerror = (e) => {
            console.log("连接报错", e);
            this.handleClose(e);
          };
        }

        /**
         * 心跳检查
         */
        handleHeartCheck() {
          if (this.pingTimer) {
            clearTimeout(this.pingTimer);
            this.pingTimer = null;
          }
          if (this.serverTimer) {
            clearTimeout(this.serverTimer);
            this.serverTimer = null;
          }
          this.pingTimer = setTimeout(() => {
            // 发送探测指令
            this.ws.send(JSON.stringify({ type: "ping" }));
            // 超过一定时间还没响应, 手动关闭,再重连
            this.serverTimer = setTimeout(() => {
              console.log("断线重连");
              this.ws.close();
            }, this.timeout);
          }, this.timeout);
        }

        /**
         * 关闭webSocket
         */
        handleClose(e) {
          if (this.pingTimer) {
            clearTimeout(this.pingTimer);
            this.pingTimer = null;
          }
          if (this.serverTimer) {
            clearTimeout(this.serverTimer);
            this.serverTimer = null;
          }
          this.connect();
        }
      }
    </script>
    <script>
      // 初始化
      const ws = new Socket({
        url: "ws://localhost:8765",
        timeout: 3000, // 心跳检测时间
        handleMessage: function (data) {
          // 监听服务器返回信息
          console.log("服务器返回信息", data);
        },
      });
      ws.connect();
      document.getElementById("btn").onclick = () => {
        console.log("手动关闭");
        ws.ws.close();
      };
    </script>
  </body>
</html>

四、Stomp介绍

        概念:面向消息的简单文本协议

        websocket定义了两种传输信息类型:文本信息和二进制信息。类型虽然被确定,但是他们的传输体是没有规定的。所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议。

        STOMP是基于帧的协议,客户端和服务器使用STOMP帧流通讯。

        一个STOMP客户端是一个可以以两种模式运行的用户代理,可能是同时运行两种模式。

        作为生产者,通过SEND框架将消息发送给服务器的某个服务。

        作为消费者,通过SUBSCRIBE制定一个目标服务,通过MESSAGE框架,从服务器接收消息。

五、Stomp常用代码

// 发送消息
stompClient.send('/topic/terminal_chart', {}, "ping") 
// 订阅(subscription是一个对象,包含一个id属性,对应这个这个客户端的订阅ID)
var subscription = client.subscribe("/queue/test", callback);
// 终止订阅消息
subscription.unsubscribe();
// 获取STOMP子协议的客户端对象
stompClient = Stomp.over(socket);
// 心跳发送频率
stompClient.heartbeat.outgoing = 20000;
// 心跳接收频率
stompClient.heartbeat.incoming = 20000;
// 调用.connect方法连接Stomp服务端进行验证
stompClient.connect({}, ()=>{
    // 成功回调
}, ()=>{
    // 失败回调(第一次连接失败和连接后断开连接都会调用这个函数)
})
// 判断是否连接
stompClient.connected

六、Stomp自带心跳机制

stomp默认的心跳为10000ms,
heartbeat.outgoing:客户端发给服务端的心跳,* 0表示它不能发送心跳 * 否则它是能保证两次心跳的最小毫秒数
heartbeat.incoming:客户端希望服务端发送的心跳。* 0表示它不想接收心跳 * 否则它表示两次心跳期望的毫秒数
CONNECT
heart-beat:<cx>,<cy> 客户端

CONNECTED:
heart-beat:<sx>,<sy> 服务端
对于client发送server的心跳: * 如果<cx>为0(client不能发送心跳)或者<sy>为0(server不想接收心跳),将不起任何作用。心跳频率为MAX(<cx>,<sy>)毫秒数.
对于server发送client的心跳:心跳频率为MAX(<cy>,<sx>)毫秒数.

七、Stomp手写心跳检测

        如果服务端禁止接收心跳,则stomp自带心跳机制将不起作用,如下手写的代替。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./stomp.js"></script>
  </head>
  <body>
    <script>
      class Socket {
        constructor(options) {
          // 地址
          this.url = options.url;
          // 目的地
          this.target = options.target;
          // 客户端对象
          this.stompClient = null;
          // 心跳频率
          this.timeout = options.timeout;
          // 定时发送心跳
          this.pingTimeout = null;
          // 定时重连
          this.serverTimer = null;
          // 服务端返回的数据处理
          this.handleMessage = options.handleMessage;
        }
        connect() {
          let websocket = new WebSocket(this.url);
          this.stompClient = Stomp.over(websocket);
          this.stompClient.connect(
            {},
            this.connectCallback,
            this.errorCallback
          );
        }
        // 成功回调
        connectCallback = (e) => {
          console.log("连接成功", e);
          this.handleHeartBeat();
          this.stompClient.subscribe(this.target, (res) => {
            if (res.body === "ping") {
              console.log("心跳返回数据", res);
              this.handleHeartBeat();
            } else {
              // 其他逻辑
              this.handleMessage(res);
            }
          });
        };
        // 失败回调(连接失败时的回调函数,此函数重新调用连接方法,形成循环,直到连接成功)
        errorCallback = () => {
          console.log("失败重连");
          if (this.pingTimeout) {
            clearInterval(this.pingTimeout);
            this.pingTimeout = null;
          }
          if (this.serverTimer) {
            clearInterval(this.serverTimer);
            this.serverTimer = null;
          }
          this.connect();
        };

        handleHeartBeat() {
          if (this.pingTimeout) {
            clearInterval(this.pingTimeout);
            this.pingTimeout = null;
          }
          if (this.serverTimer) {
            clearInterval(this.serverTimer);
            this.serverTimer = null;
          }
          this.pingTimeout = setTimeout(() => {
            // 发出探测指令
            this.stompClient.send(this.target, {}, "ping");
            this.serverTimer = setTimeout(() => {
              this.errorCallback();
            }, this.timeout);
          }, this.timeout);
        }
      }
    </script>
    <script>
      let ws = new Socket({
        url: "ws://10.100.31.48:1995/customer/bj-metro-server-customer",
        target: "/topic/terminal_chart",
        timeout: 10000,
        // 订阅信息的返回
        handleMessage: (res) => {},
      });
      ws.connect();
    </script>
  </body>
</html>

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。