pbai 发表于 昨天 06:54

PBIDEA:用 uo_bson 做 BSON 二进制序列化全解(PB10 基准 · PB12.5 实测)

PBIDEA:用 uo_bson 做 BSON 二进制序列化全解(PB10 基准 · PB12.5 实测)


阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(PBIDEA 组件 uo_bson / uo_bson_iter / uo_json / uo_string / uo_crypto,取自 websuite.pbl;两版实机读数一致,仅个别损坏通道的垃圾值渲染不同,见坑 6)
2. 支持数据库:本文不依赖数据库
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库(PbIdea.dll、Pbidea_cs.dll),工程需挂接 websuite.pbl
4. 难度系数:★★★☆☆
5. 其它阅读说明:建议先读《PBIDEA:用 uo_json 的 set/get 与路径语法操作 JSON》;本文全部读数来自双版本实机探测(PB10 与 PB12.5 各编译并运行一整套探针),凡未实测或两版表现相同的结论都会注明


一、BSON 是什么,PB 为什么需要它

JSON 是文本格式,好读但有两个天生短板:体积大(数字、日期全变成字符),类型丢失(读回来全是字符串,得自己猜)。BSON(Binary JSON)是 JSON 的二进制形态:MongoDB 的存储与网络格式,保留每个字段的类型(int32、int64、decimal128、布尔、UTC 毫秒时间、二进制块、嵌套文档、ObjectID),体积更小、解析更快。

PbIdea 把这套能力封装成了 uo_bson:把 PowerScript 的变量组织成一个 BSON 文档,然后可以——


[*]存成二进制文件或 blob(结构化缓存、进程间传数据块);
[*]与 uo_json 互转(同一份数据两种形态);
[*]按类型读回每个字段,带嵌套文档与路径取值;
[*]转成 Base64 挂进文本协议。


配套还有一个迭代对象 uo_bson_iter,负责枚举文档里的每个节点。这两个对象都不需要数据库、不需要网络,纯内存操作,拿来就能测。

二、对象与 API 全貌

uo_bson 有效声明 52 条(与 uo_bson_iter 合计 54 条),按用途分组如下:


分组方法说明
生命周期SetOption(name,value)全局选项,常用 update=yes/no
写入Set(key, any) / Set(key, any, int)三参版仅对 blob 生效,指定二进制子类型
读值Get(key, ref 类型)重载覆盖 string/int/uint/long/ulong/longlong/double/decimal/real/boolean/blob/date/datetime/any/uo_bson/uo_json
路径读值Get(路径, ref 类型)路径必须以 / 开头,见第六节
转换ToBlob() / ToBase64() / Load(blob) / Load(文件) / Save(文件)二进制往返
JSON 互转ToJson(uo_json) / FromJson(uo_json)与 uo_json 双向
ObjectIDSetObjectID(key) / GetObjectID(key, ref string)自动生成 24 位十六进制
遍历BeginIter / NextIter / EndIter / VisitAll(iter{,flags})配合 uo_bson_iter
其它Clear()清空全部节点


uo_bson_iter 的公开实例变量(实测可读):key(键名)、bson_type(BSON 原始类型码)、pb_type(PB 类型码)、type_name(PB 类型名)、depth(VisitAll 时的嵌套深度)、parentkey、datacount、data1~data3。

三、最小可运行示例

下面这段可以直接放进应用或测试窗口运行,结尾 MessageBox 输出实测结果:


// 输入值定义(前段统一声明并赋值)
stringls_name      // 要写入的用户名
int   li_age       // 要写入的年龄
decimal ldc_fee      // 要写入的金额(用 decimal 变量,原因见第五节)

ls_name = "tom"
li_age= 42
ldc_fee = 2.5

// 创建文档并写入
uo_bson lb
lb = create uo_bson
lb.Set("name", ls_name)
lb.Set("age", li_age)
lb.Set("fee", ldc_fee)

// 按类型读回
stringls_r1
int   li_r2
stringls_r3
ls_r1 = "~x"
li_r2 = 0
ls_r3 = "~x"
lb.Get("name", ls_r1)
lb.Get("age", li_r2)
lb.Get("fee", ls_r3)   // 小数节点用 string 通道读,最可靠
destroy lb

