源码已经上传至我的 [Gitee](https://gitee.com/ascloudwalker),可用如下代码下载:
git clone https://gitee.com/ascloudwalker/c-publish--subscribe.git
- 前言
最近接触到一些工程上的代码,使用全局变量或者函数指针的方式进行数据传递,在某些时候不是很方便:
- 通过函数一层一层传递数据很麻烦;
- 修改全局变量不可控;
此时,我想起了PX4的uORB(Micro Object Request Broker,微对象请求代理器)。
- 原理
uORB本质上是一种发布订阅模式,什么是发布订阅模式?发布订阅模式:假设存在老师T(teacher),黑板B(blackboard),同学S(student).老师T每隔一个时间t1就去黑板B上发布一则通知,学生S每隔一段时间就去看(订阅)黑板上以下老师发布了什么消息。当然,这是最简单的模式,实际应用中实现还有很多附加功能,比如说:实际实现中,学生并不会先去看黑板上的内容,他可能通过某种机制先检查黑板上的内容是否更新,如果没更新就没有必要去看,如果更新了就去看看写了什么。又比如多个老师都可以在黑板上发布消息,也可以有多个同学去查看黑板发布了什么,当然也会有多块黑板。
以上是一种通俗的说法,通俗的优点是好理解,缺点是显得Low。如果想要显得更加专业,请参考以下文章。
再好的想法,只有实现了才能发挥作用,我开始查找现成的源码。
- 首先,PX4的uORB、ROS等成熟的代码中都有该功能,优点是代码完善、功能齐全、结构完备;缺点是繁杂,不适合初学者理解,就算是看明白了也记不住;
- 其次,网上一些单独的所谓发布订阅模式实现,与我所想的有些差别。
我决定,将uORB中最基本的架构抽离出来,使用Visual Studio C++实现。首先列出参考资源以示尊敬:
- 实现
干饭人干饭魂,干完代码干工程,我想实现最简单的功能,构建四个线程:
1. 主线程:1000ms/1HZ,打印主线程运行次数;
2. 线程:50ms/20HZ,发布一次更新数据;
3. 线程1:100ms/10HZ,订阅数据并打印;
4. 线程2:100ms/10HZ,订阅数据并打印
实现方法,当前的PX4版本,uORB的封装更好,代码结构更加简洁,但是不便于提取结构,本文主要参考了zarathustr /uORB提取出来的一个可移植版本,并在其基础上构建了Visual C++工程:
> step1: 构建黑板
// uORB.h
/// 这相当于定义黑板的基本结构
struct Data {
void *data = nullptr;
bool published = false;
};
extern Data *topic_data[1];
// pubsub.cpp
/// 实例化黑板,需要发布多少个主题(topic),就需要实例化多少黑板
const int total_uorb_num = 1;
Data *topic_data[total_uorb_num];
// pubsub.cpp
/// 对象请求代理器初始化,主要是因为结构体指针必须初始化,可以通过断点运行查看该步骤的作用
void orbInit(void)
{
for (int i = 0; i < total_uorb_num; ++i)
{
topic_data[i] = new Data;
}
}
> step2: 构建主题(topic),相当于准备好需要在黑板上发布数据的表格形式
// sample_topic.h
#ifdef __cplusplus
struct sample_topic_s {
#else
struct sample_topic_s {
#endif
time_t timestamp; // 时间戳,用于日志
float rollBody;
float pitchBody;
float yawBody;
int q;
#ifdef __cplusplus
#endif
};
> step3: 在黑板上构建公告板,相当于在黑板上画好表格。
// pubsub.cpp
/// 公告
advert_t Advertise(const void *data, int len)
{
advert_t advert = nullptr;
if (topic_data[0][0].data == nullptr)
{
topic_data[0][0].data = new int[len]; // 根据需要发布的数据data长度和数据类型,分配内存空间
// 相当于根据需要发布的数据长度,在黑板上画好表格,发布的时候就只更新表格中的数据
}
std::memcpy(topic_data[0][0].data, data, len);
advert = (void*)(&topic_data[0][0]);
return advert;
}
> step4: 发布主题
// pubsub.cpp
/// 发布
void Publisher::Publish(advert_t handle, const void *data, int len)
{
std::memcpy(topic_data[0][0].data, data, len); // 将数据拷贝到黑板的表格中
}
> step5: 订阅主题
// pubsub.cpp
/// 订阅
void Subscriber::Subscribe(void *buffer)
{
memcpy(buffer, topic_data[0][0].data, sizeof(*(sample_topic_s *)topic_data[0][0].data));
}
// 将黑板上的数据拷贝到本地缓存使用
通过以上过程,构建了发布订阅的基本流程
> step5 应用
/*
uORB.cpp : 定义控制台应用程序的入口点。
Author:as.ck
Date:2021-01-20
Function: 模拟PX4的uORB的发布订阅模式
Copyright(C):2021, All Rights Reserved.
*/
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <windows.h>
#include <iostream>
#include <time.h>
#include "sample_topic_s.h"
#include "pubsub.h"
using namespace std;
/// 创建发布器和订阅器
Publisher *pub;
Subscriber *sub;
advert_t sample_adv;
/// 线程函数0-主题更新发布线程
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
sample_topic_s sample_topic_user = {};
/// 数据更新
sample_topic_user.pitchBody = 1.0;
sample_topic_user.rollBody = 2.0;
sample_topic_user.yawBody = 3.0;
sample_topic_user.timestamp = time(NULL);
//int a = sizeof(sample_topic_user);
/// 公布主题
sample_adv = Advertise(&sample_topic_user,sizeof(sample_topic_user));
while(1)
{
/// 数据更新
sample_topic_user.pitchBody++;
/// 向主题发布数据
pub->Publish(sample_adv, &sample_topic_user, sizeof(sample_topic_user));
cout <<" " << "数据更新" << endl;
Sleep(50);
}
return 0L;
}
/// 线程函数1-主题订阅线程
sample_topic_s sample_topic_sub;
DWORD WINAPI ThreadProc1(LPVOID lpParameter)
{
while (1)
{
sub->Subscribe(&sample_topic_sub);
cout<< " " << "线程1:"<<sample_topic_sub.pitchBody << endl;
Sleep(100);
}
return 0L;
}
/// 线程函数2-主题订阅线程
DWORD WINAPI ThreadProc2(LPVOID lpParameter)
{
while (1)
{
sub->Subscribe(&sample_topic_sub);
cout<< " " << "线程2:" << sample_topic_sub.pitchBody << endl;
Sleep(100);
}
return 0L;
}
/// 主函数入口
int main()
{
/// 初始化发布订阅模式
orbInit();
/// 创建三个线程
HANDLE thread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
HANDLE thread1 = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
HANDLE thread2 = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
/// 关闭线程
CloseHandle(thread);
CloseHandle(thread1);
CloseHandle(thread2);
/// 主线程的执行路径
int i = 0;
while (1)
{
cout << "主线程:i = " << i << endl;
Sleep(1000);
i++;
}
return 0;
}
/*--------------End of File----------------*/
运行结果:
-后记
以上是基本的发布订阅模式实现,实际PX4 uORB功能更加齐全,比如多个主题、更新检查、优先等级等,值得借鉴。
需要注意的是,在本文中,我订阅的时候是直接将数据拷贝回来了,有些不合理。订阅应当返回黑板(公告板)的地址,在需要用数据的时候,根据黑板地址检查更新,再将数据拷贝回本地使用。