STOMP 概念
STOMP 是一种简单易用的长链接协议,他是一种基于帧frame的协议,其帧在HTTP上建模。
它内部实现了心跳检测,使用的是window.setInterval。
也可以根据环境自定义心跳包。
自定义长链接
STOMP 通过 over 方法接收自定义长链接对象。
微信小程序长链接
1. wx.connectSocket
使用 wx.connectSocket 创建长链接,它会返回一个 socketTask。
            
            
              js
              
              
            
          
          let socketTask = wx.connectSocket({
    url: socketUrl,
});
        2. 监听 WebSocket 连接打开事件
            
            
              js
              
              
            
          
          socketTask.onOpen((frame) => {
    // 处理打开事件
});
        3. 监听 WebSocket 接收到服务器的消息事件
            
            
              js
              
              
            
          
          socketTask.onMessage((frame) => {
    // 处理接收消息
});
        4. 监听 WebSocket 连接关闭事件
            
            
              js
              
              
            
          
          socketTask.onClose((frame) => {
    // 处理关闭事件,比如重连
});
        与STOMP绑定
1. 创建 ws 对象与 socketTask 绑定
创建wx.connectSocket后,创建 ws 对象
            
            
              scss
              
              
            
          
          let ws = {
    send(frame) {
      socketTask.send({ data: frame });
    },
    close(frame) {
      socketTask.close(frame);
    },
};
        - 定义 send 方法,内部使用 
socketTask.send发送消息 - 定义 close 方法,内部使用 
socketTask.close关闭长链接 
2. 修改 socketTask.onOpen 绑定 open 事件
        
            
            
              js
              
              
            
          
          socketTask.onOpen((frame) => {
+    ws.onopen(frame);
+    if (this.RECONNECT_COUNT > 0) { // 连接成功后回调
+      this.reconnectCallback && this.reconnectCallback();
+    }
+    this.RECONNECT_COUNT = 0;
});
        3. 修改 socketTask.onMessage 绑定 onmessage 事件
        
            
            
              js
              
              
            
          
          socketTask.onMessage((frame) => {
    // 处理接收消息
+    ws.onmessage(frame);
});
        4. 修改 socketTask.onClose 绑定 close 事件
        
            
            
              js
              
              
            
          
          socketTask.onClose((frame) => {
    // 处理关闭事件,比如重连
+    this.stompClient._cleanUp(); // 清理 stomp客户端
+    setTimeout(() => {
+       // 处理重连逻辑代码
+       // ......省略代码
+    }, this.RECONNECT_TIME_INTERVAL);
});
        5. 使用 Stomp.over 方法绑定 ws 对象
        
            
            
              js
              
              
            
          
          let stompClient = Stomp.over(ws);
this.stompClient = stompClient;
        6. Stomp 客户端进行连接
            
            
              js
              
              
            
          
          stompClient.connect(header, (frame) => {
    let timer = setInterval(() => {
      let socketStatus = this.getSocketStatus();
      if (socketStatus) {
        clearInterval(timer);
        return;
      }
      this.connectCallback = connectCallback; // 连接成功的回调
      this.socketTask = socketTask; // wx.connectSocket返回的对象
      this.reconnectCallback = reconnectCallback; // 重连的回调
      connectCallback(stompClient); // 连接回调后可以订阅
    }, 60);
});
        7. 订阅subscribe
            
            
              js
              
              
            
          
          stompClient.subscribe(
    `/topic/message/${this.roomId}`,
    (message) => {
      const wsResult = JSON.parse(message.body);
      this.handleWSInfo(wsResult);
    }
)
        它会返回一个对象,可以调用对象的 unsubscribe 取消订阅。
完整代码
stomp 代码
raw.githubusercontent.com/jmesnil/sto...
封装微信小程序长链接并与stomp绑定
            
            
              js
              
              
            
          
          import { Stomp } from './stomp';