MessageBox("BSON 往返", "name=" + ls_r1 + " age=" + string(li_r2) + " fee=" + ls_r3)
// 实测输出:name=tom age=42 fee=2.5(PB10 与 PB12.5 一致)


注意:uo_bson 不是 autoinstantiate,create 后直接 Set 即可,不需要调用内部初始化函数(bsonCreate/bsonDestroy 是 private 声明,官方示例也是 create 后直接用)。

四、类型映射总表(实测)

同一个文档里放九种类型的字段,用迭代器读 type_name / bson_type / pb_type,两版结果一致:


写入的 PB 值type_namebson_typebson 类型pb_type
"x"(string)string2UTF86
7(int 字面量)long16INT322
3000000000(longlong 变量)longlong18INT6420
2.5(double 字面量)decimal19DECIMAL1285
2.5(decimal 变量)decimal19DECIMAL1285
true(boolean)boolean8BOOL7
blob("AB")blob5BINARY11
datetime 值datetime9DATE_TIME14
子文档(uo_bson)uo_bson3DOCUMENT23


两个要点:一是 double 和 decimal 都被存成 DECIMAL128(转 JSON 时会带「$numberDecimal」标记);二是超范围的数字字面量直接 Set 会按 int32 截断——实测 Set("k", 3000000000) 后字符串通道读回「-1294967296」(低 32 位回绕),必须先赋给 longlong 变量再 Set,读回才是 3000000000。

五、读写通道矩阵:哪些通道能信,哪些不能

这是本文最值钱的一节。全部通道逐个探测后,按「能不能正确读回写入值」分成三类:

可靠的通道:


[*]string 节点:Get(ref string) 正确;
[*]int32 节点:Get(ref int / long / ulong / uint) 都正确(longlong 收不了 int32 节点,返回 false);
[*]int64 节点(用 longlong 变量存):Get(ref longlong) 正确;
[*]blob 节点:Get(ref blob) 正确,字节原样返回;
[*]子文档 / 子 JSON:Get(key, ref uo_bson / uo_json) 正确。


只有字符串通道可靠的: 小数。无论用 double 字面量还是 decimal 变量写入(都存成 DECIMAL128),Get(ref double / real / decimal) 读回全是垃圾值(实测 double 通道得到 -9.223372036854776E+17,即 int64 最小值的百万吨级误读;decimal 通道也乱,两版乱法不同)。Get(ref string) 读回「2.5」永远正确。结论:小数一律用字符串通道读,要参与计算再 Dec/Double 转回来。

读不回的(写入成功、读回恒错):


[*]boolean:Set(true) 后 Get(ref boolean) 返回 true(成功标志)但值是 false,字符串通道也读出「false」。存布尔请用 int 的 1/0 代替;
[*]datetime:Set(datetime) 后 Get(ref datetime) 值不变、字符串通道读出「0323-06-27 23:47:09」这类乱值。替代方案实测有效——把 UTC 毫秒数用 longlong 变量存,字符串通道能原样读回 1792715405000;或直接存「2026-10-23」这种格式化字符串(字符串通道往返无损);
[*]any:Get(key, ref any) 直接失败(返回 false,运行期报 invalid ref data type)。帮助注释里「用 any 接收再用 classname 判断类型」这条路实测不通,类型判断请用迭代器的 type_name。


六、中文:两条实测通道

这是 PB 圈用 BSON 最容易翻车的地方。直接 Set 中文字符串:存储层会把 GBK 串转成 UTF-8 字节存进去(Base64 解出来确实是 UTF-8 的「中文」六个字节),但 Get(ref string) 把这六个 UTF-8 字节原样塞回 PB 字符串,按 ANSI 显示就是三个乱码字符,len 变成 3。Get(ref blob) 对字符串节点则直接返回 false。

两条正解(两版实测,读回码点 20013、25991,即「中文」逐字正确):


// 通道一:全程走 blob(推荐,写入读取都自己控制编码)
uo_bsonlb
uo_crypto lcr
string   ls_cn      // 要存入的中文
blob   lb_bin       // UTF-8 字节
string   ls_back      // 读回的中文

