探索C++中的轻量级RPC:打造高性能网络通信

开发 前端
我们可以直观地验证 RPC 框架的正确性和有效性。客户端能够像调用本地函数一样,方便地调用服务端的方法,并且能够正确地传递参数和获取返回结果。

在当今数字化浪潮中,分布式系统已成为构建大规模、高性能应用的基石。而远程过程调用(RPC),作为分布式系统中的关键技术,宛如一座桥梁,连接着不同服务器上的服务,使得它们能够协同工作,为用户提供无缝的体验。想象一下,你正在使用一款热门的在线购物应用。当你点击 “加入购物车” 按钮时,背后的系统需要与多个服务进行交互,包括库存管理服务、用户信息服务等。RPC 就像一位幕后英雄,让这些分布在不同服务器上的服务之间的通信变得简单高效,仿佛它们都运行在同一台机器上。

然而,构建一个高效、可靠的 RPC 框架并非易事。不同的编程语言、网络环境、服务需求等,都给 RPC 的实现带来了巨大的挑战。今天,我们就来探讨如何用 C++ 打造一个轻量级的 RPC 分布式网络通信框架,看看它是如何在复杂的环境中实现高效通信的。

一、RPC分布式简介

1.1概述

RPC,即远程过程调用(Remote Procedure Call) ,是一种让程序在不同计算机之间像调用本地函数一样进行通信的技术。打个比方,你去餐厅点餐,服务员就像是本地调用,你直接告诉服务员你想吃什么,他能马上响应。而如果这家餐厅的厨房在另一个地方,你通过对讲机向厨房点餐,这个过程就类似 RPC。你不需要知道对讲机是如何工作、信号如何传输的,只要像和身边的服务员沟通一样点餐就行。在分布式系统中,不同的服务可能部署在不同的服务器上,RPC 就像是这个 “对讲机”,让不同服务器上的服务之间能够轻松地进行通信和交互。

在分布式系统中,各个服务分布在不同的节点上,为了实现它们之间的协同工作,进程间通信至关重要。传统的进程间通信方式,如 Socket,需要开发者深入了解网络编程细节,包括连接建立、数据传输、序列化与反序列化等,这无疑增加了开发的难度和复杂性。而 RPC 则通过将远程调用抽象成类似本地调用的形式,极大地简化了分布式系统的开发过程。开发者可以专注于业务逻辑的实现,而无需过多关注底层网络通信的细节,从而提高开发效率,降低出错的概率。RPC通信框架的大致结构流程图如下:

图片图片

⑴ZooKeeper

ZooKeeper在这里作为服务方法的管理配置中心,负责管理服务方法提供者对外提供的服务方法。服务方法提供者提前将本端对外提供的服务方法名及自己的通信地址信息(IP:Port)注册到ZooKeeper。当Caller发起远端调用时,会先拿着自己想要调用的服务方法名询问ZooKeeper,ZooKeeper告知Caller想要调用的服务方法在哪台服务器上(ZooKeeper返回目标服务器的IP:Port给Caller),Caller便向目标服务器Callee请求服务方法调用。服务方在本地执行相应服务方法后将结果返回给Caller。

⑵ProtoBuf

ProtoBuf能提供对数据的序列化和反序列化,ProtoBuf可以用于结构化数据的串行序列化,并且以Key-Value格式存储数据,因为采用二进制格,所以序列化出来的数据比较少,作为网络传输的载体效率很高。

Caller和Callee之间的数据交互就是借助ProtoBuf完成,具体的使用方法和细节后面会进一步拓展。

⑶Muduo

Muduo库是基于(Multi-)Reactor模型的多线程网络库,在RPC通信框架中涉及到网络通信。另外我们可以服务提供方实现为IO多线程,实现高并发处理远端服务方法请求。

1.2常见RPC 框架

在 RPC 的世界里,有许多优秀的框架,它们各有千秋。

gRPC,由 Google 开发并开源,基于 HTTP/2 协议和 Protocol Buffers(Protobuf)序列化协议。它就像一个全能选手,支持多种编程语言,如 Java、Go、C++、Python 等。基于 HTTP/2,它具备双向流、多路复用、头部压缩和请求优先级等特性,传输效率极高。使用 Protobuf 作为序列化协议,使得数据传输高效且安全。gRPC 适用于对性能要求极高、需要跨语言支持和强类型约束的分布式系统,比如大规模互联网应用、金融系统等。