class WebSocket {
  /**
   * 微信 WebSocket 任务
   */
  socketTask = null;
  /**
   * Stomp代理
   */
  stompClient = null;
  /**
   * 默认监听的消息频道
   */
  channel = null;
  /**
   * 重连成功的回调
   */
  reconnectCallback = null;
  /**
   * 主动断开连接的标识
   */
  disconnectFlag = false;
  /**
   * 默认最大重连次数
   */
  RECONNECT_MAX_COUNT = 30;
  /**
   * 默认重连时间间隔(单位:ms)
   */
  RECONNECT_TIME_INTERVAL = 1000;
  /**
   * 断线重连计数
   */
  RECONNECT_COUNT = 0;
  constructor() {
    /* setInterval是用来发心跳包的,而小程序没有window对象 */
    Stomp.setInterval = function (interval, f) {
      return setInterval(f, interval);
    };
    Stomp.clearInterval = function (id) {
      return clearInterval(id);
    };
  }
  /**
   * 建立websocket连接和频道监听,绑定消息处理器
   * @param header            消息头
   * @param socketUrl      连接地址
   * @param connectCallback  连接成功回调
   * @param reconnectCallback 重连成功回调
   */
  init(params) {
    const {
      header = {},
      socketUrl,
      connectCallback,
      reconnectCallback,
    } = params;
    if (!this.getSocketStatus()) {
      let socketTask = wx.connectSocket({
        url: socketUrl,
      });
      let ws = {
        send(frame) {
          socketTask.send({ data: frame });
        },
        close(frame) {
          socketTask.close(frame);
        },
      };
      socketTask.onOpen((frame) => {
        ws.onopen(frame);
        if (this.RECONNECT_COUNT > 0) {
          this.reconnectCallback && this.reconnectCallback();
        }
        this.RECONNECT_COUNT = 0;
      });
      socketTask.onMessage((frame) => {
        console.log('websocket接收消息', frame);
        ws.onmessage(frame);
      });
      socketTask.onClose((frame) => {
        this.stompClient._cleanUp();
        /* 客户端主动断开连接,不启动重连。 */
        if (this.disconnectFlag) {
          this.disconnectFlag = false;
          return;
        }
        setTimeout(() => {
          this.RECONNECT_COUNT += 1;
          if (this.RECONNECT_COUNT >= this.RECONNECT_MAX_COUNT) {
            console.log('websocket连接失败');
            return;
          }
          this.init({
            header,
            socketUrl,
            connectCallback,
            reconnectCallback: this.reconnectCallback,
          });
        }, this.RECONNECT_TIME_INTERVAL);
      });
      let stompClient = Stomp.over(ws);
      this.stompClient = stompClient;
      stompClient.connect(header, (frame) => {
        let timer = setInterval(() => {
          let socketStatus = this.getSocketStatus();
          if (socketStatus) {
            clearInterval(timer);
            return;
          }
          this.connectCallback = connectCallback;
          this.socketTask = socketTask;
          this.reconnectCallback = reconnectCallback;
          connectCallback(stompClient);
        }, 60);
      });
    }
  }
  /**
   * 发送消息
   * @param channel 频道
   * @param header  消息头
   * @param body    消息体
   */
  sendMessage(channel, header, body) {
    if (this.getSocketStatus()) {
      this.stompClient.send(channel, header, JSON.stringify(body));
    }
  }
  /**
   * 关闭连接
   */
  close() {
    if (this.getSocketStatus()) {
      this.stompClient.disconnect();
      this.disconnectFlag = true;
    }
  }
  /**
   * 获取连接状态
   * @return boolean
   */
  getSocketStatus() {
    let boolean = false;
    if (this.socketTask && this.socketTask.readyState) {
      boolean = this.socketTask.readyState === 1;
    }
    return boolean;
  }
}
export default WebSocket;
        注意:constructor 函数里自定义了心跳包
封装本地
            
            
              js
              
              
            
          
          import WebSocket from '@/utils/websocket';