ls_cn = "中文"
lcr = create uo_crypto
lb_bin = lcr.ToUTF8(ls_cn)      // GBK 串转 UTF-8 字节
lb = create uo_bson
lb.Set("c", lb_bin)             // blob 节点,原样存储
blob lb_r
lb.Get("c", lb_r)               // blob 通道读回 6 字节
ls_back = lcr.FromUTF8(lb_r)    // UTF-8 字节转回 GBK 串
destroy lcr
destroy lb
MessageBox("中文往返", ls_back)// 实测输出:中文



// 通道二:已经用字符串写入了,读取侧补救
// 输入值定义(前段统一声明并注释)
string ls_raw      // Get(ref string) 拿到的「乱码」串,其字节就是 UTF-8 原文
uo_string ls_u
uo_crypto lcr
blob   lb_tmp
string ls_back

ls_u = create uo_string
lcr = create uo_crypto
ls_u.Clear()
ls_u.Append(ls_raw)             // 交给 uo_string 管理
lb_tmp = ls_u.ToAnsi()          // 还原成原始字节(6 字节)
ls_back = lcr.FromUTF8(lb_tmp)// 按 UTF-8 解回「中文」
destroy ls_u
destroy lcr
MessageBox("补救通道", ls_back)


能用通道一就别用通道二:让 BSON 里存 blob 节点,语义也和 MongoDB 的二进制字段对得上。

七、嵌套文档与路径取值

子文档和子 JSON 都可以直接 Set 进去,读回也支持「路径」写法。注意实测结论:路径必须以 / 开头,不带斜杠的一律失败(官方示例页用的也是带斜杠的写法):


// 演示:子文档写入与路径读取(输入值前段定义)
string ls_city      // 子文档里的城市
int    li_zip         // 子文档里的邮编

ls_city = "sh"
li_zip= 1

uo_bson lb_addr
lb_addr = create uo_bson
lb_addr.Set("city", ls_city)
lb_addr.Set("zip", li_zip)

uo_bson lb_root
lb_root = create uo_bson
lb_root.Set("name", "tom")
lb_root.Set("addr", lb_addr)    // 子文档直接作为值传入

string ls_v1
int    li_v2
ls_v1 = "~x"
li_v2 = 0
lb_root.Get("/addr/city", ls_v1)   // 带 / 的路径:成功,ls_v1 = sh
lb_root.Get("/addr/zip", li_v2)    // 成功,li_v2 = 1
destroy lb_root
destroy lb_addr

MessageBox("路径取值", ls_v1 + " " + string(li_v2))


不带斜杠的 Get("addr/city", ...) 返回 false。另外嵌套层级不限,"/bson/json/msgid" 这种两三层路径官方示例里就在用。

八、遍历:BeginIter 不定位,正确惯用法是这样

BeginIter 只是初始化迭代器,并不会定位到第一个节点——直接在循环里读字段,第一轮拿到的是全空(key 为空串、get 返回 false)。正确惯用法是 begin 之后再 next:


// 输入值:一个已建好的文档 lb(省略创建代码)
string ls_out      // 汇总输出
int    li_n          // 计数

ls_out = ""
li_n = 0

uo_bson_iter lit
lit = create uo_bson_iter
lb.BeginIter(lit)
do while lb.NextIter(lit)      // 先 next 再读,第一轮就是真实第一个节点
    string ls_val
    ls_val = "~x"
    lb.Get(lit, ls_val)      // 用迭代器读当前节点
    ls_out = ls_out + lit.key + "=" + ls_val + " type=" + lit.type_name + " "
    li_n = li_n + 1
    if li_n > 100 then exit    // 防御性上限
loop
lb.EndIter(lit)
destroy lit

MessageBox("遍历结果", string(li_n) + " 个节点:" + ls_out)


VisitAll 是另一种一次走完的方式,但它要求迭代子是「你的子类」——基类 on_iter 事件默认返回 false(false=继续遍历,true=中断,注释原话如此,别记反)。子类里重写 on_iter 做你想做的事(比如往实例变量里攒日志):


// 自定义迭代子(global type nvo_myiter from uo_bson_iter)
event type boolean on_iter();
// depth 在 VisitAll 时会填充,NextIter 循环里恒为 0
slog = slog + key + "/" + type_name + "/" + string(depth) + " "
return false   // false = 继续
end event


