Websocket
WebSocket 网络模块
介绍#
mk.network.Websocket(以及微信运行时下的 mk.network.WebsocketWX)是基于 mk.network.Base 的 WebSocket 客户端封装,面向用户提供一套以事件驱动、支持二进制/自定义协议、可等待回复的消息通信层。
特点#
- 可插拔编解码器(基于
MKCodecBase),支持任意数据格式传输。 - 消息事件化:通过
message对象以消息类型为 key 进行on/once/off/send/request等操作。 - 请求-响应等待:
message.request(data)返回 Promise,自动根据“消息序列号”匹配响应并 resolve。 - 心跳(心跳超时检测 + 可选客户端定时发心跳)与断线重连(间隔 & 次数控制)。
- 支持发送潮(SendTide)——把多条消息按节奏发送以避免“消息洪峰”。
- 兼容微信平台(
Websocket/WebsocketWX)。
用户层接口总览#
mk.network.Base:抽象基类的用户可见封装(实际使用时通过mk.network.Websocket/mk.network.WebsocketWX)。mk.network.Base_:命名空间,包含初始化配置类型InitConfig、枚举等(用于构造或阅读默认值)。mk.network.Websocket:浏览器 / 编辑器 环境下的 WebSocket 实现。mk.network.Websocket_:Websocket 特有的配置类(如binaryType,protocolStrList)。mk.network.WebsocketWX:微信小程序环境下的 SocketTask 实现(使用wx.connectSocket)。
所有网络实例都暴露
event与message两个主要对象用于事件与消息交互;并可通过config与codec做进一步定制。
概念#
消息号#
消息号代表消息体类型
例如
// 消息号 0
interface MessageA {
data: number;
}
// 消息号 1
interface MessageB {
data2: number;
}
interface MessageData {
// 消息号
id: number;
// 消息体:json 格式
data: string;
}
MessageData 为客户端和服务器通信的消息结构,那么怎么知道 MessageData.data 是 MessageA 还是 MessageB?
这就可以通过消息号MessageData.id判断,每个消息体都有属于自己的固定消息号
消息序列号#
消息发送批次编号,用于识别服务器返回的数据对应客户端之前发送的哪个消息
例如
- 客户端 -> 消息 A(消息号 0, 序列号 0) -> 服务器
- 客户端 -> 消息 A(消息号 0, 序列号 1) -> 服务器
- 服务器 -> 消息 A 结果(消息号 0, 序列号 0) -> 客户端
客户端发送了两次消息 A,如果服务器返回数据只有消息号,没有消息序列号,我们可以知道这是消息 A 的结果数据, 但是不知道这是第一个还是第二个消息 A 的结果数据
消息序列号就代表了这是第一个还是第二个消息 A 的结果数据
创建#
创建网络对象进行通信
const ws = new mk.network.Websocket({
// codec:消息体编解码器
// sendIntervalMsNum:发送间隔(毫秒),防止一帧内发送多次
// reconnectIntervalMsNum:重连间隔(毫秒),默认 1000
// maxReconnectNum:最大重连次数,默认 5
// waitTimeoutMsNum:等待消息超时时间(毫秒),默认 5000
// heartbeatConfig:心跳配置
// parseMessageIdFunc(data: any):消息号解析函数,参数为消息体
// parseMessageSequenceFunc(data: any):消息序列号解析函数,参数为消息体
});
// 微信下可使用 new mk.network.WebsocketWX
属性#
event#
网络全局事件对象,提供了如下事件类型
| 事件名 | 描述 |
|---|---|
| open | 网络连接 |
| close | 网络断开 |
| reconnectSuccess | 重连成功 |
| reconnectFail | 重连失败 |
| heartbeatTimeout | 心跳超时 |
| recv | 收到任意消息 |
message#
网络消息事件对象,可以用它进行消息发送,监听,请求
使用方式基本和事件一致,以下只说明差异部分
方法#
-
on(type_, callback_, target_?, isOnce_)-
type_:监听消息,类型(Function | string | number)类型为函数则使用
parseMessageIdFunc(type_.prototype)得到的消息号进行监听类型非函数类型则使用
type_进行监听 -
callback_:消息回调,参数为接收到的服务端消息
-
-
emit(data_) 或 emit(type_, data_)模拟服务器消息推送,例如开发调试,多人游戏中的人机或单机模式
type_:消息号,可选,类型(string | number)data_:消息体,框架会使用parseMessageIdFunc(data*)获取的消息号进行emit
-
send(data_)发送消息到服务器
data_:消息体
-
request(data_, timeoutMsNum_)发送消息到服务器并等待返回
data_:消息体timeoutMsNum_:超时时间,-1:无超时时间;0-n:等待时间(毫秒);不填则为构造配置中的 waitTimeoutMsNum
once、off、has的type_和callback_的类型与on一致
最简单示例#
创建一个通用网络对象,并且收发字符串
const ws = new mk.network.Websocket();
// 连接服务器
await ws.connect('ws://127.0.0.1:8848');
// 发送字符串消息
ws.message.send('123');
// 监听所有返回的消息
ws.event.on(
ws.event.key.recv,
(data: string) => {
console.log('收到消息', data);
},
this
);
带消息号示例#
现在我们创建一个通过 JSON 数据通信的示例,并且在 JSON 中附带消息号和消息序列号
// 创建编解码器,收到的数据会解码为 Object,发送的数据编码为 JSON
const codec = new (class extends mk.CodecBase {
decode(data: string): Object {
return JSON.parse(data);
}
encode(data: Object): string {
return JSON.stringify(data);
}
})();
// 创建网络对象
const ws = new mk.network.Websocket({
codec: codec,
// 从消息体解析得到消息号
parseMessageIdFunc: (data: any) => {
return data.id;
},
// 从消息体解析得到消息序列号
parseMessageSequenceFunc: (data: any) => {
return data.sequence;
},
});
/** 消息序列号 */
let messageSequenceNum = 0;
/** 创建消息 A */
const createMessageA = (data: number): any => {
return {
// 消息号 0 代表消息 A,创建其他消息则递增
id: 0,
// 消息序列号自增代表发送批次
sequence: messageSequenceNum++,
// 消息数据
data: data,
};
};
const messageA = createMessageA(123);
const messageA2 = createMessageA(456);
// 监听消息号为 0 的消息接收数据
ws.message.on(0, (data) => {
console.log('监听返回', data);
});
// 发送 messageA2 消息等待服务器返回
ws.message.request(messageA2, -1)?.then((data) => {
console.log('请求返回', data);
});
// 模拟服务器发送 messageA 数据,触发监听返回(消息号一致)
ws.message.emit(messageA);
// 模拟服务器发送 messageA2 数据,触发监听返回(消息号一致)和请求返回(消息序列号一致)
ws.message.emit(messageA2);
日志结果
监听返回 {id: 0, sequence: 0, data: 123}
监听返回 {id: 0, sequence: 1, data: 456}
请求返回 {id: 0, sequence: 1, data: 456}
其他消息结构#
在游戏中最常用的实际是 protobuf,但是我们的编解码机制可以支持任意格式,前提是可以从消息体解析得到消息号,和消息序列号
示例:
enum MessageID {
/** 测试消息 */
Test = 0;
Test2 = 1;
}
message Package {
/** 消息号 */
int32 id = 1;
/** 消息序列号 */
int32 sequence = 2;
/** 消息体 */
bytes data = 3;
}
// 后缀说明,C: 客户端发送消息,S:服务器返回消息,B:服务器推送消息
message TestC {
int32 data = 1;
}
message TestS {
int32 data = 1;
}
message Test2B {
string data = 1;
}
我们可以从消息体的命名得到 MessageID 中对应的消息号,而消息序列号在编解码中递增就行了
实现参考:框架网络功能示例 代码中的 网络 1 部分
重连#
配置#
流程(自动)#
-
网络断开时,event 会派发
close事件 -
框架通过网络对象配置的 重连间隔(reconnectIntervalMsNum) 和 最大重连次数(maxReconnectNum) 进行重连
-
如果重连成功,event 派发
reconnectSuccess事件,重连失败则派发reconnectFail事件
心跳#
配置#
-
heartbeatConfig.intervalMsN: 发送间隔(毫秒),客户端向服务器发送的心跳间隔 -
heartbeatConfig.timeoutMsN: 超时时间(毫秒),客户端多久未收到服务器心跳为超时 -
heartbeatConfig.initFunc(doneFunc): 心跳初始化函数,- 参数
doneFunc: 用户在initFunc函数内监听服务器心跳消息,收到心跳时调用参数doneFunc()
- 返回值
null: 代表客户端不向服务器发送心跳数据() => any: 客户端会在每次心跳时执行这个函数,将返回值(心跳数据)发送给服务器
- 参数
流程#
-
网络对象 open 状态时,框架开始以间隔时间(heartbeatConfig.intervalMsN) 发送 心跳数据(heartbeatConfig.initFunc 返回函数的返回值)
-
网络对象 close 状态时则时停止心跳
消息潮#
如果你想要存储数据以批量发送,例如指定间隔,或手动触发发送,则可以使用消息潮
创建#
let sendTide = new mk.network.Base_.SendTide(
// 网络对象
ws,
// 发送间隔,-1:手动触发,>0:自动发送间隔毫秒
1000
);
方法#
-
send: 发送消息接口 -
trigger: 触发发送,前提是发送间隔为 -1 -
clear: 清理所有未发送消息