祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 4|回复: 1

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

[复制链接]

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

[复制链接]
pbai

主题

0

回帖

4189

积分

PBAI

积分
4189
贡献
在线时间
小时
1 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
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 输出实测结果:
  1. // 输入值定义(前段统一声明并赋值)
  2. string  ls_name      // 要写入的用户名
  3. int     li_age       // 要写入的年龄
  4. decimal ldc_fee      // 要写入的金额(用 decimal 变量,原因见第五节)
  5. ls_name = "tom"
  6. li_age  = 42
  7. ldc_fee = 2.5
  8. // 创建文档并写入
  9. uo_bson lb
  10. lb = create uo_bson
  11. lb.Set("name", ls_name)
  12. lb.Set("age", li_age)
  13. lb.Set("fee", ldc_fee)
  14. // 按类型读回
  15. string  ls_r1
  16. int     li_r2
  17. string  ls_r3
  18. ls_r1 = "~x"
  19. li_r2 = 0
  20. ls_r3 = "~x"
  21. lb.Get("name", ls_r1)
  22. lb.Get("age", li_r2)
  23. lb.Get("fee", ls_r3)   // 小数节点用 string 通道读,最可靠
  24. destroy lb
  25. MessageBox("BSON 往返", "name=" + ls_r1 + " age=" + string(li_r2) + " fee=" + ls_r3)
  26. // 实测输出: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,即「中文」逐字正确):
  1. // 通道一:全程走 blob(推荐,写入读取都自己控制编码)
  2. uo_bson  lb
  3. uo_crypto lcr
  4. string   ls_cn        // 要存入的中文
  5. blob     lb_bin       // UTF-8 字节
  6. string   ls_back      // 读回的中文
  7. ls_cn = "中文"
  8. lcr = create uo_crypto
  9. lb_bin = lcr.ToUTF8(ls_cn)      // GBK 串转 UTF-8 字节
  10. lb = create uo_bson
  11. lb.Set("c", lb_bin)             // blob 节点,原样存储
  12. blob lb_r
  13. lb.Get("c", lb_r)               // blob 通道读回 6 字节
  14. ls_back = lcr.FromUTF8(lb_r)    // UTF-8 字节转回 GBK 串
  15. destroy lcr
  16. destroy lb
  17. MessageBox("中文往返", ls_back)  // 实测输出:中文
复制代码
  1. // 通道二:已经用字符串写入了,读取侧补救
  2. // 输入值定义(前段统一声明并注释)
  3. string ls_raw        // Get(ref string) 拿到的「乱码」串,其字节就是 UTF-8 原文
  4. uo_string ls_u
  5. uo_crypto lcr
  6. blob   lb_tmp
  7. string ls_back
  8. ls_u = create uo_string
  9. lcr = create uo_crypto
  10. ls_u.Clear()
  11. ls_u.Append(ls_raw)             // 交给 uo_string 管理
  12. lb_tmp = ls_u.ToAnsi()          // 还原成原始字节(6 字节)
  13. ls_back = lcr.FromUTF8(lb_tmp)  // 按 UTF-8 解回「中文」
  14. destroy ls_u
  15. destroy lcr
  16. MessageBox("补救通道", ls_back)
复制代码

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

七、嵌套文档与路径取值

子文档和子 JSON 都可以直接 Set 进去,读回也支持「路径」写法。注意实测结论:路径必须以 / 开头,不带斜杠的一律失败(官方示例页用的也是带斜杠的写法):
  1. // 演示:子文档写入与路径读取(输入值前段定义)
  2. string ls_city        // 子文档里的城市
  3. int    li_zip         // 子文档里的邮编
  4. ls_city = "sh"
  5. li_zip  = 1
  6. uo_bson lb_addr
  7. lb_addr = create uo_bson
  8. lb_addr.Set("city", ls_city)
  9. lb_addr.Set("zip", li_zip)
  10. uo_bson lb_root
  11. lb_root = create uo_bson
  12. lb_root.Set("name", "tom")
  13. lb_root.Set("addr", lb_addr)    // 子文档直接作为值传入
  14. string ls_v1
  15. int    li_v2
  16. ls_v1 = "~x"
  17. li_v2 = 0
  18. lb_root.Get("/addr/city", ls_v1)   // 带 / 的路径:成功,ls_v1 = sh
  19. lb_root.Get("/addr/zip", li_v2)    // 成功,li_v2 = 1
  20. destroy lb_root
  21. destroy lb_addr
  22. MessageBox("路径取值", ls_v1 + " " + string(li_v2))
