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,用来回复对应的客户端

小技巧: 我个人习惯把 smsgrmsg 定义成结构体,这样数据清晰。比如定义一个 struct requeststruct 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 运行流程:你看着我,我看着你

咱们把这两个程序跑起来,流程是这样的:

  1. 服务器先启动,调用 ChannelCreate 创建通道,然后 MsgReceive 阻塞等待。
  2. 客户端启动,调用 ConnectAttach 连接到服务器通道,然后 MsgSend 发送请求,客户端阻塞
  3. 服务器收到消息,从 MsgReceive 返回,处理加法,然后 MsgReply 回复。
  4. 客户端从 MsgSend 返回,拿到结果,继续执行。

你看,整个过程就像两个人打电话:你说话,我听着;我说完,你回答。谁都不抢话,谁都不挂断。

3.5 避坑指南:我曾经踩过的坑

我曾经在一个项目中,服务器端 MsgReplystatus 参数传了个负数,结果客户端 MsgSend 返回后,以为发送失败,直接退出了。其实 status 是服务器自定义的,不是错误码。所以记住:

  • MsgSend 返回 -1 才是真的错误,其他值都是服务器给你的状态。
  • MsgReply 的 status 参数,你可以用它来传递简单的成功/失败标志,但别和系统错误码搞混。
  • 消息大小要匹配:客户端发的 sbytes 和服务器收的 rbytes 要一致,否则数据会截断或读错。

3.6 总结:三件套的精髓

好了,咱们总结一下:

  • MsgSend:客户端发消息,然后阻塞等回复。
  • MsgReceive:服务器收消息,没消息就阻塞等。
  • MsgReply:服务器处理完,回复客户端,客户端才醒。

这套模型虽然简单,但非常强大。你想想看,在实时系统中,这种同步阻塞的方式,天然就避免了竞态条件和数据不一致的问题。很多 RTOS 的 IPC 机制,其实底层都是这个思路。

下一章,咱们会深入讲讲通道(Channel)和连接(Connection)的区别,以及怎么用 name_attach 让客户端不用知道 PID 也能找到服务器。嗯,那才是真正实用的技巧。