Files
aistream-test/测试流式传输uniapp/pages/index/ProtocolCodec.ts
T
2025-12-01 21:00:54 +08:00

338 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 导入 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、undefinednull/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, // 反序列化后的原始业务数据
};
}
}