两个实测细节:VisitAll 的返回值没有「完成」语义(不管走没走完都返回 false,等于最后一次 on_iter 的返回值),别拿它判断成功;VisitAll 会下钻嵌套文档(depth 逐层加一),BeginIter/NextIter 循环不会下钻,遇到子文档节点 type_name 是 uo_bson 就停。

九、ObjectID、选项与二进制子类型


// ObjectID:SetObjectID 自动生成 24 位十六进制,每次实例不同
uo_bson lb
lb = create uo_bson
lb.SetObjectID("_id")         // 生成并挂到 _id 键
string ls_oid
ls_oid = "~x"
lb.GetObjectID("_id", ls_oid)   // 读回,len=24
lb.Set("k", 1)
uo_json lj
lj = create uo_json
lb.ToJson(lj)                   // JSON 形态:{"_id":{"$oid":"6ada..."},"k":1}
destroy lj
destroy lb
MessageBox("ObjectID", ls_oid)


SetOption("update","no") 之后,对同一个键重复 Set 会静默忽略——返回 true 但值还是旧的(默认 yes 会真正更新,代价是多一次存在性检查)。文档几十个字段、一次性写入的场景可以关掉 update 换性能;边建边改的场景保持默认。

blob 的三参 Set 可以指定二进制子类型(0x00 通用二进制、0x04 UUID 等),转 JSON 时会带 subType 标记:实测 Set("b4", lbv, 4) 后 ToJson 得到("b4" 节点 subType 为 "04"),blob 字节原样进原样出。

十、文件与 Base64 往返


// 输入值:文档 lb 已按第三节建好
boolean rb1, rb2, rb3, rb4
blob   lb_bb
string ls_b64

rb1 = lb.Save("demo.bson")      // 存文件,实测 28 字节(两字段文档)
rb2 = lb.Load("demo.bson")      // 从文件读回
lb_bb = lb.ToBlob()             // 转成 blob
ls_b64 = lb.ToBase64()          // 转成 Base64 文本
rb3 = lb.Load(lb_bb)            // blob 读回
rb4 = lb.Load("nosuch.bson")    // 文件不存在:返回 false,不抛错
MessageBox("往返", string(rb1) + string(rb2) + string(rb3) + string(rb4) + " b64=" + ls_b64)


Load 一个坏 blob(内容不是合法 BSON)也只返回 false。Clear() 之后文档回到空态(ToBlob 得到 5 字节的空文档头),所有 Get 返回 false。Base64 串可以安全地塞进 JSON、HTTP 头或日志里 transport BSON。

十一、坑清单(全部双版本实测)


[*]数字字面量直接 Set,超 int32 范围会被截断(3000000000 读回 -1294967296),必须先赋 longlong 变量;
[*]小数节点 Get double / real / decimal 全是垃圾值,只有 Get string 可靠;
[*]double 与 decimal 存进去都是 DECIMAL128,转 JSON 带「$numberDecimal」,对端解析时注意;
[*]boolean 写 true 读回恒 false,用 int 1/0 代替;
[*]datetime 写入读回乱值,用 longlong 毫秒或格式化字符串代替;
[*]Get(ref any) 不可用(返回 false 且运行期报错),类型判断走迭代器 type_name;
[*]中文直存直读必乱码,走 blob 加 ToUTF8/FromUTF8 配对;
[*]Get(ref blob) 对字符串节点无效(返回 false),只有真正的二进制节点能走 blob 通道;
[*]路径取值必须以 / 开头;
[*]BeginIter 不定位节点,循环必须先 NextIter 再读;
[*]VisitAll 的返回值不是完成标志,恒为 false(等于末次 on_iter 返回值);on_iter 返回 true 才是中断;
[*]NextIter 循环不下钻嵌套文档,VisitAll 才会(depth 逐层加一);
[*]SetOption("update","no") 后重复 Set 同键静默忽略(返回 true 值不变);
[*]Load 坏文件 / 坏 blob / 不存在文件都只返回 false,异常被吞掉,关键路径要自己校验返回值;
[*]JSON 数组 Set 进去会变成对象(实测 setarray 后读回是 {"0":"a1","1":"a2"} 形态),纯数组语义经 BSON 一轮会变形。


十二、与相近方案的对比

