前言
CyberRT 里 Transport 传输层设计有三种:
- INTRA:进程内通信,通过函数调用直接传递消息(零拷贝)
- SHM:同主机进程间通信,利用共享内存实现高效数据交换
- RTPS:跨主机通信,基于 RTPS 协议实现可靠网络传输
这篇文章,重点梳理 SHM 和 RTPS的设计。首先是基类的设计。
基类设计
transport 单例
对外使用单例,提供 CreateTransmitter 和 CreateReceiver 函数。
cpp
//返回一个Transmitter的指针
template <typename M>
auto CreateTransmitter(const RoleAttributes &attr,
const OptionalMode &mode = OptionalMode::RTPS) ->
typename std::shared_ptr<Transmitter<M>>;
//返回一个Receiver的指针
template <typename M>
auto CreateReceiver(const RoleAttributes &attr,
const typename Receiver<M>::MessageListener &msg_listener,
const OptionalMode &mode = OptionalMode::RTPS) ->
typename std::shared_ptr<Receiver<M>>;
其中:
- RoleAttributes 结构体是 当前主机信息的标记(主机名、IP、进程ID、channel相关、
id节点的哈西唯一标识符、qos相关、node相关,message_type消息类型) - OptionalMode:INTRA、SHM、RTPS
- CreateReceiver 额外需要一个 Receiver::MessageListener,其实就是一个回调函数。形参列表为:MessagePtr(模板参数 M 消息结构体)、MessageInfo(额外消息)、RoleAttributes(主机))。
RoleAttributes
cpp
struct RoleAttributes : public Serializable {
std::string host_name; //主机名
std::string host_ip; //主机IP
int32_t process_id; //进程ID
std::string channel_name; // channel name
uint64_t channel_id; // hash value of channel_name
QosProfile qos_profile; // Qos配置策略
uint64_t id; // 节点 的 哈希唯一标识符
std::string node_name; // node name
uint64_t node_id; // hash value of node_name
std::string message_type; // 消息类型
SERIALIZE(host_name, host_ip, process_id, channel_name, qos_profile, id, node_name, node_id,
message_type)
};
MessageListener
cpp
using MessagePtr = std::shared_ptr<M>;
using MessageListener = std::function<void(const MessagePtr &, const MessageInfo &, const RoleAttributes &)>;
MessageInfo
cpp
class MessageInfo
{
public:
...
private:
Identity sender_id_; //发送者ID
Identity spare_id_; //备用发送者 ID
uint64_t channel_id_ = 0;
uint64_t seq_num_ = 0;
};
Endpoint
Endpoint 作为 Transmitter 和 Receiver 的基类 主要声明几个公共变量:
- enabled_ : 标识该端点当前是否已启用。发送端在启用后才允许 Transmit ;接收端在启用后才会向对应的 Dispatcher 注册监听。用于生命周期与状态控制。
- id_ : 端点唯一标识,类型为 Identity 。用于区分不同的发送者/接收者,并在分发时按 sender_id 做针对性路由或连接管理,例如 ListenerHandler 的按对端连接使用 msg_info.sender_id().HashValue() 进行匹配。
- attr_ : 端点的角色属性 RoleAttributes ,包含 channel_id 、 channel_name 、 host_ip 、QoS 配置等。创建 Transmitter/Receiver 时由上层传入,用于选择传输模式、在 Dispatcher 侧建 Reader/Segment、以及在分发时识别通道和参数。
cpp
class Endpoint
{
public:
explicit Endpoint(const RoleAttributes &attr);
virtual ~Endpoint();
const Identity &id() const { return id_; }
const RoleAttributes &attributes() const { return attr_; }
protected:
bool enabled_;
Identity id_;
RoleAttributes attr_;
};
Identity
cpp
class Identity
{
public:
...
private:
void Update(); // 更新标识符
char data_[ID_SIZE]; // 8个字节
uint64_t hash_value_; // 标识符的哈希值
};
Transmitter
主要是声明了Enable和Transmit 纯虚函数。以及 seq_num_ 消息帧号、MessageInfo 附加数据
发送端抽象,负责将消息序列化并通过所选传输模式(RTPS 或 SHM)发出,同时携带 MessageInfo (发送者 ID、序号等)
Receiver
主要是声明了
- Enable纯虚函数
- 保存构造传入的 MessageListener
- protected OnNewMessage 执行回调的函数。(子类吧 OnNewMessage 触发即可触发 MessageListener)
接收端抽象,向对应 Dispatcher 注册监听,并在收到消息后执行用户回调
Dispatcher
Dispatcher 基类,还有 RtpsDispatcher 和 ShmDispatcher 的派生子类。
三种功能:
- Receiver注册管理:Dispatcher 负责管理所有的Receiver实例,确保每个Receiver能够正确接收并处理其订阅的消息。
- 消息获取与转发:Dsipatcher 需要从不同共享主题中获取数据,并根据消息内容将其转发到合适的Receiver
- 回调函数触发
关键成员:
- bool is_shutdown_ 控制生命周期
- AtomicHashMap<uint64_t, ListenerHandlerBasePtr> msg_listeners_ 用于跨线程安全地保存各通道监听器
- channel_id --- ListenerHandlerBase
- AtomicRWLock rw_lock_ 保护并发访问
首次注册会为该 channel_id 创建 ListenerHandler 并建立连接;后续复用同一 handler 并检查类型一致性
Dispatcher的 RtpsDispatcher 和 ShmDispatcher 子类 主要是在 Receiver 的子类 ShmReceiver 和 RtpsReceiver
- 构造获取
- Enable 注册,把 Receiver注册到Dispatcher
ListenerHandlerBase 抽象基类
- 定义 Disconnect RunFromString 纯虚函数
Signal 信号槽的实现
Slot
模板参数是 Args:回调函数的形参列表。
每个Slot就是对回调函数的封装
cpp
template <typename... Args>
class Slot
{
public:
using Callback = std::function<void(Args...)>;
Slot(const Slot &another) : cb_(another.cb_), connected_(another.connected_) {}
explicit Slot(const Callback &cb, bool connected = true) : cb_(cb), connected_(connected) {}
virtual ~Slot() {}
void operator()(Args... args)
{
if (connected_ && cb_) {
cb_(args...);
}
}
void Disconnect() { connected_ = false; }
bool connected() const { return connected_; }
private:
Callback cb_;
bool connected_ = true;
};
Connection
Connection 代表了一对 Slot和Signal的连接关系。
Signal
模板参数也是 Args:回调函数的形参列表。
Signal 内部成员:
- std::list<std::shared_ptr<Slot<Args...>>>:一个 Slot 列表
- std::mutex 对 Slot 列表 互斥访问。
调用方式:
-
Signale::Connect(const Callback &cb) 传入一个回调函数,封装成Slot,添加到Slot列表,然后生成一个Connection<Args...>返回。
-
Signale::Disconnect(const ConnectionType &conn) 则是传入一个Connection<Args...>,从Slot列表里找到 conn里那个slot,然后slot::DisConnect。
ListenerHandler 子类 信号槽机制
信号槽机制,基类 连接状态
两种绑定关系:
- 广播绑定
- 定向绑定
Connect 提供了两种绑定,如果只提供self_id和回调,那么注册到signal_统一的(只要有消息就会广播触发所有)
如果额外提供 oppo_id,那么当 对应的消息来了,单独触发。
SHM 设计思路