Thrift,由 Facebook 开发并捐赠给 Apache,是一个跨语言的高效 RPC 框架。它支持多种序列化格式和传输协议,扩展性超强。就像一个百变星君,能适应各种复杂的多语言环境,在数据平台、异构服务集成等场景中表现出色。

Dubbo阿里巴巴开源的高性能 Java RPC 框架,主要用于构建微服务架构中的服务调用和治理。它支持多种通信协议,具备强大的服务治理能力,如服务注册与发现、负载均衡、限流、熔断降级等。Dubbo 就像是一位贴心的管家,在 Java 技术栈下的高性能微服务架构中,尤其是需要复杂服务治理功能的企业应用中,发挥着重要作用 。

二、C++实现RPC分布式原理

2.1性能卓越:快人一步

C++ 以其卓越的性能在编程语言中独树一帜,这一特性在构建 RPC 分布式网络通信框架时更是发挥得淋漓尽致。从执行效率来看,C++ 作为一种编译型语言,能够直接将代码编译成机器码,这使得程序在运行时无需像解释型语言那样进行逐行解释,大大减少了运行时的开销。在对实时性要求极高的金融交易系统中,每毫秒的延迟都可能导致巨大的损失。使用 C++ 实现的 RPC 框架,能够快速处理大量的交易请求,确保交易信号的及时传递和执行,为系统的高效运行提供了坚实保障。

C++ 在资源利用方面也表现出色。它赋予开发者对内存的精细控制权,开发者可以根据实际需求精确地分配和释放内存,避免了内存泄漏和不必要的内存占用。这在分布式系统中尤为重要,因为分布式系统通常需要处理大量的数据和并发请求,对内存的合理利用能够有效提升系统的整体性能和稳定性。以一个大规模的电商系统为例,在促销活动期间,系统会面临海量的用户请求,C++ 实现的 RPC 框架能够高效地管理内存,确保系统在高并发的情况下依然能够稳定运行,为用户提供流畅的购物体验。

有数据表明,在处理大规模数据传输和复杂计算任务时,C++ 实现的 RPC 框架相比一些其他语言实现的框架,性能提升可达 30% - 50%。这一显著的性能优势,使得 C++ 成为追求高性能 RPC 框架的首选语言 。

2.2灵活定制:量体裁衣

C++ 的强大可定制性,使其能够像一位技艺精湛的裁缝,根据不同场景的特殊需求,为 RPC 框架量身定制解决方案。

在通信协议方面,C++ 给予开发者极大的自由度。开发者可以根据应用场景的特点,如数据量大小、传输频率、网络环境等,选择或设计最适合的通信协议。在网络环境复杂、带宽有限的情况下,开发者可以设计一种轻量级的自定义通信协议,减少数据传输的开销,提高传输效率。而对于对安全性要求极高的场景,开发者可以基于现有的安全协议,如 SSL/TLS,进行定制化开发,确保数据在传输过程中的安全性。

对于数据序列化和反序列化方式,C++ 同样提供了丰富的选择。常见的序列化方式如 JSON、XML、Protocol Buffers 等,都可以在 C++ 中轻松实现。开发者可以根据数据的结构和应用场景的需求,选择最适合的序列化方式。如果数据结构较为复杂,且对传输效率要求较高,Protocol Buffers 可能是一个不错的选择,因为它能够将数据高效地编码为二进制格式,减少数据传输的大小,提高传输速度。而如果数据需要与其他系统进行交互,且对可读性有一定要求,JSON 则可能更为合适,因为它的格式较为简洁,易于阅读和解析。

在不同的应用场景中,C++ 的灵活定制性得到了充分的体现。在游戏开发中,由于游戏对实时性和性能要求极高,开发者可以使用 C++ 定制一个高效的 RPC 框架,优化网络通信,减少延迟,为玩家提供流畅的游戏体验。在工业自动化领域,由于不同的设备和系统具有不同的通信需求,C++ 的可定制性使得开发者能够为每个设备和系统定制专属的 RPC 框架,实现设备之间的高效通信和协同工作 。

三、核心实现步骤

3.1基础搭建:打牢根基

在构建这个轻量级 RPC 分布式网络通信框架时,我们需要一些趁手的工具,ZooKeeper、ProtoBuf 和 Muduo 库便是我们的得力助手。

ZooKeeper客户端(Callee)首先将Watcher注册到服务端,同时把Watcher对象保存到客户端的Watcher管理器中。当ZooKeeper服务端监听到ZooKeeper中的数据状态发生变化时,服务端主动通知客户端(告知客户端事件类型和状态类型),接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑(GlobalWatcher),从而完成整体的数据发布/订阅流程。

