# protobuf-playground **Repository Path**: ever-lose/protobuf-playground ## Basic Information - **Project Name**: protobuf-playground - **Description**: protobuf 通信协议 - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-07-08 - **Last Updated**: 2025-07-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 项目启动 安装依赖 ```bash npm install ``` node 脚本测试 ``` node node-test.js ``` 浏览器打开 ``` npm run dev ``` ## protobufjs 使用 以下是一个使用 `protobuf.js` 在 Node.js 环境中运行的完整基础示例,涵盖安装、定义 `.proto` 文件、编译、序列化与反序列化全流程: ### 1. 安装依赖 ```bash # 在项目根目录执行 npm install protobufjs protobufjs-cli ``` ### 2. 定义 `.proto` 文件 创建 `person.proto` 文件,定义数据结构: ```protobuf // proto/person.proto syntax = "proto3"; message Person { string name = 1; int32 id = 2; optional string email = 3; // 可选字段 repeated string phones = 4; // 重复字段(数组) } ``` ### 3. 编译 `.proto` 为 JSON 描述符(避免运行时解析) 使用 `pbjs` 命令行工具将 `.proto` 编译为 JSON 文件: ```bash npm run json # npx pbjs -t json proto/*.proto -o proto/proto.json ``` **作用**: - 生成 JSON 描述符文件,避免在运行时动态解析 `.proto`(提升性能)。 ### 4. 编写启动代码 脚本代码参照 node 启动的 `node-test.js` 和浏览器启动的 `src/index.js` ### 5. 运行代码 ```bash node index.js ``` 输出示例: ``` Serialized Buffer: Deserialized Object: { name: "Alice", id: 123, phones: ["123-4567", "987-6543"] } ``` ### 大整数处理问题思考 大整数处理: 1. 若使用 `int64/uint64`,需配置 `Long` 类型(参考 `long.js` 库) 1. 优点: 与 protobufjs 二进制编码完全兼容, 无需额外转换 API 丰富. 2. 缺点: 增加依赖库 2. 引入 BigInt 类型. 无需额外引入库, 但是兼容性低. 1. 优点: 与 1 类似, 完全兼容, 和后端处理逻辑无需额外转换. 2. 缺点: 比 1 的兼容性更差, 无 polyfill 3. 使用 String 表示 int64 1. 优点: 与开发者直观友好, 兼容性好, 与其他 JS 代码集成无需额外转换. 不丢精度 2. 缺点: 需要在序列化前和反序列化后做额外转换. 也不能做数值运算 4. 在 proto 文件中直接定义为 string 1. 优点: 对前端开发者最友好, 没有歧义, 也不用转换, 完全避免丢精度 2. 缺点: 改变了协议定义, 会影响后端处理逻辑. 推荐选 1, 举个例子, id 和 createTime 都是服务器定义的 Long 类型数据, 传输二进制数据时就占 8 字节. 但是前端处于一些考虑, TS 上层暴露的定义 `{ id: string; createTime: number; }`, 原因如下 1. 因为 id 不需要数值运算, 用 string 表达能避免处理精度问题 2. 而 createTime 作为 unix 时间戳, 达到 Number 的最大值还得几十万年, 自然在此时定义为 number 类型没啥问题. 选 3 的实操, 让 proto 文件定义依旧是 int64 ```proto // person.proto syntax = "proto3"; message Person { string name = 1; int64 id = 2; // 前端为 string,后端为 Long int64 createTime = 5; // 前端为 number,后端为 Long } ``` 而处理代码为 ```js // 3. 准备数据 - 在验证阶段使用 Long const personDataRaw = { name: "Alice", id: "123456789012345", // 原始字符串格式 phones: ["123-4567", "987-6543"], createTime: Date.now() // 数字时间戳 }; // 4. 处理数据,将字符串 id 转换为 Long,以通过验证 const personData = { ...personDataRaw, id: Long.fromString(personDataRaw.id), // 使用 Long 表示 id createTime: Long.fromNumber(personDataRaw.createTime) // 使用 Long 表示时间戳 }; // 不会遇到阻碍...如果 id 此时传入字符串, 那么 verify 一定会报错要求 number const errMsg = Person.verify(personData); if (errMsg) throw new Error(errMsg); // 编码 const buffer = Person.encode(person).finish(); // 解码 const decoded = Person.decode(buffer); // 现在可以安全地使用 id 和 createTime,它们都是字符串类型 const decodedObj = Person.toObject(decoded, toObjectOptions); // 确保createTime是数字类型 decodedObj.createTime = Number(decodedObj.createTime); console.log(decodedObj); // { // "phones": [ // "123-4567", // "987-6543" // ], // "name": "Alice", // "id": "12337581991284842", // "createTime": 1751954336606, // "email": "chentongta@qq.com", // "_email": "email" // } ``` ### protobufjs 是如何把 number 类型数据转化 int32 所对应的二进制数据格式的 当编码 int32 字段时,protobuf.js 首先将 JavaScript 的 number 值转换为 32 位有符号整数,然后使用 varint 编码格式写入二进制数据 。 int32 类型使用 varint 编码(wire type 0),这是一种变长编码格式 : * 对于小于 128 的值,只需要 1 个字节 * 对于较大的值,每个字节使用 7 位存储数据,最高位作为继续标志 * 采用小端序(低位字节在前) int32 类型最终的编码范围. * 值 < 128(2^7):1 字节 * 值 < 16384(2^14):2 字节 * 值 < 2097152(2^21):3 字节 * 值 < 268435456(2^28):4 字节 * 其他:5 字节 核心代码如下 ```js Writer.prototype.uint32 = function write_uint32(value) { // here, the call to this.push has been inlined and a varint specific Op subclass is used. // uint32 is by far the most frequently used operation and benefits significantly from this. this.len += (this.tail = this.tail.next = new VarintOp( (value = value >>> 0) < 128 ? 1 : value < 16384 ? 2 : value < 2097152 ? 3 : value < 268435456 ? 4 : 5, value)).len; return this; }; function VarintOp(len, val) { this.len = len; this.next = undefined; this.val = val; } VarintOp.prototype = Object.create(Op.prototype); VarintOp.prototype.fn = writeVarint32; // 每 7 位印到这个字节里, 然后继续印下一个 7 位到下一个字节里, 循环下去. function writeVarint32(val, buf, pos) { while (val > 127) { buf[pos++] = val & 127 | 128; val >>>= 7; } buf[pos] = val; } ``` ### 附数据类型 | Protobuf 类型 | JavaScript 类型 | 说明 | | :--- | :--- | :--- | | int32, sint32 | number | 32位整数(sint32 对负数编码更高效) | | int64, sint64 | Long 或 BigInt | 大整数需用 Long 库处理(如 protobufjs 内置 Long 类型) | | uint32, fixed32 | number | 无符号32位整数(fixed32 固定4字节) | | uint64, fixed64 | Long 或 BigInt | 无符号大整数 | | float, double | number | 单/双精度浮点数 | | bool | boolean | 布尔值 | | string | string | UTF-8 字符串 | | bytes | Uint8Array/Buffer | 二进制数据(Node.js 用 Buffer,浏览器用 Uint8Array) |