- version1: 完成json转db 再读取回json
- version2: 新增jszip功能 压缩测试 方案分析;重复字符串测试;读写测试;
- version3: 完成基于jszip的整体实现 简化2中的无用内容
- Buffer Node.js中特有 处理二进制数据流
Buffer.alloc(9)
Buffer.from([1,3,4])
Buffer.from("hello", "utf8")
- ArrayBuffer
- es6标准 二进制数据缓冲区 本身不提供读写方法 需要通过类型化数组-Uint8Array之类 DateView
new ArrayBuffer(10) 10字节大小
- Uint8Array 一种类型化数组视图 初始化为0
new Uint8Array(9)
new Uint8Array([1,3,4])
new Uint8Array(arraybuffer)
- 互转
Buffer => ArrayBuffer Uint8Array
buf = Buffer.from([1,3])
arrBuf = buf.buffer.slice(buf.byteOffset, buf.byteOffset+buf.byteLength)
u8arr = new Uint8Array(buf)
ArrayBuffer Uint8Array => Buffer
ab = new ArrayBuffer(4)
u8arr = new Uint8Array(ab)
buf = Buffer.from(ab)
buf = Buffer.from(u8arr)
- heads.json 表头信息:所有表名、表头、表头类型、双键映射(多列映射为id)、总表数
tables:["activity", "skill"]
heads: [
["id", "name", "time"],
["id", "name", "effect"]
]
heads_type: [ -2:int 1:string 2:json;
int 默认是 -2 string 默认是""" json 默认 是 null
[-2, 1, 0],
[-2, 1, 2],
double_keys:[ 列的约定在导表工具中 这里并不知道 只有做业务的人清楚
"beatGame_treasure":{"999_1":1,"1001_2":2,
"rule_worldSkill":{"-1_0":1000,"1_1":1001,
]
"file_num":2 有几个data文件 浏览器限制了5M的大小 超过后不缓存 每次都下载 小游戏是否也会?
- data-1.json data2.json
values:[
[ 表-1的数据
[-1, "name1", 1000],
[0, "name2", 2000],
],
]
heads.json data-1.json data2.json
原始大小:143k 4.34M 2.5M
第一版: 37.4k 4.01M 2.28M .db
格式:
head.db
head_info: 表头信息部分
version: uint32 20240626
file_num: uint8
tables_num: varint
for tables:
name: utf6string
headdata_off: varint head_data数据块中的偏移 将head和head_type单独存储 减少预解析量
bodydata_off: varint body.db数据块中的偏移
bodydata_index: uint6 第几个data文件
head_data: 表头数据部分
for table1 headDataOffset 每个表的数据偏移量
heads_num: uint8 列数
heads: [utf8string, ...]
heads_type: [uint8, ...]
data*.db
body_data-1: 表内容数据
version: uint32 20240626
body_table_num: varint 这个data*.db中包含的表数量
for
table-1:
double_keys num: varint
for
[<utf6string, varint>, ...] 映射对 {key:value, ...}
values_num: varint 某个表的数据行数
根据heads_type读取内容 0:int 1:string 2:json 3:float 4:any
for rows
for columes
[ [varint, string, ...], [...], ...] json采用字符串方式存储和解析
heads.json data-1.json data2.json
原始大小:146k 4.34M 2.5M 总6.99 zip:762K
第一版: 24k 1.17M 765K string.db2.12M 总:4.07M zip:897K
- 新增string.db
- 出现的字符串全部替换为varint-偏移值
- 读取数据时 若head_type为string 则动态从strings.db中解析出字符串
json:88ms
db:244ms
分析原因:
json整体字符串一起分析
二进制根据表逐个解析 逐条解析内容; 流程更复杂 所以更久
测试head head_type有没必要转二进制
大小 写入输入 读取+解析
json:180k 2ms 0ms
db: 146k 11ms 23ms
-结论:除了大一点,若整体保存和解析,则json完胜
- db的使用场合:需要动态解析时,保持内存一块很小的部分,再根据需要解析出具体某部分数据
str = "hello世界".repeat(1000);
原始txt 11000字节
writeutf8str 11002字节
解释:
二进制方式多了一个int16的长度
原始txt方式 估计是一直到文件结尾 所以少2字节
"hello世界123456789"
json:20bit
db:17bit
分析:字符串部分 db多2字节
数字部分 db 4字节 原始:9字节 少5字节
所以总db少3字节
str = "hello世界123456789".repeat(100);
byte.writeInt32(123456789)
原始txt 2000字节
writeutf8str writeInt32 1502字节
分析:
123456789 字符串方式 9字节; int32:4字节
5*100-2 = 498
结论:只要字符串长度超过8字节 就能省内存;反之,原始串更小
有多少重复的字符串? 重复的部分有多少是夸文件的?
- 数据量对比
全部大小:6.06M
去重大小:2.24M
根据数据抓取重复字符 安装次数 占的表数 排序后:
"null": 33944 几十个表
"":空字符串 24984 几十个表
{ count: 11520, str: '可在套装界面内装备', desc: [ 'suit_suitEquip' ] },
{ count: 3773, str: 'add_buff', desc: [ 'skill_effect' ] },
{ count: 2877, str: 'change_attr', desc: [ 'skill_buff' ] },
类似的还有name id
...
{
count: 470,
str: '["云游寻宝","sys_treasure",[0],31]',
desc: [
'backgroundActive_activeTask',
'backgroundActive_battlePassTask',
'backgroundActive_bgActive3Task',
'openActive_activeTask',
'return_returnTask',
'slg_activeBpTask'
]
},
...
{ count: 279, str: 'buff/+shanghai.png', desc: [ 'skill_buff' ] },
...
{ count: 144, str: '麻痹项链', desc: [ 'suit_suitEquip' ] },
结论:
只有少量特殊字符串 才会覆盖大量的表
大部分情况都在某个表内部 才会出现大量重复
- 方案:字符串去重 根据表名拆分;方便动态读取和解析;方便使用jszip库
- 方案1:
所有的单个表数据 独立zip压缩 就不用在游戏中加载大的buffer
只需在需要时 动态解析出数据内容
包含两块数据:head_data body_data
- jszip jszip 只支持文件级别的压缩和解压;适合对整个db文件压缩 换个思路:每个表头或表内容 都当做一个内部文件 通过zip.file(name).async("string/uint8array")来动态得到解析的内容
- 格式定义1
head.zip
count varint
[<name:{bodyidx,data_off}>}
data1.zip data2.zip 每个表单独一个string块 用于去重 放到表数据后
某一个表的数据 通过data_off来读取这个表的所有内容
double_key_count 多列映射数
for
key: utf8str
id: any 可能是number或字符串 看表的配置 但肯定是第一列
row_count 数据行数
for 根据head_type写入数据
[id, name, ....]
string_buffer: 上面用的所有字符串 都在这个块内 通过offset读取utf8string
zipFile.file("name").async("nodebuffer");
- 再次优化double_key: 没有采用二进制解析的必要,已经走了zip,所以真快使用json字符串
- data部分:本身就想只解析某一条,像skill类大表就没必要全部解析到json内存中,所以通过二进制方式存储
- 格式定义2
heads.zip
count varint
[<name:{bodyidx, body_off, data_off, str_off}>}
data1.zip data2.zip 每个表单独一个string块 用于去重 放到表数据后
某一个表的数据 通过data_off来读取这个表的所有内容
:body_off
head: 表头名称 ["id", "name", "title", ...] json string
head_type: 表头类型 [number, string, ...] json string
row_count 数据行数 varint
data_line_size 单行大小 varint
ids:[id1,id2] json string id=>行索引*行大小=偏移=>读取这个表的某一行数据
double_key: utf8string 可能是空字符串 若没有映射表
for 根据head_type写入数据 :data_off
[id, name, ....]
string_buffer: 上面用的所有字符串 都在这个块内 通过offset读取utf8string :str_off
zipFile.file("heads").async("nodebuffer");
- 再次优化double_key: 没有采用二进制解析的必要,已经走了zip,所以真快使用json字符串
- 根据整体压缩效果900k以内 所以不再区分多个data文件 所有文件都在一个压缩包中
- 省去head 也放入table中
- 格式定义3
data.db
table1 全部以单独的表压缩保存
table2
...
table 每个表单独一个string块 用于去重 放到表数据后
某一个表的数据 通过data_off来读取这个表的所有内容
head: 表头通用信息 {row_count, line_data_size, data_off, strblock_off} json string
row_count 数据行数
data_line_size 单行大小
head_title: 表头名称 ["id", "name", "title", ...] json string
head_type: 表头类型 [number, string, ...] json string
ids:[id1,id2] json string id=>行索引*行大小=偏移=>读取这个表的某一行数据
double_key: json string 可能是null 若没有映射表
:data_off 数据块起始
for 根据head_type写入数据
[id, name, ....]
:strblock_off
string_buffer: 上面用的所有字符串 都在这个块内 通过offset读取utf8string
zipFile.file("heads").async("nodebuffer");
- 测试结果
data.zip
压缩等级9: 1.26M 带names表
压缩等级1: 1.39M
时间:
read json:86ms
read .zip+load bufer 14ms
read one table:5ms
parse one line:1ms
parse all db: 6ms 都是打log的时间 去除后 1ms
- 遗留问题: 读表函数是异步的 无法和业务协调使用 解决:使用早期的jszip库2.6.1 使用同步方式读写
使用同步库2.6.1版本 整理第三版代码 删除无关函数 整理实现接口:方便使用和查看内存情况 开发模式多names表 用开关控制
- 方案
由于jszip按文件目录压缩 第二版中的按照偏移方案来解析
数据结构重新设计 全部按表名映射:head head_type double_key body_data
怎样只解析某一行数据 而非整个表?
第一层解析:
通过fs/http加载.zip文件 得到arraybuffer
jszip加载这个buffer 等待解析
第二层解析:
根据表明解析表相关信息
jszip.file(name).async("arraybuffer").then(u8)
byte = new Byte(Buffer.from(u8))
解析表头:
head_info: {row_count, name}
head_title: ["id", "name", ...]
head_type: [0, 1, ...]
double_keys: {key:id, 11_0:1} 多列合并值到id的映射
ids id在body_byte中的偏移 {1:0, 2:8} id到具体行数据的偏移
data_byte: Byte
string_block:Byte
第三层解析:动态解析数据
若通过id方式获取数据 就只解析这一行的数据
若getArray方式 一次更加ids 解析出所有内容
head_title head_type ids => {id:{id:1,name:"",...}}
- 测试结果
原始游戏项目:
下载json:219ms
解析内容:34ms
占内存:>20M
二进制:
read json:86ms
read .zip+load bufer 86ms 比异步的慢很多 估计是一次解析了所有文件
read one table:3ms 这个变快了 正常:表文件先解析了一部分
parse one line:0ms
parse all db: 1ms
内存:等接入游戏后查看
- 结论:
- 除了下载+加载需要一点时间 动态读取的速度很快
- 可以不用做缓存了
- 大小从3个文件的6.99M到单db文件的1.27M,只有原来的18.1%
- 之前版本遇到的问题:
- 对excel表头有严格要求 尤其float类型 json不能混用 需要用any等
- 对excel的单元格格式有要求 尤其看着是数字 导出变成了字符串 需要到表里点击修复为数字
- float的打包和解包不精准 默认DateView.setFloat32 getFloat32有精度问题;msgpack5的库倒是没这个问题
- 语言包问题:现在字符串全部压缩在单表内,怎么翻译?怎么支持多语言?
- 原方式:提取所有表的中文,翻译为目标语言-单独保存语言文件ko_excel.json
excel_head_lang.json "activity": ["name","showStr"],
ko_excel: "activity":{"1":["일일 패키지","종료됨"],"1001":["크로스서버 보물암","종료됨"]
dbmgr.get(name, id)
sg.langMgr.translateExcelLine(name, id, line, noTrans);
=>this._excelLangPack.translateLine(name, id, line, noTrans);
let head = this._headData[name]; 对应excel_head_lang.json
for (let i = 0; i < head.length; i++) {
let column = head[i];
line[column + "_cn"] = line[column];//记下中文
line[column] = langData[i];
缺点:其他语音也会包含中文内容
优点:
一包可切任何语言;
采用表列替换的方式,而非中文映射,更精准,效率高
- 解决:
- 表的行数据 不再采用二进制方式(好像也没省什么空间) 直接采用json转utf8str方式
- 评估是否需要字符串缓存 因为改为json后 解析后就直接可以用了;如果还需要去重,得根据head_type重新赋值
- 语言包:
- 翻译提取:不变 还是得到ko_excel.json文件
- 转为新的db格式,按表名做zip包的文件,内容还是json字符串
- 游戏动态翻译时,只需修改langData的获取方式即可
- 测试结果
原始游戏项目: 6.99M 全部压到zip后762k
下载json:219ms
解析内容:34ms
占内存:>20M
二进制: data.db 1.27M
read json:86ms
read .zip+load bufer 86ms 比异步的慢很多 估计是一次解析了所有文件
read one table:3ms 这个变快了 正常:表文件先解析了一部分
parse one line:0ms
parse all lines: 1ms
内存:等接入游戏后查看
json方式的二进制: data.zip 1.17M
read json:85ms
read .zip+load bufer 87ms 比异步的慢很多 估计是一次解析了所有文件
read one table:2ms 这个变快了 正常:表文件先解析了一部分
parse one line:0ms
parse all lines: 2ms
load all tables time 731
skill_skill 47
monster_monster 40
内存:等接入游戏后查看
- 结论:
- 除了下载+加载需要一点时间 动态读取的速度很快
- 可以不用做缓存了
- 大小从3个文件的6.99M到单db文件的1.27M,只有原来的18.1%
- string缓存去重测试- 废弃不用-不值当-还要担心json数据的内容被乱用成:int 普通string导致解析报错
将字段中的string和json格式 用offset方式保存 解析后再重新根据偏移读取
开启:1.27M
关闭:1.17M
意外:反而变大了
原因:
zip本身有去重功能
转offset保存后 反而多了很多int32来记录偏移值
案例:解压zip文件夹
suit_suitEquip 非缓存:1.94M 缓存:902k => zip 129K 147k反而更小了-说明重复部分全被压缩了
skill_skill 1.05M 0.97M
数据大的原因:焰魔胸针 脱胎火狱,燃烧一切的胸针 有十几条 大量重复
- 解析ids时json.parse报错 查看发现字符串不完成
对比写入时的情况 内容完整 转json后长度为82119
读取时长度变成了16583 内容被截断不再完整
分析:
82119
0001 0100 0000 1100 0111
16583
0100 0000 1100 0111
writeUTFString 使用int16做为长度位 导致数据丢失
解决:使用writeUTFString32
扩展:除了ids 是否还有字符串长度会超过?
double_keys ids