// 导入 GZIP 处理库(浏览器/Node.js 通用,需提前安装:npm install pako @types/pako) // import * as pako from "pako"; /** * 协议常量类:存储协议核心配置(不可修改,确保前后端一致) */ export class ProtocolConst { /** 协议版本:v1(4位,对应字节3~4的低4位,剩余位保留用于后续升级) */ static readonly PROTOCOL_VERSION = 0b0001; /** 头部固定长度:8字节(字节1~8,结构严格定义,不可修改) */ static readonly HEADER_SIZE = 8; /** 最大包体大小:10MB(防止内存溢出,与 Python 端 MAX_BODY_SIZE 一致) */ static readonly MAX_BODY_SIZE = 1024 * 1024 * 10; /** 字符串编码格式:UTF-8(统一前后端字符串编解码,避免乱码) */ static readonly STRING_ENCODING = "utf-8" as const; // as const 固定字符串类型,避免类型拓宽 } /** * 消息类型枚举(4位,存储在字节1高4位) * 与 Python 端 MessageType 枚举值完全对齐,支持16种类型(当前用5种,剩余预留扩展) */ export enum MessageType { PING = 0b0000, // 心跳消息(用于检测连接存活) AUDIO_DATA = 0b0001, // 纯音频数据(PCM 二进制流,对应 RAW 序列化) TEXT_MESSAGE = 0b0010, // 纯文本消息(对应 STRING 序列化,无 JSON 包装) AUDIO_TEXT_MIX = 0b0011, // 音频+文本混合数据(复杂结构,对应 JSON 序列化) CONTROL_CMD = 0b0100, // 控制指令(暂停/继续/停止等,对应 JSON 序列化) } /** * 序列化方式枚举(4位,存储在字节2高4位) * 按数据类型选择最优序列化方式,减少开销 */ export enum SerializationType { RAW = 0b0000, // 原始二进制(无需编解码,直接透传,适合音频等二进制数据) JSON = 0b0001, // JSON 格式(适合字典、列表等复杂结构,如控制指令、混合数据) STRING = 0b0010, // 直接字符串(UTF-8 编码,无 JSON 包装,适合纯文本消息) } /** * 压缩方式枚举(4位,存储在字节2低4位) * 按需选择压缩策略,平衡性能和传输体积 */ export enum CompressionType { NONE = 0b0000, // 无压缩(小数据包如控制指令、短文本,避免压缩开销) GZIP = 0b0001, // GZIP 压缩(大数据包如长文本、混合数据,减少网络传输量) } /** * 控制指令枚举(配合 MessageType.CONTROL_CMD 使用) * 定义业务层面的控制指令,与 Python 端控制指令值一致 */ export enum ControlCommand { HEARTBEAT = 0b0001, // 心跳响应(回复 PING 消息) PAUSE = 0b0010, // 暂停指令(如暂停音频播放) RESUME = 0b0011, // 继续指令(如恢复音频播放) STOP = 0b0100, // 停止指令(如停止音频传输) } /** * 解包返回结果接口(强类型约束,明确返回数据结构) * 让 IDE 提供自动提示,避免类型错误 */ export interface UnpackedResult { msgType: MessageType; // 消息类型(枚举值,便于逻辑判断) msgTypeName: keyof typeof MessageType; // 消息类型名称(字符串,如 "TEXT_MESSAGE",便于日志打印) serialization: SerializationType; // 序列化方式(枚举值) serializationName: keyof typeof SerializationType; // 序列化方式名称(字符串) compression: CompressionType; // 压缩方式(枚举值) compressionName: keyof typeof CompressionType; // 压缩方式名称(字符串) body: Uint8Array | string | object | unknown[]; // 解包后的原始包体(根据序列化方式动态变化) } /** * 打包入参类型别名(简化方法入参类型定义,提高可读性) */ // 包体支持的类型:二进制、字符串、对象、数组 type PackBody = Uint8Array | string | object | unknown[]; // 序列化方式可选值:枚举值、null、undefined(null/undefined 时自动推导) type OptionalSerialization = SerializationType | null | undefined; /** * 协议编解码工具类(静态类,无需实例化,提供 pack/unpack 静态方法) * 核心功能:将业务数据打包为符合协议的二进制包,或解析二进制包为业务数据 * 特点:与 Python/JS 端完全兼容,性能高效,类型安全 */ export class ProtocolCodec { /** * 打包协议包:将业务数据按协议格式封装为二进制包 * @param msgType 消息类型(必须指定,枚举值) * @param body 业务数据(根据消息类型对应不同类型,如字符串、对象、Uint8Array) * @param serialization 序列化方式(可选,默认自动推导:纯文本→STRING,音频→RAW,复杂结构→JSON) * @param compression 压缩方式(可选,默认无压缩) * @returns 完整协议包(Uint8Array 类型,便于网络传输) * @throws 类型错误、不支持的枚举值、包体过大等异常 */ static pack( msgType: MessageType, body: PackBody, serialization: OptionalSerialization = null, compression: CompressionType = CompressionType.NONE ): Uint8Array { // 1. 自动推导序列化方式(减少调用方心智负担,按消息类型默认最优解) if (serialization === null || serialization === undefined) { if (msgType === MessageType.AUDIO_DATA) { // 音频数据→RAW 序列化(无需编解码,性能最优) serialization = SerializationType.RAW; } else if (msgType === MessageType.TEXT_MESSAGE) { // 纯文本消息→STRING 序列化(直接 UTF-8 编码,无冗余) serialization = SerializationType.STRING; } else if (msgType === MessageType.CONTROL_CMD || msgType === MessageType.AUDIO_TEXT_MIX) { // 控制指令/混合数据→JSON 序列化(支持复杂结构) serialization = SerializationType.JSON; } else { // 非法消息类型:抛出异常,提前阻断错误 throw new Error(`不支持的消息类型:${MessageType[msgType]}(值:${msgType})`); } } // 2. 序列化包体:将业务数据转为二进制(按序列化方式处理) let serializedBody: Uint8Array; // 序列化后的二进制数据 const textEncoder = new TextEncoder(ProtocolConst.STRING_ENCODING); // UTF-8 编码器 switch (serialization) { case SerializationType.RAW: // RAW 序列化:必须传入 Uint8Array(二进制数据直接透传) if (!(body instanceof Uint8Array)) { throw new TypeError(`RAW 序列化要求 body 必须是 Uint8Array 类型,当前传入:${typeof body}`); } serializedBody = body; break; case SerializationType.STRING: // STRING 序列化:必须传入字符串,直接 UTF-8 编码(无 JSON 包装) if (typeof body !== "string") { throw new TypeError(`STRING 序列化要求 body 必须是 string 类型,当前传入:${typeof body}`); } serializedBody = textEncoder.encode(body); break; case SerializationType.JSON: // JSON 序列化:支持字符串、对象、数组,转为 JSON 字符串后 UTF-8 编码 if (typeof body === "string") { // 已为字符串,直接编码 serializedBody = textEncoder.encode(body); } else if (typeof body === "object" && body !== null) { // 对象/数组→JSON 字符串→编码(用 JSON.stringify 序列化) const jsonStr = JSON.stringify(body); serializedBody = textEncoder.encode(jsonStr); } else { throw new TypeError(`JSON 序列化要求 body 必须是 string/object/array 类型,当前传入:${typeof body}`); } break; default: // 非法序列化方式:抛出异常 throw new Error(`不支持的序列化方式:${SerializationType[serialization]}(值:${serialization})`); } // 3. 压缩包体:按指定压缩方式处理(GZIP 或无压缩) let compressedBody: Uint8Array; // 压缩后的二进制数据 if (compression === CompressionType.NONE) { // 无压缩:直接透传序列化后的二进制数据 compressedBody = serializedBody; } else { // 非法压缩方式:抛出异常 throw new Error(`不支持的压缩方式:${CompressionType[compression]}(值:${compression})`); } // 4. 校验包体大小:防止超过最大限制(避免内存溢出) const bodyLen = compressedBody.length; // 压缩后的包体长度 if (bodyLen > ProtocolConst.MAX_BODY_SIZE) { throw new Error( `包体过大(${bodyLen}字节),最大支持${ProtocolConst.MAX_BODY_SIZE}字节(10MB)` ); } // 5. 构造头部:8字节固定结构(严格按协议定义,大端序,与 Python 端一致) const header = new Uint8Array(ProtocolConst.HEADER_SIZE); // 头部缓冲区(8字节) // 字节1:消息类型(4位) + 保留位(4位) // 消息类型左移4位(占高4位),保留位填0(后续扩展用) header[0] = (msgType << 4) | 0x00; // 字节2:序列化方式(4位) + 压缩方式(4位) // 序列化方式左移4位(占高4位),压缩方式占低4位(&0x0F 确保仅4位) header[1] = (serialization << 4) | (compression & 0x0F); // 字节3~4:协议版本(16位大端序) // 大端序:高位字节在前,低位字节在后(网络传输标准字节序) header[2] = (ProtocolConst.PROTOCOL_VERSION >> 8) & 0xFF; // 版本高8位(当前版本为1,高8位为0) header[3] = ProtocolConst.PROTOCOL_VERSION & 0xFF; // 版本低8位(当前版本为1) // 字节5~8:包体长度(32位大端序) // 32位整数:最高位字节(第5字节)→ 最低位字节(第8字节) header[4] = (bodyLen >> 24) & 0xFF; // 包体长度第24~31位 header[5] = (bodyLen >> 16) & 0xFF; // 包体长度第16~23位 header[6] = (bodyLen >> 8) & 0xFF; // 包体长度第8~15位 header[7] = bodyLen & 0xFF; // 包体长度第0~7位 // 6. 拼接头部和包体:生成完整协议包 const totalLen = ProtocolConst.HEADER_SIZE + bodyLen; // 总长度 = 头部8字节 + 包体长度 const packet = new Uint8Array(totalLen); // 完整包缓冲区 packet.set(header, 0); // 从索引0开始写入头部(占前8字节) packet.set(compressedBody, ProtocolConst.HEADER_SIZE); // 从索引8开始写入包体 return packet; // 返回完整协议包(Uint8Array,网络传输高效) } /** * 解包协议包:将二进制协议包解析为业务数据 * @param packet 完整协议包(网络接收的二进制数据,支持 Uint8Array 或 ArrayBuffer) * @returns 结构化解包结果(UnpackedResult 接口,包含消息类型、包体等信息) * @throws 包长度过短、版本不匹配、解压缩失败、反序列化失败等异常 */ static unpack(packet: Uint8Array | ArrayBuffer): UnpackedResult { // 统一数据类型:将 ArrayBuffer 转为 Uint8Array(便于按字节操作) const uint8Packet = packet instanceof ArrayBuffer ? new Uint8Array(packet) : packet; // 1. 校验包长度:至少包含8字节头部 if (uint8Packet.length < ProtocolConst.HEADER_SIZE) { throw new Error( `包长度过短(${uint8Packet.length}字节),至少需要${ProtocolConst.HEADER_SIZE}字节头部` ); } // 2. 拆分头部和包体:前8字节为头部,剩余为包体 const header = uint8Packet.subarray(0, ProtocolConst.HEADER_SIZE); // 头部(0~7索引) const bodyBuffer = uint8Packet.subarray(ProtocolConst.HEADER_SIZE); // 包体(8索引开始) // 3. 解析头部字段(按协议结构逐字节解析,大端序) // 字节1:消息类型(4位) + 保留位(4位) const byte1 = header[0]; const msgType = (byte1 >> 4) & 0x0F; // 右移4位取高4位(消息类型),&0x0F 确保仅4位 // 校验消息类型合法性(防止非法包注入) if (!Object.values(MessageType).includes(msgType as MessageType)) { throw new Error(`非法消息类型:${msgType}(无对应枚举值)`); } // 字节2:序列化方式(4位) + 压缩方式(4位) const byte2 = header[1]; const serialization = (byte2 >> 4) & 0x0F; // 右移4位取高4位(序列化方式) const compression = byte2 & 0x0F; // 取低4位(压缩方式) // 校验序列化方式合法性 if (!Object.values(SerializationType).includes(serialization as SerializationType)) { throw new Error(`非法序列化方式:${serialization}(无对应枚举值)`); } // 校验压缩方式合法性 if (!Object.values(CompressionType).includes(compression as CompressionType)) { throw new Error(`非法压缩方式:${compression}(无对应枚举值)`); } // 字节3~4:协议版本(16位大端序) const version = (header[2] << 8) | header[3]; // 大端序解析:高位字节<<8 + 低位字节 // 校验版本兼容性(仅支持当前协议版本) if (version !== ProtocolConst.PROTOCOL_VERSION) { throw new Error( `协议版本不匹配:收到v${version},当前支持v${ProtocolConst.PROTOCOL_VERSION}` ); } // 字节5~8:包体长度(32位大端序) const bodyLen = (header[4] << 24) | (header[5] << 16) | (header[6] << 8) | header[7]; // 校验包体长度一致性(头部声明长度 vs 实际包体长度) if (bodyBuffer.length !== bodyLen) { throw new Error( `包体长度不匹配:头部声明${bodyLen}字节,实际接收${bodyBuffer.length}字节(可能包丢失或非法包)` ); } // 4. 解压包体:按头部指定的压缩方式解压 let decompressedBody: Uint8Array; // 解压后的二进制数据 if (compression === CompressionType.NONE) { // 无压缩:直接透传包体数据 decompressedBody = bodyBuffer; } else { // 此处理论上不会触发(已提前校验压缩方式合法性) throw new Error(`不支持的压缩方式:${CompressionType[compression]}(值:${compression})`); } // 5. 反序列化包体:将二进制数据转为业务数据(按序列化方式处理) let body: UnpackedResult["body"]; // 反序列化后的原始业务数据 const textDecoder = new TextDecoder(ProtocolConst.STRING_ENCODING); // UTF-8 解码器 switch (serialization) { case SerializationType.RAW: // RAW 反序列化:直接返回 Uint8Array(二进制数据,如音频 PCM) body = decompressedBody; break; case SerializationType.STRING: // STRING 反序列化:UTF-8 解码为字符串(无 JSON 解析步骤) try { body = textDecoder.decode(decompressedBody); } catch (e) { throw new Error(`STRING 反序列化失败:${(e as Error).message}(UTF-8 解码错误,可能是非法字符串数据)`); } break; case SerializationType.JSON: // JSON 反序列化:先 UTF-8 解码为字符串,再 JSON.parse 转为对象/数组 try { const jsonStr = textDecoder.decode(decompressedBody); // 二进制→JSON 字符串 body = JSON.parse(jsonStr); // JSON 字符串→对象/数组 } catch (e) { if (e instanceof TypeError) { throw new Error(`JSON 反序列化失败:${ProtocolConst.STRING_ENCODING} 解码错误(非法 UTF-8 数据)`); } else if (e instanceof SyntaxError) { throw new Error(`JSON 反序列化失败:格式错误(${(e as Error).message}),请检查 JSON 语法`); } else { throw new Error(`JSON 反序列化失败:${(e as Error).message}`); } } break; default: // 此处理论上不会触发(已提前校验序列化方式合法性) throw new Error(`不支持的序列化方式:${SerializationType[serialization]}(值:${serialization})`); } // 6. 构造并返回解包结果(利用枚举反向映射获取名称,便于日志和展示) return { msgType: msgType as MessageType, // 消息类型枚举值 msgTypeName: MessageType[msgType] as keyof typeof MessageType, // 消息类型名称(如 "TEXT_MESSAGE") serialization: serialization as SerializationType, // 序列化方式枚举值 serializationName: SerializationType[serialization] as keyof typeof SerializationType, // 序列化方式名称 compression: compression as CompressionType, // 压缩方式枚举值 compressionName: CompressionType[compression] as keyof typeof CompressionType, // 压缩方式名称 body: body, // 反序列化后的原始业务数据 }; } }