import { socketUrl } from '@/api/config';
import tokens from './tokens';
let wsStomp = null;
const WS_URL = `${socketUrl}/ws/stomp`;
export const SEND_URL = '/app/message/';
let connectCount = 0;
export function initWsStomp(roomId, connectCallback, reconnectCallback) {
  if (wsStomp) {
    connectCallback(wsStomp.stompClient);
    return wsStomp;
  }
  wsStomp = new WebSocket();
  const token = tokens.get();
  const params = {
    socketUrl: `${WS_URL}?Authorization=${token}`,
    connectCallback,
    reconnectCallback,
    header: {
      Authorization: `${token}`,
    },
  };
  wsStomp.init(params);
}
export function closeWsStomp() {
  if (wsStomp) {
    wsStomp.close();
    wsStomp = null;
  }
}
export function sendWsStomp(roomId, message, header = {}) {
  const token = tokens.get();
  if (roomId && wsStomp) {
    wsStomp.sendMessage(
      `${SEND_URL}${roomId}`,
      {
        Authorization: `${token}`,
        ...header,
      },
      message
    );
  }
}
        使用
- 在业务合适的时机调用 initWsStomp
 
            
            
              js
              
              
            
          
          initWsStomp(
    this.roomId,
    (stompClient) => {
      // 连接成功开始订阅
      this.topicUnsub = stompClient.subscribe(
        `/topic/message/${this.roomId}`,
        (message) => {
          // 接收服务器推送的消息
          const wsResult = JSON.parse(message.body);
          this.handleWSInfo(wsResult);
        }
      );
      // 第二个订阅
      this.userUnsub = stompClient.subscribe(
        `/user/${uid}/message/${this.roomId}`,
        (message) => {
          // 接收服务器推送的消息
          const wsResult = JSON.parse(message.body);
          this.handleWSInfo(wsResult);
        }
      );
    },
    () => {
      console.log('reconnectResult');
    }
  );
        - 在合适的时机取消订阅
 
            
            
              js
              
              
            
          
          if (this.topicUnsub) {
    this.topicUnsub.unsubscribe();
}
if (this.userUnsub) {
    this.userUnsub.unsubscribe();
}
        比如页面跳转,就需要取消当前页面长链接的订阅 3. 合适的时机关闭长链接
            
            
              js
              
              
            
          
          closeWsStomp();
        踩坑
iPhone 手机总断链
苹果手机接收消息的最后一个字符不是 0x00 导致内部进行分割失败。
修复方法是在 onMessage 里判断最后一个字符是否是0x00,不是就添加上。
            
            
              js
              
              
            
          
          socketTask.onMessage((frame) => {
+    if (frame && frame.data) {
+      let value = frame.data;
+      let code = value.charCodeAt(value.length - 1);
+      if (code !== 0x00) {
+        value += String.fromCharCode(0x00);
+        frame.data = value;
+      }
+    }
    ws.onmessage(frame);
});
        有时候客户端发送消息后收不到对应的消息
客户端可能发送了一个不在前后端约定的 COMMAND 里,导致收不到消息。
最好让服务端再接收到消息后,如果 COMMAND 不在处理范围内,返回一个 ERROR 。
同一个页面处理两种身份的逻辑
长链接是为了即时处理状态。当一方状态变更,另一方也需要即时知道。
如果把两种身份处理逻辑混在一个页面或组件里,会很难处理。需要很多标识。
简单处理方式就是,按身份抽取组件。
在微信小程序里,可以根据 onload 的 query 获取不同的身份,然后渲染不同身份的逻辑,这样简单,不容易混乱。
调试
微信小程序里长链接调试很不方便,开发者工具是抓不到长链接的包的。
特别类似双人对战这种模式,在手机上只能看到日志,不能 debug。
如果可以,找同事的电脑配合,用两台电脑,就有两个开发者工具,这样就可以 debug,快速发现问题,解决问题。
欢迎关注微信公众号 闹闹前端