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,快速发现问题,解决问题。
欢迎关注微信公众号 闹闹前端