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

本文涉及的产品
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 在先前的文章基础上,本文讨论如何通过分层封装提升一个针对嵌入式Linux的RPC框架的易用性。设计包括自动服务注册、高性能通信、泛型序列化和简洁API。框架分为6个关键类:BindingHub、SharedRingBuffer、Parcel、Binder、IBinder和BindInterface。BindingHub负责服务注册,SharedRingBuffer实现高效数据传输,Parcel处理序列化,而Binder和IBinder分别用于服务端和客户端交互。BindInterface提供简单的初始化接口,简化应用集成。测试案例展示了客户端和服务端的交互,验证了RPC功能的有效性。

[TOC]

引言

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

概述

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

需求

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

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


## 类图

  针对上述需求的分析,以及RPC功能的理解。初步可将其分为6个类实现:```BindingHub```、```BindInterface```、```Binder```、```IBinder```、```Parcel```、```SharedRingBuffer```。类图如下:

![RPC框架类图](https://files.mdnice.com/user/6284/5536be4d-a6d4-43fa-8118-6fbcbd490d2f.jpg)

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

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

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

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

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

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

## 源码实现   

**编程环境**    

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

**接口定义**     

* **BindingHub (Bind中央协调器)**  
```c++
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传输,业务比较简单。
    ```C++
    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 (服务端交互标识)**       
&emsp; 此类作用于服务端,通过服务名`name`和`key`生成两个`Parecl`实例:`reqParcel`和`rspParcel`。作为通信桥梁与客户端进行交互。
```c++
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的应答消息。从而实现客户端与服务端的全双工通信。
    ```C++
    class IBinder
    {
    public:
    IBinder(const std::string& name, int key) : mKey(key), mName(name) {};
    ~IBinder() {};

    int32_t GetParcel(std::shared_ptr& reqParcel, std::shared_ptr& rspParcel);

private:
int mKey;
std::string mName;
};


* **BindInterface (Bind初始化接口类)**     
&emsp; 该类的作用在于抽象化`Binder`和`IBinder`接口的复杂性,用户仅需通过初始化方法`Initialize`即可轻松获得预配置的Parcel实例,进而开展数据交换操作。
```C++
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);
};

测试验证

  • 测试代码
    篇幅有限,这里只列举关键部分代码:
    ```C++
    int Client()
    {

    std::shared_ptr pReqParcel = nullptr;
    std::shared_ptr 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 pReqParcel = nullptr;
std::shared_ptr 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;

}


* 测试结果    
```shell
$ ./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技术底层逻辑与原理,从技术和设计的层面提升自己。

相关文章
|
1月前
|
自然语言处理 负载均衡 API
gRPC 一种现代、开源、高性能的远程过程调用 (RPC) 可以在任何地方运行的框架
gRPC 是一种现代开源高性能远程过程调用(RPC)框架,支持多种编程语言,可在任何环境中运行。它通过高效的连接方式,支持负载平衡、跟踪、健康检查和身份验证,适用于微服务架构、移动设备和浏览器客户端连接后端服务等场景。gRPC 使用 Protocol Buffers 作为接口定义语言,支持四种服务方法:一元 RPC、服务器流式处理、客户端流式处理和双向流式处理。
|
4月前
|
Dubbo 网络协议 Java
RPC框架:一文带你搞懂RPC
这篇文章全面介绍了RPC(远程过程调用)的概念、原理和应用场景,解释了RPC如何工作以及为什么在分布式系统中广泛使用,并探讨了几种常用的RPC框架如Thrift、gRPC、Dubbo和Spring Cloud,同时详细阐述了RPC调用流程和实现透明化远程服务调用的关键技术,包括动态代理和消息的编码解码过程。
RPC框架:一文带你搞懂RPC
|
3月前
|
XML 负载均衡 监控
分布式-dubbo-简易版的RPC框架
分布式-dubbo-简易版的RPC框架
|
4月前
|
XML 存储 JSON
(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解
如今这个分布式风靡的时代,网络通信技术,是每位技术人员必须掌握的技能,因为无论是哪种分布式技术,都离不开心跳、选举、节点感知、数据同步……等机制,而究其根本,这些技术的本质都是网络间的数据交互。正因如此,想要构建一个高性能的分布式组件/系统,不得不思考一个问题:怎么才能让数据传输的速度更快?
113 1
|
5月前
|
分布式计算 负载均衡 数据安全/隐私保护
什么是RPC?有哪些RPC框架?
RPC(Remote Procedure Call,远程过程调用)是一种允许运行在一台计算机上的程序调用另一台计算机上子程序的技术。这种技术屏蔽了底层的网络通信细节,使得程序间的远程通信如同本地调用一样简单。RPC机制使得开发者能够构建分布式计算系统,其中不同的组件可以分布在不同的计算机上,但它们之间可以像在同一台机器上一样相互调用。
165 8
|
5月前
|
网络协议 Dubbo Java
什么是RPC?RPC和HTTP对比?RPC有什么缺点?市面上常用的RPC框架?
选择合适的RPC框架和通信协议,对于构建高效、稳定的分布式系统至关重要。开发者需要根据自己的业务需求和系统架构,综合考虑各种因素,做出适宜的技术选型。
507 1
|
7月前
|
负载均衡 Dubbo Java
Dubbo 3.x:探索阿里巴巴的开源RPC框架新技术
随着微服务架构的兴起,远程过程调用(RPC)框架成为了关键组件。Dubbo,作为阿里巴巴的开源RPC框架,已经演进到了3.x版本,带来了许多新特性和技术改进。本文将探讨Dubbo 3.x中的一些最新技术,包括服务注册与发现、负载均衡、服务治理等,并通过代码示例展示其使用方式。
400 9
|
7月前
|
JSON 负载均衡 网络协议
Rpc编程系列文章第二篇:RPC框架设计目标
Rpc编程系列文章第二篇:RPC框架设计目标
|
7月前
|
设计模式 负载均衡 网络协议
【分布式技术专题】「分布式技术架构」实践见真知,手把手教你如何实现一个属于自己的RPC框架(架构技术引导篇)
【分布式技术专题】「分布式技术架构」实践见真知,手把手教你如何实现一个属于自己的RPC框架(架构技术引导篇)
296 0
|
7月前
|
Dubbo Java 应用服务中间件
Rpc编程系列文章第三篇:Hessian RPC一个老的RPC框架
Rpc编程系列文章第三篇:Hessian RPC一个老的RPC框架
下一篇
DataWorks