美文网首页
嵌入式RPC框架设计实践:六大核心类构建高效RPC框架

嵌入式RPC框架设计实践:六大核心类构建高效RPC框架

作者: 拂去尘世尘 | 来源:发表于2024-04-27 22:56 被阅读0次

    [TOC]

    引言

      在先前发布的文章中,我们构建了RPC底层数据传输的基础设计并实现了其功能(详尽代码与深入分析可参阅《实战高效RPC方案在嵌入式环境中的应用与揭秘》)。本文将继续以此为基础,探讨如何通过分层封装来提升RPC框架的易用性,旨在提供更便捷和正式的使用接口。

    概述

      在之前的文章中,我们阐述了结合共享内存与环形缓冲区技术,设计并实现了一种创新的共享环形缓冲区机制,用以支持RPC进程间高效的数据请求与响应交互。本篇文章将进一步依托此共享环形缓冲区的核心架构,专注于RPC框架的接口层次封装,力求精简对外接口,减轻使用者负担,从而实现实现服务进程与RPC框架之间的无缝集成与简便应用。

    需求

      针对专为嵌入式Linux环境定制的小型RPC框架,其核心功能需求可概括为以下几点,旨在实现高效、灵活且易于集成的远程通信解决方案:

    1. 自动服务注册与发现
      框架应在服务端启动时自动完成RPC接口的注册,同时,客户端需能动态识别并连接至可用服务端口,无缝调用指定服务接口。
    2. 极致性能与低延迟通讯
      强调从客户端请求发出到接收服务端响应的全链路快速响应,确保过程无明显阻塞,适合实时性要求高的嵌入式应用。
    3. 泛型数据序列化支持
      支持丰富数据类型的参数传递,通过高效的序列化与反序列化机制,保障各类数据在调用过程中的准确无损传输。
    4. 高度抽象与易用性设计
      框架设计应隐藏复杂的数据通信逻辑,为开发者提供简洁直观的API接口,使得远程方法调用如同调用本地函数一样直接和自然。

    注:由于是针对嵌入式环境定制的RPC框架设计,重点聚焦于核心实用功能,鉴于资源限制与特定场景需求,我们将注意力集中于以下基本需求,暂不涵盖如数据加密等高级安全特性和跨语言交互能力。

    类图

      针对上述需求的分析,以及RPC功能的理解。初步可将其分为6个类实现:BindingHubBindInterfaceBinderIBinderParcelSharedRingBuffer。类图如下:

    RPC框架类图
    • BindingHub (Bind中央协调器)
      该类核心职责在于维护一个全局的服务注册表,其中记录了所有服务端的远程调用元数据。其作为独立的服务运行在后台,扮演着信息维护的角色,向客户端提供查询服务端远程入口能力。

    • SharedRingBuffer (数据传输类)
      构成数据传输基础设施的关键组件,利用先进的共享内存技术结合高效的环形缓冲区设计,极大提升了进程间数据传输的速率与效率。此组件减少了系统调用的开销,特别是在资源受限的嵌入式环境中,其轻量级特性和高性能表现尤为显著。

    • Parcel (数据封装类)
      该类是数据封装与传输的核心实现,负责数据的序列化与反序列化操作,确保信息在不同进程间准确无误地流动。通过集成SharedRingBuffer,它不仅高效地利用共享内存与环形缓冲区技术完成数据交换,还实现了数据交互的同步控制,增强了传输的可靠性与一致性。

    • Binder (服务端交互标识)
      在服务端侧,该类扮演数据传输控制器的角色,依据服务标识生成对应的rspParcelreqParcel实例与客户端通信。此类接口封装在BindInterface中,服务侧代码无感知。

    • IBinder (客户端交互标识)
      在客户端侧,负责依据服务名称获取与之相匹配的rspParcelreqParcel实例与服务端通信。此类接口封装在BindInterface中,客户侧代码无感知。

    • BindInterface (Bind初始化接口类)
      此接口定义了RPC框架与外部应用交互的边界,为应用程序提供简洁的初始化接口。无论是服务端部署还是客户端查询适配,都通过此类返回类型安全的BinderIBinder实例。

    源码实现

    编程环境

    ① 编译环境: Linux环境
    ② 语言: C++语言

    接口定义

    • BindingHub (Bind中央协调器)
    class BindingHub
    {
    public:
        ~BindingHub();
    
        static BindingHub* GetInstance();
        int32_t HandleMsgLoop();
    
    private:
        BindingHub();
    
        int32_t EnvReady(const std::string& srvName);
        int32_t MsgResponseAddService();
        int32_t MsgResponseRemoveService();
        int32_t MsgResponseGetService();
    
    private:
        using HandleFunction = int32_t (BindingHub::*)(void);
    
        std::map<std::string, BinderInfo> mBinderMap;
        std::map<int32_t, HandleFunction> mHandleFuncs;
    };
    

    BindHub 维护了一个全局服务注册表mBinderMap,用于实现进程间服务的高效发现与通信。分为如下步骤描述:

    1. 步骤一:服务注册过程
      初始化请求
      当一个服务进程欲将其服务注册至系统中时,首先向BindHub发起注册请求。请求中包含了服务的唯一标识(进程名),作为服务辨识的基础信息。
      分配唯一标识
      接收到注册请求后,BindHub 会随即为该服务生成一个随机的、唯一的 key 值。用于为每个服务分配一个系统内的代理标识,便于后续的匿名化调用与管理。
      建立映射关系
      将生成的 key 与服务端进程名绑定,缓存至 mBinderMap 中。此步骤用于确定服务名与key之间的绑定关系,为后续查找与调用服务奠定了基础。
      响应确认
      完成映射关系的建立后,BindHub 将生成的 key 与服务名一并返回给服务端进程。服务端进程以key和服务名,创建共享内存和信号量,作为通信凭证,为即将到来的客户端请求做好准备。

    2. 步骤二:服务获取与通信
      客户端请求服务
      客户端进程启动服务调用前,需先通过BindHub请求指定服务。此请求中需包含服务的名称。
      查询服务信息
      接收到客户端的请求后,BindHub 在其维护的 mBinderMap 中依据服务名进行查找,获取与该服务关联的唯一key
      返回通信凭证
      查询到key后,BindHub 向客户端返回该服务的 name 与对应的 key。这两个元素共同构成了客户端与服务端通信的凭证,允许客户端直接且安全地与目标服务建立连接。
      建立直接通信
      客户端利用获得的 namekey,可直接与服务端进程建立点对点通信链路(共享内存和信号量),无需再经过 BindHub 中介,从而实现高效的进程间通信。

    • SharedRingBuffer (数据传输类)
      此类为共享环形缓冲区,用于存储通信数据,之前文章有详细说明,这里不再细说。

    • Parcel (数据封装类)
      此类用实现数据序列化和反序列化,然后通过SharedRingBuffer传输,业务比较简单。

    class Parcel
    {
    public:
        Parcel(const std::string& path, int key, bool master);
        ~Parcel();
        Parcel(const Parcel& other) = delete;
        Parcel& operator=(const Parcel& other) = delete;
        Parcel(Parcel&& other) = delete;
        Parcel& operator=(Parcel&& other) = delete;
    
        int Wait();
        int Post();
        int WriteBool(bool value);
        int ReadBool(bool& value);
        int WriteInt(int value);
        int ReadInt(int& value);
        int WriteString(const std::string& value);
        int ReadString(std::string& value);
        int WriteData(void* data, int size);
        int ReadData(void* data, int& size);
        
    private:
        bool                mMaster;
        int                 mShmKey;
        sem_t*              mSem ;
        std::string         mShmPath;
        SharedRingBuffer*   mRingBuffer;
    };
    
    • Binder (服务端交互标识)
        此类作用于服务端,通过服务名namekey生成两个Parecl实例:reqParcelrspParcel。作为通信桥梁与客户端进行交互。
    class Binder
    {
    public:
        Binder(const std::string& name, int key) : mKey(key), mName(name) {};
        ~Binder() {};
    
        int32_t GetParcel(std::shared_ptr<Parcel>& reqParcel, std::shared_ptr<Parcel>& rspParcel);
    
    private:
        int32_t mKey;
        std::string mName;
    };
    
    • IBinder (客户端交互标识)
        此类作用于客户端,作用和Binder相似,只是生成的是用于与服务端交互的两个Parcel实例。
        客户端的reqParecel负责向服务端rspParcel发数据,客户端的rspParcel负责接收服务端reqParecel的应答消息。从而实现客户端与服务端的全双工通信。
    class IBinder
    {
    public:
        IBinder(const std::string& name, int key) : mKey(key), mName(name) {};
        ~IBinder() {};
    
        int32_t GetParcel(std::shared_ptr<Parcel>& reqParcel, std::shared_ptr<Parcel>& rspParcel);
    
    private:
        int mKey;
        std::string mName;
    };
    
    • BindInterface (Bind初始化接口类)
        该类的作用在于抽象化BinderIBinder接口的复杂性,用户仅需通过初始化方法Initialize即可轻松获得预配置的Parcel实例,进而开展数据交换操作。
    class BindInterface
    {
    public:
        ~BindInterface() = default;
    
        static BindInterface* GetInstance();
        bool InitializeServiceBinder(const std::string& srvName, std::shared_ptr<Parcel>& pReqParcel, std::shared_ptr<Parcel>& pRspParcel);
        bool InitializeClientBinder(const std::string& srvName, std::shared_ptr<Parcel>& pReqParcel, std::shared_ptr<Parcel>& pRspParcel);
    
    private:
        BindInterface() = default;
        std::shared_ptr<Binder>  AddService(const std::string& name);
        std::shared_ptr<IBinder> GetService(const std::string& name);
        int32_t RemoveService(const std::string& name);
    };
    
    

    测试验证

    • 测试代码
      篇幅有限,这里只列举关键部分代码:
    int Client()
    {
    
        std::shared_ptr<Parcel> pReqParcel = nullptr;
        std::shared_ptr<Parcel> pRspParcel = nullptr;
    
        BindInterface::GetInstance()->InitializeClientBinder(SERVICE_NAME, pReqParcel, pRspParcel);
        if (pReqParcel == nullptr || pRspParcel == nullptr) {
            SPR_LOGE("GetParcel failed!\n");
            return -1;
        }
    
    ...
        pReqParcel->WriteInt(CMD_SUM);
        pReqParcel->WriteInt(10);
        pReqParcel->WriteInt(20);
        pReqParcel->Post();
    
        int sum = 0, ret = 0;
        pRspParcel->Wait();
        pRspParcel->ReadInt(sum);
        pRspParcel->ReadInt(ret);
        SPR_LOGD("sum = %d, ret = %d\n", sum, ret);
    ...
    }
    
    ...
    int Server()
    {
        std::shared_ptr<Parcel> pReqParcel = nullptr;
        std::shared_ptr<Parcel> pRspParcel = nullptr;
    
        BindInterface::GetInstance()->InitializeServiceBinder(SERVICE_NAME, pReqParcel, pRspParcel);
        if (pReqParcel == nullptr || pRspParcel == nullptr) {
            SPR_LOGE("GetParcel failed\n");
            return -1;
        }
    
        do {
            int cmd = 0;
            pReqParcel->Wait();
            pReqParcel->ReadInt(cmd);
            switch(cmd)
            {
                case CMD_SUM:
                {
                    SPR_LOGD("CMD_SUM\n");
                    int a = 0, b = 0;
                    pReqParcel->ReadInt(a);
                    pReqParcel->ReadInt(b);
    
                    int sum = a + b;
                    pRspParcel->WriteInt(sum);
                    pRspParcel->WriteInt(0);
                    pRspParcel->Post();
                    break;
                }
    
                default:
                {
                    SPR_LOGE("Unknown cmd: %d\n", cmd);
                    break;
                }
            }
    
        } while(1);
    
        return 0;
    }
    
    • 测试结果
    $ ./debugbinder
    ------------------------------------------------------------------
    Usage:
    0: CMD_TEST
    1: CMD_SUM
    q: Quit
    ------------------------------------------------------------------
    146 DebugBinder D: Input:1
    173 DebugBinder D: sum = 30, ret = 0
    

    这里只是一个简单测试,客户端发起请求,并同步获取服务端返回值30,初步验证RPC功能OK。

    总结

    • 先前探讨了SharedRingbuffer在数据传输中的低层机制。本文章重心主要聚焦软件设计,旨在简化RPC框架的使用体验,提升易用性。

    • RPC通信核心在于参数传递与返回值的高效同步。该过程依托共享内存与信号量技术,确保数据流畅传输与操作同步,提升交互效率。

    • RPC框架的实践不仅为项目提供便捷工具,也能够深入了解RPC技术底层逻辑与原理,从技术和设计的层面提升自己。

    相关文章

      网友评论

          本文标题:嵌入式RPC框架设计实践:六大核心类构建高效RPC框架

          本文链接:https://www.haomeiwen.com/subject/trqzxjtx.html