CyberRT Transport传输层设计

前言

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 的派生子类。

三种功能:

  1. Receiver注册管理:Dispatcher 负责管理所有的Receiver实例,确保每个Receiver能够正确接收并处理其订阅的消息。
  2. 消息获取与转发:Dsipatcher 需要从不同共享主题中获取数据,并根据消息内容将其转发到合适的Receiver
  3. 回调函数触发

关键成员:

  • 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 子类 信号槽机制

信号槽机制,基类 连接状态

两种绑定关系:

  1. 广播绑定
  2. 定向绑定

Connect 提供了两种绑定,如果只提供self_id和回调,那么注册到signal_统一的(只要有消息就会广播触发所有)

如果额外提供 oppo_id,那么当 对应的消息来了,单独触发。

SHM 设计思路

  • Transmitter会根据channel_id_作为key生成一块Segment共享内存
    • (SegmentFactory::CreateSegment 有两种构造方法:Posix IPC PosixSegment和System V IPC XsiSegment(默认)),
  • 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
  • Transmit: 发送数据
    • Segment::AcquireBlockToWrite 获取一块可写的Block(包含index、Block*、uint8_t* buffer指针)拿到的这块block加上写锁 ⭐

    • (模板类型M + MessageInfo 序列化写入uint8_t* buffer;给Block设置msg_size消息大小,msg_info_size附加消息大小,)

    • 释放此block的写锁 Segment::ReleaseWrittenBlock

    • 新建 ReadableInfoNotifierBase::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]定向通知。

此时 当消息到来

  1. 触发的回调传入listener_adapter(const std::shared_ptr<ReadableBlock> &rb,const MessageInfo &msg_info)
    ReadableBlock 解析 MessageT
  2. 执行 listener(const std::shared_ptr<MessageT> &, const MessageInfo &)

RTSP 设计

FastRTPS通信

在基于FastRTPS实现跨进程通信时,发送端和接收端需遵循特定流程完成配置,以实现数据通信。其具体步骤如下所述,

  • 在发送端,首先要创建 RtpsParticipant。接着,创建RtpsWriter的配置信息实例并完成填充,随后创建 RtpsWriter和 RtpsWriterHistory,最后将 RtpsWriter 进行注册。准备工作完成后,就可以进行数据装载与发送。

  • 在接收端,同样需要先创建RtpsParticipant,然后创建RtpsReader的配置信息实例并填充相关信息,创建RtpsReader并为其设置回调函数,再创建RtpsReaderHistory,完成这些步骤后,就能在接收到数据时执行回调操作。当发送端和接收端都按照上述流程配置完成双方即可开始通信。

相关推荐
Moe4881 小时前
合并Pdf、excel、图片、word为单个Pdf文件的工具类(技术点的选择与深度解析)
java·后端
Java水解1 小时前
20个高级Java开发面试题及答案!
spring boot·后端·面试
Moe4881 小时前
合并Pdf、excel、图片、word为单个Pdf文件的工具类(拿来即用版)
java·后端
bcbnb1 小时前
手机崩溃日志导出的工程化方法,构建多工具协同的跨平台日志获取与分析体系(iOS/Android 全场景 2025 进阶版)
后端
Java水解2 小时前
为何最终我放弃了 Go 的 sync.Pool
后端·go
二川bro2 小时前
第41节:第三阶段总结:打造一个AR家具摆放应用
后端·restful
aiopencode2 小时前
苹果应用商店上架全流程 从证书体系到 IPA 上传的跨平台方法
后端
百***86052 小时前
Spring BOOT 启动参数
java·spring boot·后端
wei_shuo2 小时前
基于Linux平台的openGauss一主两备高可用集群部署与运维实践研究
后端