PBIDEA:用 uo_mailslot_server / uo_mailslot_client 做同机进程间通信全解(PB10 基准 · PB12.5 实测)
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) 返回 booleantoUtf8=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(字符串)6UTF-8(每字 3 字节)String(blob, EncodingUTF8!)
write(字符串, true)6UTF-8String(blob, EncodingUTF8!)
write(字符串, false)4ANSI(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 字节true1000 字节
60000 字节true60000 字节
70000 字节true70000 字节
1000000 字节(约 1MB)true1000000 字节
结论:本机通信场景下单条消息到 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)。
【PB10 兼容版交付】uo_mailslot 邮槽进程间通信演示包
把文章全部示例整理成了 PB10 兼容版源码包(PB10 全量编译 0 错误,PB12.5 同源复验 0 错误 + 运行通过,输出逐项一致)。
一、环境要求
1. Windows 7+(邮槽是 Windows 系统机制);
2. PB10 及以上运行库(32 位,与你的 PB 版本一致);
3. 需要已安装 PBIDEA 运行库(PbIdea.dll 放运行目录或系统路径),工程库列表挂上 PBIDEA 的 websuite.pbl(uo_mailslot_server / uo_mailslot_client / uo_blob 都在里面);
4. 不需要数据库、不需要网络,全部演示本机回环。
二、操作步骤
1. 新建一个空 PBL,用 Import 把包内 src 下 4 个源文件(a_ms69.sra / w_ms69.srw / nvo_ms_demo.sru / uo_ms_sub.sru)导进去;
2. 工程库列表加上 websuite.pbl;
3. 以 a_ms69 为应用对象运行(或直接 open(w_ms69));
4. 窗口 4 个按钮对应文章:1=最小回环(第三节)、2=编码对照(第四节)、3=FIFO 与计数(第六节)、4=挂死红线说明(第八节)。
三、数据库
本文与示例完全不依赖数据库,无需建表。
四、实机验证情况
1. PB10:全量编译 0 错误;主线演示运行通过(收到 hello mailslot / 中文,计数 1→0);
2. PB12.5:同源编译 0 错误 + 同批用例运行通过,与 PB10 输出逐字节一致(零版本差异);
3. 专题实测:write 编码三重载(默认与 true 都是 UTF-8、false 是 GBK)、FIFO、空队列非阻塞返回 size=0、单条消息 1KB~1MB 全完整、服务端销毁后 write=false、双客户端共存、子路径槽名;
4. 如实标注:异步模式 onmessage 事件在无窗口验证环境未触发(消息被后台线程消费,count 恒 0),窗口应用中请自行确认;4 个会无限挂死的调用结论来自实机探针(远程主机名建服务端 / 空槽名 / 客户端连不存在的槽 / 同名第二服务端)。
详细说明见包内 doc/使用说明.txt。欢迎跟帖反馈。
页:
[1]