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)。

所有网络实例都暴露 eventmessage 两个主要对象用于事件与消息交互;并可通过 configcodec 做进一步定制。

概念#

消息号#

消息号代表消息体类型

例如

// 消息号 0
interface MessageA {
    data: number;
}

// 消息号 1
interface MessageB {
    data2: number;
}

interface MessageData {
    // 消息号
    id: number;
    // 消息体:json 格式
    data: string;
}

MessageData 为客户端和服务器通信的消息结构,那么怎么知道 MessageData.dataMessageA 还是 MessageB? 这就可以通过消息号MessageData.id判断,每个消息体都有属于自己的固定消息号

消息序列号#

消息发送批次编号,用于识别服务器返回的数据对应客户端之前发送的哪个消息

例如

  1. 客户端 -> 消息 A(消息号 0, 序列号 0) -> 服务器
  2. 客户端 -> 消息 A(消息号 0, 序列号 1) -> 服务器
  3. 服务器 -> 消息 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、hastype_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,但是我们的编解码机制可以支持任意格式,前提是可以从消息体解析得到消息号,和消息序列号

示例:

test.proto
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 部分

重连#

配置#

初始化参数

流程(自动)#

  1. 网络断开时,event 会派发 close 事件

  2. 框架通过网络对象配置的 重连间隔(reconnectIntervalMsNum)最大重连次数(maxReconnectNum) 进行重连

  3. 如果重连成功,event 派发 reconnectSuccess 事件,重连失败则派发 reconnectFail 事件

心跳#

配置#

初始化参数

  • heartbeatConfig.intervalMsN: 发送间隔(毫秒),客户端向服务器发送的心跳间隔

  • heartbeatConfig.timeoutMsN: 超时时间(毫秒),客户端多久未收到服务器心跳为超时

  • heartbeatConfig.initFunc(doneFunc): 心跳初始化函数,

    • 参数
      • doneFunc: 用户在 initFunc 函数内监听服务器心跳消息,收到心跳时调用参数 doneFunc()
    • 返回值
      • null: 代表客户端不向服务器发送心跳数据
      • () => any: 客户端会在每次心跳时执行这个函数,将返回值(心跳数据)发送给服务器

流程#

  1. 网络对象 open 状态时,框架开始以间隔时间(heartbeatConfig.intervalMsN) 发送 心跳数据(heartbeatConfig.initFunc 返回函数的返回值)

  2. 网络对象 close 状态时则时停止心跳

消息潮#

如果你想要存储数据以批量发送,例如指定间隔,或手动触发发送,则可以使用消息潮

创建#

let sendTide = new mk.network.Base_.SendTide(
    // 网络对象
    ws,
    // 发送间隔,-1:手动触发,>0:自动发送间隔毫秒
    1000
);

方法#

  • send: 发送消息接口

  • trigger: 触发发送,前提是发送间隔为 -1

  • clear: 清理所有未发送消息