3. 消息传递基础:MsgSend/MsgReceive/MsgReply 三件套,同步阻塞模型,最简单的C/S示例
好,咱们今天来聊聊 QNX 消息传递最核心的东西——MsgSend、MsgReceive、MsgReply 这三兄弟。我把它叫做「三件套」,因为它们是 QNX 进程间通信的基石。你想想看,在嵌入式世界里,进程之间怎么说话?靠的就是这套机制。
3.1 同步阻塞模型:到底是怎么个「堵」法?
先说说这个同步阻塞模型。说白了,就是「你等我,我等你」。
- 客户端(Client) 调用
MsgSend()发送消息后,立刻阻塞。它啥也不干,就等着服务器回话。 - 服务器(Server) 调用
MsgReceive()接收消息时,如果没有消息,也阻塞。它就在那儿等着客户端来找它。 - 服务器处理完消息后,调用
MsgReply()回复。这时候,客户端才从MsgSend()里醒过来,拿到回复数据。
为什么会这样?因为 QNX 的设计哲学就是「简单可靠」。你想想看,如果发完消息就不管了,那服务器怎么知道消息处理完了?客户端怎么知道结果对不对?同步模型虽然看起来「堵」,但它保证了数据的一致性和确定性。
核心要点: 在 QNX 里,消息传递是 同步且阻塞 的。发送方和接收方在消息交换期间,是「一对一」绑定的。没有消息队列,没有异步回调,就是这么纯粹。
我在项目中遇到过一个问题:有个同事把 MsgSend 当成异步调用来用,发完消息就去干别的事,结果服务器还没处理完,客户端又发了一条。嗯,那数据就乱套了。所以记住:MsgSend 发完就等,MsgReceive 收不到就等。
3.2 三件套的 API 长什么样?
咱们直接看函数原型,我习惯用 C 语言来写,因为嵌入式里 C 是王道。
#include <sys/neutrino.h>
// 客户端发送消息
int MsgSend(int coid, const void *smsg, int sbytes, void *rmsg, int rbytes);
// 服务器接收消息
int MsgReceive(int chid, void *rmsg, int rbytes, struct _msg_info *info);
// 服务器回复消息
int MsgReply(int rcvid, int status, const void *msg, int bytes);
参数看着多,其实不难理解:
| 函数 | 关键参数 | 说明 |
|---|---|---|
MsgSend |
coid:连接 ID |
客户端通过 ConnectAttach 拿到这个 ID,然后发消息 |
MsgReceive |
chid:通道 ID |
服务器创建通道,等着客户端来连 |
MsgReply |
rcvid:接收 ID |
服务器从 MsgReceive 拿到这个 ID,用来回复对应的客户端 |
小技巧: 我个人习惯把 smsg 和 rmsg 定义成结构体,这样数据清晰。比如定义一个 struct request 和 struct response,代码可读性高很多。
3.3 最简单的 C/S 示例:一个加法服务器
光说不练假把式。咱们写一个最简单的例子:客户端发两个整数,服务器算加法,返回结果。
服务器端代码(server.c)
#include <stdio.h>
#include <sys/neutrino.h>
#include <string.h>
#define CHANNEL_NAME "adder_server"
typedef struct {
int a;
int b;
} request_t;
typedef struct {
int sum;
} response_t;
int main() {
int chid;
int rcvid;
request_t req;
response_t resp;
// 1. 创建通道
chid = ChannelCreate(0);
if (chid == -1) {
perror("ChannelCreate");
return 1;
}
printf("Server: channel created, chid = %d\n", chid);
// 2. 循环接收消息
while (1) {
rcvid = MsgReceive(chid, &req, sizeof(req), NULL);
if (rcvid == -1) {
perror("MsgReceive");
break;
}
printf("Server: received request: a=%d, b=%d\n", req.a, req.b);
// 3. 处理:算加法
resp.sum = req.a + req.b;
// 4. 回复
MsgReply(rcvid, 0, &resp, sizeof(resp));
printf("Server: replied with sum=%d\n", resp.sum);
}
ChannelDestroy(chid);
return 0;
}
客户端代码(client.c)
#include <stdio.h>
#include <sys/neutrino.h>
#include <string.h>
typedef struct {
int a;
int b;
} request_t;
typedef struct {
int sum;
} response_t;
int main() {
int coid;
request_t req;
response_t resp;
int status;
// 1. 连接到服务器通道(假设服务器 pid 已知,这里用 0 表示自己测试)
// 实际项目中,你需要知道服务器的 pid 或使用名字查找
coid = ConnectAttach(0, 0, 1, _NTO_SIDE_CHANNEL, 0);
if (coid == -1) {
perror("ConnectAttach");
return 1;
}
printf("Client: connected, coid = %d\n", coid);
// 2. 准备请求数据
req.a = 10;
req.b = 20;
// 3. 发送消息并等待回复
status = MsgSend(coid, &req, sizeof(req), &resp, sizeof(resp));
if (status == -1) {
perror("MsgSend");
return 1;
}
printf("Client: received sum = %d\n", resp.sum);
ConnectDetach(coid);
return 0;
}
注意: 上面的客户端代码里,ConnectAttach(0, 0, 1, ...) 中的参数是硬编码的。实际项目中,你需要通过 name_attach() 或进程 PID 来动态连接。我这里只是为了演示最简流程。
3.4 运行流程:你看着我,我看着你
咱们把这两个程序跑起来,流程是这样的:
- 服务器先启动,调用
ChannelCreate创建通道,然后MsgReceive阻塞等待。 - 客户端启动,调用
ConnectAttach连接到服务器通道,然后MsgSend发送请求,客户端阻塞。 - 服务器收到消息,从
MsgReceive返回,处理加法,然后MsgReply回复。 - 客户端从
MsgSend返回,拿到结果,继续执行。
你看,整个过程就像两个人打电话:你说话,我听着;我说完,你回答。谁都不抢话,谁都不挂断。
3.5 避坑指南:我曾经踩过的坑
我曾经在一个项目中,服务器端 MsgReply 的 status 参数传了个负数,结果客户端 MsgSend 返回后,以为发送失败,直接退出了。其实 status 是服务器自定义的,不是错误码。所以记住:
- MsgSend 返回 -1 才是真的错误,其他值都是服务器给你的状态。
- MsgReply 的 status 参数,你可以用它来传递简单的成功/失败标志,但别和系统错误码搞混。
- 消息大小要匹配:客户端发的
sbytes和服务器收的rbytes要一致,否则数据会截断或读错。
3.6 总结:三件套的精髓
好了,咱们总结一下:
- MsgSend:客户端发消息,然后阻塞等回复。
- MsgReceive:服务器收消息,没消息就阻塞等。
- MsgReply:服务器处理完,回复客户端,客户端才醒。
这套模型虽然简单,但非常强大。你想想看,在实时系统中,这种同步阻塞的方式,天然就避免了竞态条件和数据不一致的问题。很多 RTOS 的 IPC 机制,其实底层都是这个思路。
下一章,咱们会深入讲讲通道(Channel)和连接(Connection)的区别,以及怎么用 name_attach 让客户端不用知道 PID 也能找到服务器。嗯,那才是真正实用的技巧。