马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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 双向 | | ObjectID | SetObjectID(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 输出实测结果:
- // 输入值定义(前段统一声明并赋值)
- string ls_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)
- // 按类型读回
- string ls_r1
- int li_r2
- string ls_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_name | bson_type | bson 类型 | pb_type | | "x"(string) | string | 2 | UTF8 | 6 | | 7(int 字面量) | long | 16 | INT32 | 2 | | 3000000000(longlong 变量) | longlong | 18 | INT64 | 20 | | 2.5(double 字面量) | decimal | 19 | DECIMAL128 | 5 | | 2.5(decimal 变量) | decimal | 19 | DECIMAL128 | 5 | | true(boolean) | boolean | 8 | BOOL | 7 | | blob("AB") | blob | 5 | BINARY | 11 | | datetime 值 | datetime | 9 | DATE_TIME | 14 | | 子文档(uo_bson) | uo_bson | 3 | DOCUMENT | 23 |
两个要点:一是 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_bson lb
- 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 网关、结构化缓存还是进程间传块,评论区聊。 |