-
Notifications
You must be signed in to change notification settings - Fork 6
jpacks_abc
随着 Web 技术的流行,JavaScript(以下简称 JS) 要处理数据类型也就变得越来越丰富。
仅处理文本数据(如:JSON、XML、YAML)已经不能满足更多市场需求。
现代 JS 引擎均支持 类型数组(typed arrays)
,它提供了一个更加高效的机制来存储原始二进制数据。
用 JS 在前后端生成 GIF 图片、ZIP 压缩包,解析 Word 文档、PDF 设计稿,这类功能变得越来越多。
jpacks 是一套 JS 处理二进制结构化数据组包解包的工具。
我们有一款社交产品,已经投入市场有三年,服务器是用 C++ 编写,客户端有 iOS 和 Android,服务架构、通信协议趋于稳定。 为扩大业务启动了 Web 端项目,前端功能接近 Native,后端则要兼容已有通信数据格式。 Web 实时通信用 WebSocket,评估成本后选用 NodeJS 为后端实现。
实现业务通信协议的时候,问题来了:
- 数据类型多。四十几套通信数据格式(包括请求和应答)
- 数据结构复杂。包括:C 语言结构、ProtoBuf 数据、还有 Gzip 数据、Int64 类型,结构中还有穿插和分支。
在调研已有 JS 处理二进制的开源项目后,并没有找到适合的,所以就自己造这个轮子。
这个项目比较接近我们的需求
优势
- 结构化的声明方式
var playerSchema = new _.Schema({
id: _.type.uint16,
name: _.type.string(16),
hp: _.type.uint24,
exp: _.type.uint32
...
});
不足
- 数据类型缺少太多,不支持 Int64、浮点数、有无符号
- 不支持 UTF-8 字符串编码
- 测试用例不多
- 不容易扩展,Gzip、ProtoBuf 就很难加入
一看这个项目有两年没有更新,就放弃选用。
找到这个项目比较偶然,因为是找 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');
浏览器环境则麻烦一些,得用 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 的示例代码的测试用例和合体的。