马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_mailslot_server / uo_mailslot_client 做同机进程间通信全解(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(两版均实测,行为逐项一致);涉及 PBIDEA 组件 uo_mailslot_server / uo_mailslot_client / uo_blob(PbIdea 1.x 运行库)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows 7+;工程需挂接 PBIDEA 的 websuite.pbl,运行目录需有 PbIdea.dll;本文全部示例在本机回环即可运行,无需网络与第二台机器
4. 难度系数:★★☆☆☆
5. 其它阅读说明:前置知识只需会 create 用户对象与基本 PowerScript 语法;第八节的挂死结论建议先读
一、邮槽是什么、跟 socket 有什么区别
Windows 邮槽(mailslot)是操作系统自带的一种单向、无连接的进程间通信(IPC)机制:一个进程创建"槽",另一个进程往槽里丢消息,创建者从槽里取。PBIDEA 把它封装成了两个非可视对象:
- uo_mailslot_server:创建槽、查消息数、读消息;
- uo_mailslot_client:连槽、写消息。
它和本系列前面写过的 socket 三件套、websocket 双对象的定位不同,选型时按这张表走:
| 对比项 | 邮槽(本文) | socket / websocket | | 通信方向 | 单向:client 写、server 读 | 双向 | | 通信范围 | 本机或局域网(跨机要写主机名) | 本机 / 局域网 / 互联网 | | 连接概念 | 无连接,client 不握手 | 三次握手 / WS 握手 | | 消息边界 | 有(一条 write 就是一条消息) | TCP 是字节流,要自己拆包 | | 典型用途 | 同机两个进程间的单向通知、小数据投递 | 长连接、双向交互、跨公网 |
一句话:只是"我通知你一声"或"我把这块数据丢给你",不必搭 TCP 服务,邮槽是最省事的通道。
二、对象与 API 全貌
两个对象的全部公开成员如下(方法名与参数以 PbIdea 1.x 导出源码为准,全部经编译与运行双重验证):
uo_mailslot_server:
| 成员 | 说明 | | CreateServer(server, name, timeout) 返回 boolean | 创建槽。server 一般传 '.' 表示本机;name 是槽名;timeout 为读超时(毫秒) | | CreateServer(server, name, async) 返回 boolean | 异步版:async 传 true 时后台线程持续读消息,收到后触发 onmessage 事件 | | DestroyServer() | 销毁槽(对象 destructor 事件也会自动调) | | GetMessageCount() 返回 int | 当前排队的消息条数 | | Read() 返回 uo_blob | 读一条消息;非阻塞,空队列立即返回一个 size=0 的空容器 | | 事件 onmessage(readonly blob data) | 仅异步版 CreateServer 有意义,收到消息时触发 |
uo_mailslot_client:
| 成员 | 说明 | | CreateClient(server, name) 返回 boolean | 连接槽。server 可为 '.'(本机)或远程主机名 | | DestroyClient() | 断开(destructor 事件自动调) | | write(string data) 返回 boolean | 写字符串,实测按 UTF-8 编码 | | write(string data, boolean toUtf8) 返回 boolean | toUtf8=true 按 UTF-8;toUtf8=false 按 ANSI/GBK | | write(blob data) 返回 boolean | 写原始字节,一个字节都不转,二进制透传 |
注意 Read() 返回的是 uo_blob(PBIDEA 的二进制容器对象),不是 PB 原生 blob,取数据要再用它的 get / size / ReadByte 方法,见第五节。
三、最小可运行回环示例
前置操作步骤:1) 把 websuite.pbl 加入工程的库列表(PBT 的 liblist);2) 运行目录下放好 PbIdea.dll;3) 在应用或按钮事件里贴入下方代码;4) 运行后 MessageBox 会显示回环结果。
- // 输入:槽名(服务端与客户端必须同名)
- string ls_slot
- ls_slot = 'ms69demo'
- // 输入:要发送的消息
- string ls_msg
- ls_msg = 'hello mailslot'
- uo_mailslot_server lsrv
- uo_mailslot_client lc
- uo_blob lb
- blob ldata
- string ls_got
- int li_cnt
- boolean b1, b2, b3
- // 1) 先建服务端:'.' 表示本机;5000 为读超时(毫秒)
- lsrv = create uo_mailslot_server
- b1 = lsrv.CreateServer('.', ls_slot, 5000)
- // 2) 再建客户端:必须晚于服务端,且槽名一致
- lc = create uo_mailslot_client
- b2 = lc.CreateClient('.', ls_slot)
- // 3) 写消息(默认按 UTF-8 编码发送)
- b3 = lc.write(ls_msg)
- // 4) 查看排队消息数
- li_cnt = lsrv.GetMessageCount()
- // 5) 读消息:非阻塞;本例确定有 1 条
- lb = lsrv.Read()
- lb.get(1, ldata)
- ls_got = String(ldata, EncodingUTF8!)
- // 6) 清理
- destroy lc
- destroy lsrv
- MessageBox('回环结果', 'srv=' + String(b1) + ' cli=' + String(b2) + ' wr=' + String(b3) + ' 消息数=' + String(li_cnt) + ' 收到=' + ls_got)
复制代码
本机实测两版输出完全一致:srv=true cli=true wr=true 消息数=1 收到=hello mailslot。
两个顺序铁律(违反后果见第八节):
- 服务端必须先创建。客户端连一个不存在的槽名,程序会无限挂死。
- 同一个槽名只建一次服务端。第二个 server 实例对同名槽调 CreateServer,程序会无限挂死。
四、write 的编码规则(实测)
write 有三个重载,编码行为不一样,中文场景最容易在这里翻车。下表是本机用「中文」两个字实测的字节数与回读结果(两版一致):
| 写法 | 发出字节数 | 实际编码 | 正确的读回方式 | | write(字符串) | 6 | UTF-8(每字 3 字节) | String(blob, EncodingUTF8!) | | write(字符串, true) | 6 | UTF-8 | String(blob, EncodingUTF8!) | | write(字符串, false) | 4 | ANSI(GBK 系统下即 GBK) | String(blob, EncodingANSI!) | | write(blob) | 原样 | 不转码 | 按自己写入时的编码解释 |
下面的演示代码把三种写法各跑一遍并回读验证,可直接复制运行:
- // 输入:槽名
- string ls_slot
- ls_slot = 'ms69enc'
- // 输入:要发送的中文消息
- string ls_cn
- ls_cn = '中文'
- uo_mailslot_server lsrv
- uo_mailslot_client lc
- uo_blob lb
- blob ldata
- string ls_s1, ls_s2, ls_s3
- int li_n1, li_n2, li_n3
- lsrv = create uo_mailslot_server
- lc = create uo_mailslot_client
- lsrv.CreateServer('.', ls_slot, 5000)
- lc.CreateClient('.', ls_slot)
- // 写法一:write(字符串) —— 按 UTF-8 发出
- lc.write(ls_cn)
- lb = lsrv.Read()
- li_n1 = lb.size()
- lb.get(1, ldata)
- ls_s1 = String(ldata, EncodingUTF8!)
- // 写法二:write(字符串, true) —— 同样按 UTF-8 发出
- lc.write(ls_cn, true)
- lb = lsrv.Read()
- li_n2 = lb.size()
- lb.get(1, ldata)
- ls_s2 = String(ldata, EncodingUTF8!)
- // 写法三:write(字符串, false) —— 按 ANSI/GBK 发出(4 字节)
- lc.write(ls_cn, false)
- lb = lsrv.Read()
- li_n3 = lb.size()
- lb.get(1, ldata)
- ls_s3 = String(ldata, EncodingANSI!)
- destroy lc
- destroy lsrv
- MessageBox('编码对照', 'UTF-8: ' + String(li_n1) + '字节 回读=' + ls_s1 + ' | true: ' + String(li_n2) + '字节 回读=' + ls_s2 + ' | false: ' + String(li_n3) + '字节 回读=' + ls_s3)
复制代码
实测输出:UTF-8: 6字节 回读=中文 | true: 6字节 回读=中文 | false: 4字节 回读=中文。编码与读回方式必须配对:按 UTF-8 写的用 EncodingUTF8! 读,按 ANSI 写的用 EncodingANSI! 读,配错就得到乱码。
另外一个隐蔽坑:PB 代码里 blob('ABC') 得到的是 UTF-16LE 字节(A 0 B 0 C 0 共 6 字节),不是 ANSI 的 3 字节。想发 ANSI 字节流,应该用 write(字符串, false) 让组件转,而不是自己在 PB 里拼 blob。
五、读回数据的正确姿势(uo_blob 三种取法)
Read() 返回 uo_blob,常用三种取数方法(都是 PbIdea 对象方法,均已实测):
- // 前置:已按第三节建好槽并收到一条消息(lb 为 Read() 的返回值)
- blob ldata
- string ls_text
- ulong ll_size
- int li_b1
- // 取法一:整段拷到 PB 原生 blob,再用 String 转字符串(注意配编码)
- lb.get(1, ldata)
- ls_text = String(ldata, EncodingUTF8!)
- // 取法二:先看长度再决定怎么处理
- ll_size = lb.size()
- // 取法三:按位置取单字节(位置从 1 开始),适合二进制协议
- li_b1 = lb.ReadByte(1)
- MessageBox('取数演示', 'size=' + String(ll_size) + ' 第一字节=' + String(li_b1) + ' 文本=' + ls_text)
复制代码
两个要点:
- Read() 永远返回对象、不返回空引用。空队列时它立即返回一个 size=0 的空容器(实测 IsNull 为 false),可以放心调用 size(),但此时别调 get。
- 取文本必须指定编码。不指定编码直接 String(blob) 得到的是乱码——组件按 UTF-16LE 解释收到的字节。
六、消息队列行为:FIFO、计数、空读
邮槽内部是个消息队列,实测行为(两版一致):
- 先进先出。连写 aaa、bbb、ccc 三条,连读三次得到 aaa、bbb、ccc,顺序不乱。
- GetMessageCount 随读递减。写 3 条时 count=3,每读一条减 1,读完为 0。
- 空队列 Read 不阻塞也不报错,0 毫秒返回 size=0 的空容器。前面 CreateServer 的 timeout 参数在同步模式下实测并不让 Read 等待——它不是"轮询等待上限"。
- 消息边界完整。一条 write 对应一条消息,不会被拆开或粘连,这是它比 TCP 省心的地方。
七、消息大小边界:1KB 到 1MB
Windows 原生邮槽对跨机数据报有约 424 字节的限制,但本机回环实测远不止(两版一致):
| 单条消息大小 | write 返回 | 读回长度 | | 1000 字节 | true | 1000 字节 | | 60000 字节 | true | 60000 字节 | | 70000 字节 | true | 70000 字节 | | 1000000 字节(约 1MB) | true | 1000000 字节 |
结论:本机通信场景下单条消息到 1MB 都原样完整。但邮槽的设计初衷是小消息通知,把它当大数据传输通道并不可靠(跨机时限制会回来),大文件请走共享目录或 socket。
八、四个会无限挂死的调用(红线)
这是本文最重要的一节。以下四个调用在 PB10 与 PB12.5 下都会无限阻塞、永不返回(每条都单独实测确认,进程只能从外部杀掉):
- CreateServer 的第一参传远程主机名(如 'otherhost'):服务端只能建在本机,传远端名直接挂死。
- CreateServer 的槽名传空串:'' 一进去就挂死。
- CreateClient 连一个不存在的槽名:槽还没建(或已销毁)就去连,挂死。这是最容易踩的一条——两个进程启动顺序稍不对就全卡住。
- 对同一个槽名创建第二个服务端实例:本机已有一个同名槽活着,再来一个 CreateServer,挂死。
防挂死的正确姿势:
- // 场景:多进程协作时,保证"先服务端、后客户端、槽名唯一"
- // 输入:本进程承担的角色('server' 或 'client')
- string ls_role
- ls_role = 'server'
- // 输入:约定好的槽名(两个进程必须一致,且全机唯一)
- string ls_slot
- ls_slot = 'my_app_slot'
- // 输入:服务端只能建在本机
- string ls_host
- ls_host = '.'
- IF ls_role = 'server' THEN
- lsrv = create uo_mailslot_server
- lsrv.CreateServer(ls_host, ls_slot, 5000)
- ELSE
- lc = create uo_mailslot_client
- lc.CreateClient(ls_host, ls_slot)
- END IF
- MessageBox('角色', ls_role + ' 已就绪,槽名=' + ls_slot)
复制代码
在真实多进程场景里,更稳的做法是把"服务端先启动"写进部署文档,甚至让客户端在连之前先用一个约定机制(如探测服务端是否已落盘标记文件)确认服务端就绪。
另有一条不挂死但会骗人的:CreateClient 的第一参传远程主机名,实测约 218 毫秒就返回 true——它并不校验主机是否存在、槽是否可用(延迟到 write 才失败)。所以 CreateClient 返回 true 不代表槽已就绪。
九、错误路径与清理
实测确认的失败行为(两版一致):
- 服务端销毁后,客户端 write 返回 false,不崩溃、不挂死。所以 write 返回 false 是"对面没了"的信号。
- 同一个 server 实例先 DestroyServer 再 CreateServer 可以复活(同槽名重建返回 true,0 毫秒)。
- 两个客户端实例连同一个槽没有冲突:都返回 true,各自 write 各自落一条消息(写一条 count 就是 1,不是 2)。
- 槽名支持子路径:'dir/sub' 这样的名字实测可用(服务端、客户端、读写都正常),可以按业务分目录组织槽名。
- 对象的 destructor 事件会自动调 DestroyServer / DestroyClient,但显式 destroy 前自己调一次销毁方法更稳(尤其打算复活重建的场景)。
十、异步模式与 onmessage 事件
CreateServer 的三参重载传 async=true 即异步模式。本机实测的关键结论(两版一致):
- 异步创建返回 true,客户端 write 也返回 true;
- 消息立即被后台读取线程消费——write 后 GetMessageCount 是 0,同步轮询 Read 也读不到;
- onmessage 事件在本机无窗口的验证环境里跑了 100 轮 Yield 仍未触发——事件投递依赖窗口消息泵,无窗口环境收不到。
也就是说:异步模式下消息走 onmessage 事件,不要指望 GetMessageCount / Read。而 onmessage 的触发依赖消息泵,真实 GUI 应用(有窗口在跑消息循环)里才有意义。本段验证环境的结论已如实标注:异步模式的 onmessage 投递在无窗口环境未实测成功,读者在窗口应用中使用时请自行确认。要确定性强的收发,用同步模式(两参重载)+ 定时器或线程轮询 GetMessageCount 更可控。
十一、PB12.5 差异
本文全部 22 组用例在 PB10 与 PB12.5 下逐项一致:CreateServer / CreateClient / write 三个重载 / GetMessageCount / Read / ReadByte / get / size 的返回值、字节数、耗时量级完全相同,四个挂死调用也完全相同。未发现任何版本差异。
十二、坑清单汇总
- 调用顺序铁律:先服务端、后客户端;客户端连不存在的槽会挂死。
- 同一槽名只能有一个服务端实例,重复创建挂死。
- CreateServer 传远程主机名、传空槽名,都会挂死。
- 挂死是无限阻塞,TRY-CATCH 抓不住(不是异常,是 API 不返回),只能外部超时杀进程。
- write(string) 默认就是 UTF-8;write(string, false) 才是 ANSI/GBK。中文读写编码要配对。
- blob(字符串) 在 PB 里是 UTF-16LE 字节,别拿它当 ANSI 字节流发;发二进制用 write(blob) 且清楚自己每个字节的含义。
- Read() 非阻塞,空队列返回 size=0 的空容器,不是空引用;get 前先确认 count 或 size 大于 0。
- 读文本必须指定编码:EncodingUTF8! 或 EncodingANSI!,不指定的 String(blob) 是 UTF-16 解释,必乱码。
- CreateClient 返回 true 不代表槽就绪(对远程主机名也是秒回 true),失败会延迟到 write 才暴露。
- 服务端销毁后 write 返回 false 而不是报错,要按返回值判连通。
- 异步模式下消息被后台线程消费,GetMessageCount 恒为 0,收消息只能靠 onmessage;onmessage 又依赖窗口消息泵。
- 单条消息本机可到 1MB,但跨机有约 424 字节的数据报限制,别把邮槽当传输通道。
- uo_blob 的 ReadByte 位置从 1 开始。
- 排错时想知道收到几个字节,用 size() 和 ReadByte,不要把 blob 单字节过 String 转 ANSI 再 Asc——非 ASCII 字节会被替换成问号,读数全错。
- 两进程用同一槽名时,先启动的一方必须建服务端;把这一点写进部署文档,比代码防御更有效。
十三、适用场景与替代方案
- 同机两个 PB 进程单向通知(主程序通知托盘程序、监控进程上报事件):邮槽最合适,无端口占用、无握手、消息有边界。
- 需要双向交互:用 socket / websocket(本系列 Day63、Day67 有全解),邮槽单向回话要再开一个反向槽。
- 只是共享少量状态数据:用 uo_kv 或文件,通信语义反而多余。
- 跨机同网段通知:邮槽理论支持(client 第一参写主机名),但要留意 424 字节数据报限制与防火墙,建议直接上 socket。
十四、实机验证情况
- 验证环境:本机回环,Windows,PBIDEA 运行库(PbIdea.dll),不涉及数据库。
- PB10:22 组用例全部编译 0 错误(--pb-version 100 全量 rebuild)+ 运行 PASS;回环演示输出「hello mailslot / 中文」与计数 1→0 正确。
- PB12.5:同一组用例全部编译 0 错误(--pb-version 125 全量 rebuild)+ 运行 PASS,输出与 PB10 逐字节一致。
- 挂死类用例(第八节 4 条)以外部超时方式确认两版均无限阻塞;异步模式 onmessage 在无窗口验证环境未触发,已按实际边界如实标注(第十节)。
- 静态检查:check_pb125.py 0 错 0 提醒;check_enc.py 编码全 OK(GBK / CRLF / 无 BOM)。
|