338 lines
16 KiB
TypeScript
338 lines
16 KiB
TypeScript
// 导入 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, // 反序列化后的原始业务数据
|
||
};
|
||
}
|
||
} |