图片

Watcher的设置和获取在开发中很常见,不同的操作会收到不同的watcher信息。更多内容还是自行google吧,我自己还只有半桶水的功夫。日后会继续学习,专门对ZooKeeper做一个全面细致的剖析。

ProtoBuf,即 Protocol Buffers,是 Google 开发的一种数据序列化协议。它就像一个高效的翻译官,能够将结构化数据进行序列化和反序列化。在我们的框架中,客户端和服务端之间的数据交互就是借助 ProtoBuf 完成的。它采用二进制格式存储数据,序列化出来的数据量少,作为网络传输的载体效率极高。比如在定义一个用户登录请求时,使用 ProtoBuf 可以将用户名和密码等信息高效地编码为二进制格式进行传输,在接收端又能快速地解码还原 。

Muduo 库是基于 (Multi-) Reactor 模型的多线程网络库,在 RPC 通信框架中主要负责网络通信部分。它可以将服务提供方实现为 IO 多线程,从而实现高并发处理远端服务方法请求。在处理大量客户端同时请求服务的场景中,Muduo 库能够高效地管理网络连接和数据传输,确保系统的稳定性和高性能 。

3.2业务层实现:注入灵魂

以一个简单的用户登录和注册业务场景为例,我们来看看如何用 ProtoBuf 定义数据结构,实现业务层代码。

首先,使用 ProtoBuf 定义数据结构。假设我们有一个用户登录的场景,需要定义登录请求消息体和登录响应消息体。在.proto 文件中,可以这样定义:

syntax = "proto3";
package user;

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  bool success = 1;
  string message = 2;
}

service UserService {
  rpc Login(LoginRequest) returns (LoginResponse);
}

在这段代码中,我们定义了LoginRequest消息体,包含username和password两个字段,用于客户端向服务端发送登录请求。LoginResponse消息体则包含success字段表示登录是否成功,message字段用于返回提示信息。UserService服务定义了一个Login方法,接受LoginRequest并返回LoginResponse 。

然后,使用protoc工具编译这个.proto 文件,生成对应的 C++ 代码。编译命令如下:

protoc --cpp_out=. user.proto

编译后会生成user.pb.h和http://user.pb.cc文件,其中包含了用于 C++ 程序使用的类和方法。在客户端代码中,可以这样调用服务端的Login方法:

#include <iostream>
#include "mprpcapplication.h"
#include "user.pb.h"
#include "mprpcchannel.h"

int main(int argc, char** argv) {
    MprpcApplication::Init(argc, argv);
    user::UserService_Stub stub(new MprpcChannel());
    user::LoginRequest request;
    request.set_username("张三");
    request.set_password("123456");
    user::LoginResponse response;
    stub.Login(nullptr, &request, &response, nullptr);
    if (response.success()) {
        std::cout << "登录成功" << std::endl;
    } else {
        std::cout << "登录失败: " << response.message() << std::endl;
    }
    return 0;
}

在这段代码中,我们首先初始化MprpcApplication,然后创建UserService_Stub对象,并设置登录请求的参数。接着调用Login方法,将请求发送到服务端,并获取响应结果。根据响应结果判断登录是否成功 。

3.3服务端构建:撑起后台

在服务端,我们需要创建一个RpcProvider类,来实现服务的注册与发布。

首先,定义RpcProvider类的基本结构。在rpcprovider.h文件中,可以这样定义:

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <unordered_map>
#include <google/protobuf/service.h>

class RpcProvider {
public:
    void NotifyService(google::protobuf::Service* service);
    void Run();
private:
    muduo::net::EventLoop* m_eventLoop;
    muduo::net::TcpServer* m_tcpServer;
    std::unordered_map<std::string, google::protobuf::Service*> m_serviceMap;
};

在这个类中,NotifyService方法用于注册服务,Run方法用于启动服务端。m_eventLoop和m_tcpServer分别用于管理事件循环和创建 TCP 服务器。m_serviceMap用于存储注册的服务 。

然后,实现NotifyService方法。在http://rpcprovider.cc文件中,代码如下:

void RpcProvider::NotifyService(google::protobuf::Service* service) {
    google::protobuf::ServiceDescriptor* pserviceDesc = service->GetDescriptor();
    std::string service_name = pserviceDesc->name();
    m_serviceMap[service_name] = service;
    std::cout << "发布服务: " << service_name << std::endl;
}

在这个方法中,我们首先获取服务的描述信息,然后将服务名称和服务对象存储到m_serviceMap中 。

