使用 jpacks 处理二进制结构化数据
作者:王集鹄 2016年3月15日
随着 Web 技术的流行,JavaScript(以下简称 JS) 要处理数据类型也就变得越来越丰富。
仅处理文本数据(如:JSON、XML、YAML)已经不能满足更多市场需求。
现代 JS 引擎均支持 类型数组(typed arrays)
,它提供了一个更加高效的机制来存储原始二进制数据。
用 JS 在前后端生成 GIF 图片、ZIP 压缩包,解析 Word 文档、PDF 设计稿,这类功能变得越来越多。
jpacks 是一套 JS 处理二进制结构化数据组包解包的工具。
我们有一款社交产品,已经投入市场有三年,服务器是用 C++ 编写,客户端有 iOS 和 Android,服务架构、通信协议趋于稳定。
为扩大业务启动了 Web 端项目,前端功能接近 Native,后端则要兼容已有通信数据格式。
Web 实时通信用 WebSocket,评估成本后选用 NodeJS 为后端实现。
实现业务通信协议的时候,问题来了:
在调研已有 JS 处理二进制的开源项目后,并没有找到适合的,所以就自己造这个轮子。
这个项目比较接近我们的需求
优势
var playerSchema = new _.Schema({ id: _.type.uint16, name: _.type.string(16), hp: _.type.uint24, exp: _.type.uint32 ... });
不足
一看这个项目有两年没有更新,就放弃选用。
找到这个项目比较偶然,因为是找 NodeJS 的 ProtoBuf 模块找到。 bytebuffer 、 long (int64 处理)、 protobufjs 作者都是 dcodeIO
bytebuffer 采用链式调用
var ByteBuffer = require("bytebuffer"); var bb = new ByteBuffer() .writeUint64("21447885544221100") .writeIString("Hello world!") .writeUTF8String("你好世界!") .flip();
优势
不足
c-struct 的声明方式和 bytebuffer 的丰富数据即是我想要的。
我发现无论什么数据类型都离不开两个方法:组包(pack)和解包(unpack)
pack
:将数据转换成二进制 unpack
:将二进制转换成数据 所以就抽象出一个描述数据类型存储规则接口 Schema
interface SchemaInterface { /** * 组包 * @param {Any} value 要转换为二进制的变量 * @return {Array of Byte} 返回该变量二进制数据,即:一段 Byte 数组 */ public function pack(value) {} /** * 组包 * @param {Array of Byte} buffer 二进制数据 * @return {Any} 返回该二进制标示的类型数据 */ public function unpack(buffer) {} }
举个 bool 类型(16 位)的例子:
var bool16Schema = { function pack(value) { return value ? [255, 255] : [0, 0]; } public function unpack(buffer) { return String(buffer) !== '0,0'; } }
为了处理速度,我们得尽量使用 JS 引擎提供的标准接口 DataView 就能处理标准数值类型及其数组。
var buffer = new ArrayBuffer(16); var dv = new DataView(buffer, 0); dv.setInt16(1, 42); dv.getInt16(1); //42
标准数值类型如下
Name | DataView Type | Size | Alias | Typed Array |
---|---|---|---|---|
int8 | Int8 | 1 | shortint | Int8Array |
uint8 | Uint8 | 1 | byte | Uint8Array |
int16 | Int16 | 2 | smallint | Int16Array |
uint16 | Uint16 | 2 | word | Uint16Array |
int32 | Int32 | 4 | longint | Int32Array |
uint32 | Uint32 | 4 | longword | Uint32Array |
float32 | Float32 | 4 | single | Float32Array |
float64 | Float64 | 8 | double | Float64Array |
好在现在 utf-8
大行天下,不用考虑兼容 gb2312
的问题
NodeJS 环境 Buffer
类自带字符集的处理,比较好处理
new Buffer(value, 'utf-8');
浏览器环境则麻烦一些,得用 encodeURIComponent
、 escape
系列处理
字符集
function encodeUTF8(str) { if (/[/u0080-/uffff]/.test(str)) { return unescape(encodeURIComponent(str)); } return str; } function decodeUTF8(str) { if (/[/u00c0-/u00df][/u0080-/u00bf]/.test(str) || /[/u00e0-/u00ef][/u0080-/u00bf][/u0080-/u00bf]/.test(str)) { return decodeURIComponent(escape(str)); } return str; }
接下来只要实现 结构(Struct)
和 数组(Array)
两种重要的类型,基本 80% 的需求就能满足了。
结构类型实现代码:
function objectCreator(objectSchema) { var keys = Object.keys(objectSchema); return new Schema({ unpack: function _unpack(buffer, options, offsets) { var result = new objectSchema.constructor(); keys.forEach(function (key) { result[key] = Schema.unpack(objectSchema[key], buffer, options, offsets); }); return result; }, pack: function _pack(value, options, buffer) { keys.forEach(function (key) { Schema.pack(objectSchema[key], value[key], options, buffer); }); } }); };
unpack()
、 pack()
的 options
参数,用来处理配置项,比如字节序(Endian) unpack()
的 offsets
,用来处理数据起始偏移位置,避免频繁分配内存空间
这是最常见的数据类型,也很容易理解,对应着 JS 的 object
类型。
C 类型定义
#define DEF_NICK_NAME_LEN = 50 struct STRU_USER_BASE_INFO { int64 miUserID; // 用户 ID char mszNickName[DEF_NICK_NAME_LEN + 1]; // 昵称 // utf-8 }
var _ = require('jpacks'); require('jpacks/schemas-extend/bigint')(_); // 引入 int64 扩展 _.def('STRU_USER_BASE_INFO', { // 定义 STRU_USER_BASE_INFO 结构 miUserID: _.int64, mszNickName: _.smallString }); var buffer = _.pack('STRU_USER_BASE_INFO', { // 将变量用 STRU_USER_BASE_INFO 类型组包 miUserID: '20160315005', mszNickName: 'zswang' }); console.log(new Buffer(buffer).toString('hex').replace(/..(?!$)/g, '$& '));
7d fe a5 b1 04 00 00 00 06 00 7a 73 77 61 6e 67
如果声明中的类型是字符串,只有在执行组包和解包函数时才会去实例化。利用这一个特性就声明出递归结构类型。
var _ = require('jpacks'); _.def('User', { age: 'uint8', token: _.array('byte', 10), name: _.shortString, note: _.longString, contacts: _.shortArray('User') }); var user = { age: 6, token: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], name: 'ss', note: '你好世界!Hello World!', contacts: [{ age: 10, token: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], name: 'nn', note: '风一样的孩子!The wind of the children!', contacts: [{ age: 12, token: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], name: 'zz', note: '圣斗士星矢!Saint Seiya!', contacts: [] }] }, { age: 8, token: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], name: 'cc', note: '快乐的小熊!Happy bear!', contacts: [] }] }; // 组包 var buffer = _.pack('User', user); console.log(new Buffer(buffer).toString('hex').replace(/..(?!$)/g, '$& ')); // 06 00 01 02 03 04 05 06 07 08 09 02 73 73 1b 00 00 00 e4 bd a0 e5 a5 bd e4 b8 96 // e7 95 8c ef bc 81 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 02 0a 00 01 02 03 04 05 06 // 07 08 09 02 6e 6e 2c 00 00 00 e9 a3 8e e4 b8 80 e6 a0 b7 e7 9a 84 e5 ad a9 e5 ad // 90 21 54 68 65 20 77 69 6e 64 20 6f 66 20 74 68 65 20 63 68 69 6c 64 72 65 6e 21 // 01 0c 00 01 02 03 04 05 06 07 08 09 02 7a 7a 20 00 00 00 e5 9c a3 e6 96 97 e5 a3 // ab e6 98 9f e7 9f a2 ef bc 81 53 61 69 6e 74 20 53 65 69 79 61 ef bc 81 00 08 00 // 01 02 03 04 05 06 07 08 09 02 63 63 1f 00 00 00 e5 bf ab e4 b9 90 e7 9a 84 e5 b0 // 8f e7 86 8a ef bc 81 48 61 70 70 79 20 62 65 61 72 ef bc 81 00
现在越来越依赖 ProtoBuf(以下简称 PB)做通信协议,因为 PB 有可读性高、空间占用小、跨平台、跨语言的特性。
在 jpacks 中也能方便的使用。
var _ = jpacks; var _schema = _.array( _.protobuf('test/protoify/json.proto', 'js.Value', 'uint16'), // 指定 PB 文件路径,Message 名称,占用大小标记类型 'int8' ); console.log(_.stringify(_schema)) // > array(protobuf('test/protoify/json.proto','js.Value','uint16'),'int8') var buffer = _.pack(_schema, [{ integer: 123 }, { object: { keys: [{ string: 'name' }, { string: 'year' }], values: [{ string: 'zswang' }, { integer: 2015 }] } }]); console.log(buffer.join(' ')); // > 2 3 0 8 246 1 33 0 58 31 10 6 26 4 110 97 109 101 10 6 26 4 121 101 97 114 18 8 26 6 122 115 119 97 110 103 18 3 8 190 31 console.log(JSON.stringify(_.unpack(_schema, buffer))); // > [{"integer":123},{"object":{"keys":[{"string":"name"},{"string":"year"}],"values":[{"string":"zswang"},{"integer":2015}]}}]
数组结构、压缩结构、依赖结构也就不一一赘,感兴趣的同学请到项目的代码中详细了解。jpacks 的示例代码的测试用例和合体的。
最后给出项目地址: jpacks 。
最后给出项目地址: jpacks 。
最后给出项目地址: jpacks 。