3. 发布-订阅基本通信

发布-订阅基本通信 学习卡片

任务说明

本案例实现DDS最基本的发布-订阅通信模式:一个发布者节点周期发送温度数据(float32),一个订阅者节点接收并打印收到的温度值。通过该案例验证DDS中间件的核心机制——数据以Topic为通道,在参与者之间自动路由分发。

环境依赖

使用Eclipse Cyclone DDS(C语言实现),需安装以下依赖:

  • Cyclone DDS库(libcyclonedds-dev)
  • C编译器(gcc)
  • CMake(可选,用于构建)

IDL定义

定义温度数据类型和Topic。将以下内容保存为Temperature.idl:

module tutorial {
    struct Temperature {
        float value;  // 温度值(摄氏度)
    };
    #pragma keylist Temperature
};

发布者代码

保存为publisher.c。发布者每500ms发送一次温度数据,温度值从20.0开始递增0.5:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "dds/dds.h"
#include "Temperature.h"

int main(void)
{
    dds_entity_t participant, topic, writer;
    tutorial_Temperature sample;
    uint32_t status = 0;
    int rc;

    /* 创建参与者(域ID=0) */
    participant = dds_create_participant(0, NULL, NULL);
    if (participant < 0) {
        printf("创建参与者失败: %s\n", dds_strerrno(participant));
        return 1;
    }

    /* 创建Topic */
    topic = dds_create_topic(participant, &tutorial_Temperature_desc,
                            "TempSensor", NULL, NULL);
    if (topic < 0) {
        printf("创建Topic失败: %s\n", dds_strerrno(topic));
        return 1;
    }

    /* 创建DataWriter */
    writer = dds_create_writer(participant, topic, NULL, NULL);
    if (writer < 0) {
        printf("创建Writer失败: %s\n", dds_strerrno(writer));
        return 1;
    }

    printf("发布者启动,开始发送温度数据...\n");
    float temp = 20.0f;
    for (int i = 0; i < 20; i++) {  /* 发送20次后自动退出 */
        sample.value = temp;
        rc = dds_write(writer, &sample);
        if (rc != DDS_RETCODE_OK) {
            printf("写入失败: %s\n", dds_strerrno(rc));
            break;
        }
        printf("已发送: %.1f°C\n", temp);
        temp += 0.5f;
        usleep(500000);  /* 500ms */
    }

    /* 清理资源 */
    dds_delete(participant);
    return 0;
}

订阅者代码

保存为subscriber.c。订阅者持续监听Topic,收到数据后打印温度值,收到20个样本后退出:

#include <stdio.h>
#include <stdlib.h>
#include "dds/dds.h"
#include "Temperature.h"

int main(void)
{
    dds_entity_t participant, topic, reader;
    tutorial_Temperature sample;
    void *samples[1];
    dds_sample_info_t infos[1];
    int count = 0;

    /* 创建参与者 */
    participant = dds_create_participant(0, NULL, NULL);
    if (participant < 0) {
        printf("创建参与者失败: %s\n", dds_strerrno(participant));
        return 1;
    }

    /* 创建Topic(名称必须与发布者一致) */
    topic = dds_create_topic(participant, &tutorial_Temperature_desc,
                            "TempSensor", NULL, NULL);
    if (topic < 0) {
        printf("创建Topic失败: %s\n", dds_strerrno(topic));
        return 1;
    }

    /* 创建DataReader */
    reader = dds_create_reader(participant, topic, NULL, NULL);
    if (reader < 0) {
        printf("创建Reader失败: %s\n", dds_strerrno(reader));
        return 1;
    }

    printf("订阅者启动,等待温度数据...\n");
    while (count < 20) {
        /* 阻塞等待数据,超时5秒 */
        dds_entity_t status = dds_read(reader, samples, infos, 1, 1, 5000);
        if (status > 0) {
            for (int i = 0; i < status; i++) {
                if (infos[i].valid_data) {
                    tutorial_Temperature *temp = (tutorial_Temperature *)samples[i];
                    printf("收到温度: %.1f°C\n", temp->value);
                    count++;
                }
            }
            dds_return_loan(reader, samples, status);
        } else if (status == DDS_RETCODE_TIMEOUT) {
            printf("等待超时,未收到数据\n");
            break;
        }
    }

    dds_delete(participant);
    return 0;
}

编译与运行

使用IDL编译器生成类型支持代码,然后编译两个程序:

# 生成类型支持代码(生成Temperature.h/.c等文件)
ddsc Temperature.idl

# 编译发布者
gcc -o publisher publisher.c Temperature.c -lddsc

# 编译订阅者
gcc -o subscriber subscriber.c Temperature.c -lddsc

运行(需先启动订阅者再启动发布者,或同时启动):

./subscriber &   # 终端1
./publisher      # 终端2

验证方法

满足以下条件即判定案例通过:

  • 订阅者终端按约500ms间隔持续打印温度值,数值从20.0开始每次递增0.5,共收到20条数据
  • 发布者终端打印的发送顺序与订阅者接收顺序一致
  • 两个进程退出码均为0,无错误输出

预期输出(订阅者终端):

收到温度: 20.0°C
收到温度: 20.5°C
收到温度: 21.0°C
...

通信流程示意

发布者 DataWriter 订阅者 DataReader Topic "TempSensor" DDS域(域ID=0)

发布者通过DataWriter将温度样本写入Topic,DDS中间件自动将数据路由到同一域内订阅该Topic的DataReader,无需双方直接建立连接。