- Transmitter会根据channel_id_作为key生成一块Segment共享内存
- (SegmentFactory::CreateSegment 有两种构造方法:Posix IPC
PosixSegment和System V IPCXsiSegment(默认)),
- (SegmentFactory::CreateSegment 有两种构造方法:Posix IPC
- NotifierFactory::CreateNotifier() 获取 Indicator 通知(有两种通知:共享内存
ConditionNotifier和 组播MulticastNotifier) - Receiver 将自身回调函数 注册进 单例Dispatcher。Dispatcher通过不断监听 通知消息。如果相关,则对对应的Segment读取数据。
共享内存分布
Segment

Segment 基类 关键成员:
- State* state_ 段状态指针(跨进程共享)
- Block* blocks_ 块数组指针
Block块级读写锁设计 写优先:通过 CAS 原子操作对 lock_num_ 在0(空闲),-1(写),>0(读)
- void* managed_shm_ 映射后的共享内存基址
- std::unordered_map<uint32_t, uint8_t*> block_buf_addrs_ index buf* 的地址表
关键成员函数:
- AcquireBlockToWrite 选择可写块、加写锁并返回
- 若 state_->need_remap() 或消息超出 ceiling_msg_size() ,分别走 Remap() 与 Recreate()
- GetNextWritableBlockIndex() 基于 State::seq_.fetch_add(1) 轮询选择下一个可写块,并尝试 Block::TryLockForWrite() ,失败则继续循环。
- ReleaseWrittenBlock(const WritableBlock&) : 释放对应块的写锁
- Block::ReleaseWriteLock() 释放块写锁
- AcquireBlockToRead(ReadableBlock*) : 按给定 index 加读锁并返回
- Block::TryLockForRead 尝试读锁
- ReleaseReadBlock(const ReadableBlock&) : 释放对应块的读锁
- Block::ReleaseReadLock 释放Block的读锁
Indicator

ConditionNotifier 单例 共享内存通知
所有的 ConditionNotifier 单例在 同一块 key 创建/获取 内存分布
cpp
//
int shmid = shmget(key_, shm_size_, 0644 | IPC_CREAT | IPC_EXCL);
//
void* managed_shm_ = shmat(shmid, nullptr, 0);
// 通过 replacement new 在 共享内存上创建 Indicator
cpp
// 删除共享内存
int shmid = shmget(key_, 0, 0644);
shmctl(shmid, IPC_RMID, 0)
// 断开共享内存
shmdt(managed_shm_);
小总结
- Segment 通过 Block块级锁,限制了Buffer的读写访问
- Indicator 用于通知,对于锁的控制较弱,允许多读者多写者共同访问,允许一定的脏数据的存在。
极端情况。虽然是一圈一圈的获取的readableinfo,如果写者太多了,则存在多个写者同时写入同一块的readableinfo的情况(可能存在复写的情况)。相应,每个读者都有自己记录独立的 next_seq_(表示自己下一块读的seq数组下标,只要检测到seq对应数组数值更大,就说明 readableinfo 有新的通知数据,获取 readableinfo 检查是否相关,进行下一步处理)- 为了解决上述问题,主要是由于
读的频率 跟不上 写的频率。代码里每50us进行一次检测。
ShmTransmitter
- Enable: 工厂类创建
- SegmentFactory::CreateSegment(channel_id_)
- 默认 XsiSegment
- NotifierFactory::CreateNotifier()
- 默认 ConditionNotifier
- SegmentFactory::CreateSegment(channel_id_)
- Transmit: 发送数据
-
Segment::AcquireBlockToWrite获取一块可写的Block(包含index、Block*、uint8_t* buffer指针)拿到的这块block加上写锁 ⭐ -
(模板类型
M+ MessageInfo 序列化写入uint8_t* buffer;给Block设置msg_size消息大小,msg_info_size附加消息大小,) -
释放此block的写锁
Segment::ReleaseWrittenBlock -
新建
ReadableInfo,NotifierBase::Notify通知
-
ShmReceiver
- 初始化: 通过 ShmDispatcher::Instance() 获取到全局单例的 ShmDispatcher
- Enable(): 呼应前文,通过 AddListener 把 基类OnNewMessage 添加到 ShmDispatcher。
ShmDispatcher
- Init:
- NotifierFactory::CreateNotifier 获取到全局单例 Notify
- 创建一个thread,执行 ShmDispatcher::ThreadFunc()
- AddListener:
- 子类将 模板参数 MessageT 重写封装一个回调(ReadableBock 序列化得到 MessageT,再执行回到)
- 再调用父类 AddListener
- AddSegment()
- ThreadFunc: 监听 Indicator
- 不断调用 ConditionNotifier::Listen,获取 ReadableInfo
- 判断 ReadableInfo 的 host_id 和 本机是否一致。获取到 channel_id 和 block_index
- 接下来,根据channel_id,获取到对应的Segment。通过 Segment::AcquireBlockToRead 获取到 Block的读锁以及对应的Buffer数据。
- ShmDispatcher::OnMessage ,通过 channel_id 获取到 对应的 ListenerHandler,执行对应的 ListenerHandler::Run,接着就是 ListenerHandler signal_广播通知以及,signals_[oppo_id]定向通知。
此时 当消息到来
- 触发的回调传入
listener_adapter(const std::shared_ptr<ReadableBlock> &rb,const MessageInfo &msg_info)
ReadableBlock 解析 MessageT- 执行
listener(const std::shared_ptr<MessageT> &, const MessageInfo &)
RTSP 设计
FastRTPS通信

在基于FastRTPS实现跨进程通信时,发送端和接收端需遵循特定流程完成配置,以实现数据通信。其具体步骤如下所述,
-
在发送端,首先要创建 RtpsParticipant。接着,创建RtpsWriter的配置信息实例并完成填充,随后创建 RtpsWriter和 RtpsWriterHistory,最后将 RtpsWriter 进行注册。准备工作完成后,就可以进行数据装载与发送。
-
在接收端,同样需要先创建RtpsParticipant,然后创建RtpsReader的配置信息实例并填充相关信息,创建RtpsReader并为其设置回调函数,再创建RtpsReaderHistory,完成这些步骤后,就能在接收到数据时执行回调操作。当发送端和接收端都按照上述流程配置完成双方即可开始通信。