最后,实现Run方法。代码如下:

void RpcProvider::Run() {
    std::string ip = MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");
    uint16_t port = atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());
    muduo::net::InetAddress address(ip, port);
    m_eventLoop = new muduo::net::EventLoop();
    m_tcpServer = new muduo::net::TcpServer(m_eventLoop, address, "RpcProvider");
    m_tcpServer->setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
    m_tcpServer->setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    m_tcpServer->setThreadNum(4);
    std::cout << "RpcProvider start service at ip: " << ip << " port: " << port << std::endl;
    m_tcpServer->start();
    m_eventLoop->loop();
}

在这个方法中,我们首先从配置文件中读取服务端的 IP 和端口,然后创建InetAddress对象和TcpServer对象。接着设置连接回调函数和消息回调函数,并设置线程数量。最后启动服务器,开始监听客户端的请求 。

3.4客户端构建:打造前台

在客户端,我们需要创建一个RpcChannel类,来实现客户端对服务端的远程调用。

首先,定义RpcChannel类的基本结构。在rpcchannel.h文件中,可以这样定义:

#include <google/protobuf/channel.h>
#include <google/protobuf/message.h>
#include <muduo/net/TcpClient.h>
#include <muduo/net/EventLoop.h>

class RpcChannel : public google::protobuf::RpcChannel {
public:
    RpcChannel(muduo::net::EventLoop* loop, const std::string& ip, uint16_t port);
    void CallMethod(const google::protobuf::MethodDescriptor* method,
                    google::protobuf::RpcController* controller,
                    const google::protobuf::Message* request,
                    google::protobuf::Message* response,
                    google::protobuf::Closure* done) override;
private:
    muduo::net::TcpClient* m_tcpClient;
    muduo::net::EventLoop* m_eventLoop;
    std::string m_ip;
    uint16_t m_port;
};

在这个类中,RpcChannel构造函数用于初始化客户端,CallMethod方法用于实现远程调用。m_tcpClient和m_eventLoop分别用于管理 TCP 客户端和事件循环。m_ip和m_port用于存储服务端的 IP 和端口 。

然后,实现RpcChannel类的构造函数。在http://rpcchannel.cc文件中,代码如下:

RpcChannel::RpcChannel(muduo::net::EventLoop* loop, const std::string& ip, uint16_t port)
    : m_eventLoop(loop), m_ip(ip), m_port(port) {
    m_tcpClient = new muduo::net::TcpClient(m_eventLoop, muduo::net::InetAddress(m_ip, m_port));
    m_tcpClient->enableRetry();
}

在这个构造函数中,我们创建TcpClient对象,并设置自动重试功能 。

最后,实现CallMethod方法。代码如下:

void RpcChannel::CallMethod(const google::protobuf::MethodDescriptor* method,
                            google::protobuf::RpcController* controller,
                            const google::protobuf::Message* request,
                            google::protobuf::Message* response,
                            google::protobuf::Closure* done) {
    std::string service_name = method->service()->full_name();
    std::string method_name = method->name();
    uint32_t args_size = request->ByteSizeLong();
    std::string args_str;
    request->SerializeToString(&args_str);
    muduo::net::TcpConnectionPtr conn = m_tcpClient->getConnection();
    if (conn) {
        conn->send(service_name + method_name + std::to_string(args_size) + args_str);
        conn->setReadCallback([this, response, done](const muduo::net::TcpConnectionPtr&, muduo::net::Buffer* buffer) {
            std::string response_str = buffer->retrieveAllAsString();
            response->ParseFromString(response_str);
            done->Run();
        });
    }
}

在这个方法中,我们首先获取服务名称和方法名称,然后将请求数据序列化并发送到服务端。接着设置读取回调函数,当接收到服务端的响应数据时,将其反序列化并存储到response中,最后调用done回调函数 。

四、实例展示:眼见为实

4.1完整代码剖析

下面是一个完整的简单示例代码,用于更清晰地展示 C++ 实现的轻量级 RPC 分布式网络通信框架的运行机制。

首先,定义服务接口。在user.proto文件中,定义如下:

syntax = "proto3";
package user;

message LoginRequest {
  string username = 1;
  string password = 2;
}

message LoginResponse {
  bool success = 1;
  string message = 2;
}

service UserService {
  rpc Login(LoginRequest) returns (LoginResponse);
}