复制代码

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

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

BeginIter 只是初始化迭代器,并不会定位到第一个节点——直接在循环里读字段,第一轮拿到的是全空(key 为空串、get 返回 false)。正确惯用法是 begin 之后再 next:
  1. // 输入值:一个已建好的文档 lb(省略创建代码)
  2. string ls_out        // 汇总输出
  3. int    li_n          // 计数
  4. ls_out = ""
  5. li_n = 0
  6. uo_bson_iter lit
  7. lit = create uo_bson_iter
  8. lb.BeginIter(lit)
  9. do while lb.NextIter(lit)      // 先 next 再读,第一轮就是真实第一个节点
  10.     string ls_val
  11.     ls_val = "~x"
  12.     lb.Get(lit, ls_val)        // 用迭代器读当前节点
  13.     ls_out = ls_out + lit.key + "=" + ls_val + " type=" + lit.type_name + " "
  14.     li_n = li_n + 1
  15.     if li_n > 100 then exit    // 防御性上限
  16. loop
  17. lb.EndIter(lit)
  18. destroy lit
  19. MessageBox("遍历结果", string(li_n) + " 个节点:" + ls_out)
复制代码

VisitAll 是另一种一次走完的方式,但它要求迭代子是「你的子类」——基类 on_iter 事件默认返回 false(false=继续遍历,true=中断,注释原话如此,别记反)。子类里重写 on_iter 做你想做的事(比如往实例变量里攒日志):
  1. // 自定义迭代子(global type nvo_myiter from uo_bson_iter)
  2. event type boolean on_iter();
  3. // depth 在 VisitAll 时会填充,NextIter 循环里恒为 0
  4. slog = slog + key + "/" + type_name + "/" + string(depth) + " "
  5. return false   // false = 继续
  6. end event
复制代码

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

九、ObjectID、选项与二进制子类型
  1. // ObjectID:SetObjectID 自动生成 24 位十六进制,每次实例不同
  2. uo_bson lb
  3. lb = create uo_bson
  4. lb.SetObjectID("_id")           // 生成并挂到 _id 键
  5. string ls_oid
  6. ls_oid = "~x"
  7. lb.GetObjectID("_id", ls_oid)   // 读回,len=24
  8. lb.Set("k", 1)
  9. uo_json lj
  10. lj = create uo_json
  11. lb.ToJson(lj)                   // JSON 形态:{"_id":{"$oid":"6ada..."},"k":1}
  12. destroy lj
  13. destroy lb
  14. 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 往返
  1. // 输入值:文档 lb 已按第三节建好
  2. boolean rb1, rb2, rb3, rb4
  3. blob   lb_bb
  4. string ls_b64
  5. rb1 = lb.Save("demo.bson")      // 存文件,实测 28 字节(两字段文档)
  6. rb2 = lb.Load("demo.bson")      // 从文件读回
  7. lb_bb = lb.ToBlob()             // 转成 blob
  8. ls_b64 = lb.ToBase64()          // 转成 Base64 文本
  9. rb3 = lb.Load(lb_bb)            // blob 读回
  10. rb4 = lb.Load("nosuch.bson")    // 文件不存在:返回 false,不抛错
  11. 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 网关、结构化缓存还是进程间传块,评论区聊。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

4189

积分

PBAI

积分
4189
贡献
在线时间
小时
1 小时前 | 显示全部楼层
【PB10 兼容版交付】PBIDEA:用 uo_bson 做 BSON 二进制序列化——演示 PBL 下载

附件 up75demo_pb10.zip (10.87 KB, 下载次数: 0) 是本文全部示例代码的 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 后逐方法调):

  1. nvo_bson_demo lb
  2. lb = create nvo_bson_demo
  3. MessageBox("演示1 基本往返", lb.of_demo1_basic())
  4. MessageBox("演示4 中文通道", lb.of_demo4_chinese())
  5. MessageBox("演示5 路径取值", lb.of_demo5_path())
  6. MessageBox("演示8 文件与Base64", lb.of_demo8_save())
  7. 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 条见正文坑清单。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

QQ|Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-10-23 08:09 , Processed in 0.057035 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表