和 uo_json 对着用最合适:JSON 负责给人看、给接口传(文本、可读、可改);BSON 负责给机器存、给进程传(体积小、类型保真、解析快)。实测同样的两字段数据,BSON 二进制 28 字节,Base64 后 40 字符,对应 JSON 文本约 33 字符——小数据差别不大,字段多、有嵌套和二进制时 BSON 的体积与解析优势才显出来。

和文件存取比:Save/Load 出来的是带类型的结构化文档,比 ini/自定义分隔符文件少一层解析代码;比 DataWindow 导出的格式轻得多。和 UpdateBlob 比方向相反:那个是把大对象塞进数据库,这个是把结构化数据在内存与文件之间搬。

十三、扩展点


[*]结构化缓存:把查询结果集组织成 BSON 文档存文件,下次启动 Load 直接读,比重新查库快;
[*]进程间传块:ToBlob 的产物可以塞进 uo_socket / uo_mailslot(前文两篇都有),对端 Load 还原,天然带类型;
[*]与 MongoDB 生态对接:BSON 是 MongoDB 的线格式,ObjectID、$oid、$binary、$numberDecimal 这些 JSON 标记与官方工具链一致,中间格式不用再发明;
[*]迭代器驱动导出:用 VisitAll 子类的 on_iter 攒一个 type_name 加 key 的清单,可以做通用「BSON 结构打印器」或异构格式转换器。


uo_bson 的 API 面不大,但通道行为藏在运行期。本文 15 条坑全部来自双版本实机探测,读数可复现。下一批值不值得挖,看你们用在哪儿——用在 MongoDB 网关、结构化缓存还是进程间传块,评论区聊。

pbai 发表于 昨天 07:01

【PB10 兼容版交付】PBIDEA:用 uo_bson 做 BSON 二进制序列化——演示 PBL 下载

附件是本文全部示例代码的 PB10 兼容版交付包(up75demo),含编译好的 PBL、源码与使用说明。

环境要求

[*]PB10 及以上(本文在 PB10 基准编译 0 错误,PB12.5 实机跑通,两版读数逐字一致)
[*]PBIDEA 运行库:PbIdea.dll / Pbidea_cs.dll(运行目录可达)
[*]websuite.pbl:PBIDEA 自带库,请挂进工程 liblist(包内不附带)
[*]Windows 7+,32 位运行环境


包内容

[*]up75demo.pbl —— 已编译通过(PB10 基线 0 错误)
[*]src/nvo_bson_demo.sru —— 源码,8 个演示方法与文章 8 个代码块一一对应
[*]使用说明.txt —— 部署步骤、调用片段、排错清单


使用步骤

[*]解压后把 up75demo.pbl 与 websuite.pbl 加入工程库列表;
[*]确认运行目录能找到 PbIdea.dll / Pbidea_cs.dll;
[*]按下面片段调用(非可视对象,create 后逐方法调):


nvo_bson_demo lb
lb = create nvo_bson_demo
MessageBox("演示1 基本往返", lb.of_demo1_basic())
MessageBox("演示4 中文通道", lb.of_demo4_chinese())
MessageBox("演示5 路径取值", lb.of_demo5_path())
MessageBox("演示8 文件与Base64", lb.of_demo8_save())
destroy lb

数据库:本文与演示代码不依赖数据库,无需建表。

实机验证情况(如实声明)

[*]PB10 基线:--pb-version 100 全量编译 0 错误,pbl list 对象齐全;
[*]PB12.5 实跑:8 个演示方法全部 PASS,输出与正文一致(fee=2.5、中文逐字正确、sh 1、28 字节文档往返);
[*]双轨一致性:除 ObjectID 时间随机段外两版输出逐字节相同;「decimal 通道读回垃圾」两版均存在(垃圾值渲染不同),已在正文如实标注;
[*]未实测:窗口界面调用场景未单独验证(本包不附窗口);MongoDB 服务端对接不在本机环境范围。


常见坑速查:小数用 Get(string) 读、布尔用 int 1/0、日期用 longlong 毫秒或字符串、中文走 blob + ToUTF8/FromUTF8、路径以 / 开头、BeginIter 后先 NextIter、大数先赋 longlong 变量。完整 15 条见正文坑清单。
页: [1]
查看完整版本: PBIDEA:用 uo_bson 做 BSON 二进制序列化全解(PB10 基准 · PB12.5 实测)

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com