在这个文件中,我们定义了LoginRequest消息体,包含用户名和密码两个字段,用于客户端向服务端发送登录请求。LoginResponse消息体则包含登录是否成功的标志以及相应的提示信息。UserService服务定义了一个Login方法,接受LoginRequest类型的请求,并返回LoginResponse类型的响应。

接着,使用protoc工具编译user.proto文件,生成对应的 C++ 代码。编译命令如下:

protoc --cpp_out=. user.proto

编译后会生成user.pb.h和http://user.pb.cc文件,这两个文件包含了用于 C++ 程序使用的类和方法,是实现 RPC 通信的基础。

服务端代码如下:

#include <iostream>
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <google/protobuf/service.h>
#include "user.pb.h"
#include "rpcprovider.h"

class UserServiceImpl : public user::UserService::Service {
public:
    void Login(::google::protobuf::RpcController* controller,
               const ::user::LoginRequest* request,
               ::user::LoginResponse* response,
               ::google::protobuf::Closure* done) override {
        std::string username = request->username();
        std::string password = request->password();
        if (username == "admin" && password == "123456") {
            response->set_success(true);
            response->set_message("登录成功");
        } else {
            response->set_success(false);
            response->set_message("用户名或密码错误");
        }
        done->Run();
    }
};

int main(int argc, char* argv[]) {
    RpcProvider provider;
    provider.NotifyService(new UserServiceImpl());
    provider.Run();
    return 0;
}

在这段服务端代码中,我们定义了UserServiceImpl类,继承自user::UserService::Service,实现了Login方法。在Login方法中,根据接收到的用户名和密码进行验证,并设置相应的响应结果。main函数中,创建RpcProvider对象,注册UserServiceImpl服务,并启动服务端。

客户端代码如下:

#include <iostream>
#include "mprpcapplication.h"
#include "user.pb.h"
#include "mprpcchannel.h"

int main(int argc, char** argv) {
    MprpcApplication::Init(argc, argv);
    user::UserService_Stub stub(new MprpcChannel());
    user::LoginRequest request;
    request.set_username("admin");
    request.set_password("123456");
    user::LoginResponse response;
    stub.Login(nullptr, &request, &response, nullptr);
    if (response.success()) {
        std::cout << "登录成功: " << response.message() << std::endl;
    } else {
        std::cout << "登录失败: " << response.message() << std::endl;
    }
    return 0;
}

客户端代码中,首先初始化MprpcApplication,然后创建UserService_Stub对象,并设置登录请求的参数。接着调用Login方法,将请求发送到服务端,并获取响应结果。最后根据响应结果输出相应的信息。

4.2运行效果呈现

运行服务端代码后,服务端会启动并监听指定的端口,等待客户端的请求。当客户端代码运行时,它会向服务端发送一个登录请求,携带用户名和密码。服务端接收到请求后,进行验证,并返回相应的响应。

在客户端的控制台输出中,我们可以看到:

登录成功: 登录成功

这表明客户端成功调用了服务端的Login方法,并且服务端验证通过,返回了成功的响应。如果将客户端的用户名或密码修改为错误的值,如:

request.set_username("admin");
request.set_password("wrongpassword");

再次运行客户端代码,控制台输出将变为:

登录失败: 用户名或密码错误

通过这样的运行结果,我们可以直观地验证 RPC 框架的正确性和有效性。客户端能够像调用本地函数一样,方便地调用服务端的方法,并且能够正确地传递参数和获取返回结果。这充分展示了 C++ 实现的轻量级 RPC 分布式网络通信框架在简化分布式系统开发方面的强大能力 。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2021-10-27 11:29:32

框架Web开发

2019-09-25 08:25:49

RPC网络通信

2020-10-13 18:09:22

开发框架开源

2024-10-31 10:03:17

2019-10-22 08:11:43

Socket网络通信网络协议

2020-09-04 09:27:40

开源C++搜狗

2024-11-05 18:34:27

2022-12-05 09:25:17

Kubernetes网络模型网络通信

2020-11-12 08:52:16

Python

2024-04-26 09:13:34

RPCHTTP协议

2023-06-19 07:54:37

DotNetty网络通信框架

2024-02-20 19:53:57

网络通信协议

2009-08-24 17:20:13

C#网络通信TCP连接

2024-01-03 07:42:49

分割模型高性能

2025-01-15 08:56:53

2017-01-15 17:44:56

node网络通信Socket

2017-10-11 16:12:19

内存

2023-12-12 13:50:00

代码业务状态

2024-06-07 10:34:28

Rust开发工具

2024-05-27 00:40:00

C++bitset
点赞
收藏

51CTO技术栈公众号