转载

使用 jpacks 处理二进制结构化数据

使用 jpacks 处理二进制结构化数据

背景

作者:王集鹄 2016年3月15日

随着 Web 技术的流行,JavaScript(以下简称 JS) 要处理数据类型也就变得越来越丰富。

仅处理文本数据(如:JSON、XML、YAML)已经不能满足更多市场需求。

现代 JS 引擎均支持 类型数组(typed arrays) ,它提供了一个更加高效的机制来存储原始二进制数据。

用 JS 在前后端生成 GIF 图片、ZIP 压缩包,解析 Word 文档、PDF 设计稿,这类功能变得越来越多。

jpacks 是什么?

jpacks 是一套 JS 处理二进制结构化数据组包解包的工具。

设计 jpacks 初衷是什么?

我们有一款社交产品,已经投入市场有三年,服务器是用 C++ 编写,客户端有 iOS 和 Android,服务架构、通信协议趋于稳定。

为扩大业务启动了 Web 端项目,前端功能接近 Native,后端则要兼容已有通信数据格式。

Web 实时通信用 WebSocket,评估成本后选用 NodeJS 为后端实现。

实现业务通信协议的时候,问题来了:

  1. 数据类型多。四十几套通信数据格式(包括请求和应答)
  2. 数据结构复杂。包括:C 语言结构、ProtoBuf 数据、还有 Gzip 数据、Int64 类型,结构中还有穿插和分支。

在调研已有 JS 处理二进制的开源项目后,并没有找到适合的,所以就自己造这个轮子。

设计过程

竞品调研

c-struct

这个项目比较接近我们的需求

优势

  • 结构化的声明方式
var playerSchema = new _.Schema({   id: _.type.uint16,   name: _.type.string(16),   hp: _.type.uint24,   exp: _.type.uint32   ... }); 

不足

  • 数据类型缺少太多,不支持 Int64、浮点数、有无符号
  • 不支持 UTF-8 字符串编码
  • 测试用例不多
  • 不容易扩展,Gzip、ProtoBuf 就很难加入

一看这个项目有两年没有更新,就放弃选用。

bytebuffer

找到这个项目比较偶然,因为是找 NodeJS 的 ProtoBuf 模块找到。 bytebuffer 、 long (int64 处理)、 protobufjs 作者都是 dcodeIO

bytebuffer 采用链式调用

var ByteBuffer = require("bytebuffer"); var bb = new ByteBuffer()     .writeUint64("21447885544221100")     .writeIString("Hello world!")     .writeUTF8String("你好世界!")     .flip(); 

优势

  • 数据类型丰富,支持 Int64、Float64、Float32
  • 支持 UTF-8、Base64 字符串编码
  • 高低字节序(Big and little endianness)
  • 文档、测试用例齐全
  • 具备实战,有多个开源项目依赖
  • 项目近一个月有更新

不足

  • 结构不容易复用;
  • 功能不容易扩展。

调研结论

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'); 

浏览器环境则麻烦一些,得用 encodeURIComponentescape 系列处理

字符集

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 } 

使用 jpacks 处理二进制结构化数据

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 

Protocol Buffer

现在越来越依赖 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 。

参考资源

  • DataView
  • JavaScript typed arrays
  • ProtoBuf
原文  http://div.io/topic/1632
正文到此结束
Loading...