手写rpc远程过程调用框架

这是一个模仿grpc,基于TCP协议和protobuf编解码的RPC框架,支持多种数据类型的序列化和反序列化,使用muduo网络库实现高并发处理,使用zookeeper作为服务配置中心。同时,采用了函数特征的模板类和protobuf框架自动生成代码等技术,使得代码实现简洁高效。

源码地址

https://gitee.com/to-the-sea/rpcframe/

rpc通信原理

在这里插入图片描述
黄色部分:设计rpc方法参数的打包和解析,也就是数据的序列化和反序列化,使用Protobuf。
绿色部分:网络部分,包括寻找rpc服务主机,发起rpc调用请求和响应rpc调用结果,使用muduo网络库和zookeeper服务配置中心(专门做服务发现)。
rpc框架主要包含以上两个部分的内容

rpcframe项目时序图

以客户端调用UserServerRpc服务的Login方法为例,绘制时序图如下:
在这里插入图片描述
这张时序图展示了RPC 调用过程。它包括三个主要部分:RPC Provider、RPC Channel 和 UserServiceRpc_Stub。

RPC Provider 是服务端,负责注册 RPC 服务,为客户端提供远程调用功能。它需要提前配置 RPC Config,并启动 ZooKeeper Client。一旦 ZooKeeper Client 连接到 ZooKeeper Server,RPC Provider 就会向 ZooKeeper Server 注册它的地址和端口,使得客户端可以找到它。

RPC Channel 是客户端和服务端之间通信的桥梁,用于将客户端的请求序列化并通过网络发送到 RPC Provider,然后将 RPC Provider 的响应序列化并返回给客户端。

UserServiceRpc_Stub 是客户端调用 RPC 服务的接口,它会将调用请求封装成对应的 protobuf 消息,通过 RPC Channel 发送给 RPC Provider,然后等待 RPC Provider 的响应并将其解析。

在这张图中,当客户端调用 UserServiceRpc_Stub 的 Login() 方法时,它会将参数封装成 LoginRequest 消息,并调用 RPC Channel 的 CallMethod() 方法将请求发送给 RPC Provider。RPC Provider 接收到请求后,将请求参数反序列化并调用本地的 Login() 方法进行处理。在处理完成后,RPC Provider 将响应结果封装成 LoginResponse 消息,并通过 RPC Channel 发送给客户端。客户端接收到响应后,解析响应结果并返回给用户。

源代码解析

RpcProvider类

RpcProvider类,用于发布RPC服务。它可以通过NotifyService()函数注册RPC服务,然后在Run()函数中启动RPC服务节点,开始提供RPC远程网络调用服务。

RpcProvider类中的NotifyService()函数用于注册RPC服务,它接收一个google::protobuf::Service*类型的参数,该参数代表一个RPC服务对象。函数首先从服务对象中获取服务名称和服务方法数量,然后依次遍历服务方法并将方法信息保存到ServiceInfo结构体中。最后,将服务对象和方法信息保存到m_serviceMap中。

RpcProvider类中的Run()函数用于启动RPC服务节点,它首先从配置文件中读取RPC服务器的IP地址和端口号,然后使用muduo库创建一个TCP服务器对象。将连接回调函数和消息读写回调函数绑定到OnConnection()和OnMessage()函数上。之后,调用server.setThreadNum(4)设置线程池中线程数量为4,并使用server.start()启动RPC服务。

最后,RPC服务启动后,RpcProvider类会将要发布的服务全部注册到Zookeeper上,让RPC客户端可以从Zookeeper上发现服务。在注册服务时,会先创建一个服务节点,再在该节点下为每个方法创建一个临时性节点,以存储当前这个RPC服务节点主机的IP和端口号。然后客户端可以在Zookeeper上通过服务名和方法名来查找对应的服务。

RpcChannel类

RpcChannel类是一个google::protobuf::RpcChannel实现,负责对RPC请求进行序列化和发送。CallMethod方法用于每个RPC方法调用,它接受方法描述符、控制器、请求消息、响应消息和闭包作为参数。

CallMethod方法首先从参数中检索服务名称、方法名称和序列化请求消息。然后,使用这些信息构造RPC头并进行序列化。头的大小也计算出来,并插入到RPC请求字符串的开头。最后,使用TCP套接字将RPC请求发送到网络上,并接收响应并以字符串形式返回。

RPC客户端查询ZooKeeper服务器以获取要调用的RPC服务的主机信息。这是通过从服务名称和方法名称构造ZooKeeper路径并调用GetData来完成的。返回的值应该以主机:端口的格式呈现,然后用于与RPC服务器建立TCP连接。

RpcController类

用于控制 RPC(远程过程调用)方法的执行。

类中包含了以下方法:

RpcController():构造函数,初始化 RPC 方法执行状态和错误信息。
Reset():重置 RPC 方法执行状态和错误信息。
Failed():获取 RPC 方法执行状态,返回 true 表示执行失败,返回 false 表示执行成功。
ErrorText():获取 RPC 方法执行过程中的错误信息。
SetFailed():设置 RPC 方法执行状态为失败,并传入错误信息。
其中,RPC 方法执行状态和错误信息在类中分别用 bool 类型和 std::string 类型的变量进行存储和管理。在方法的实现中,通过操作这些变量来达到控制 RPC 方法执行的目的。

总体来说,这个类是实现 RPC 机制的重要组成部分,可以帮助控制 RPC 方法的执行状态,并获取执行过程中的错误信息,方便调试和异常处理。

RpcConfig类

读取配置文件并将配置项作为键值对存储在无序映射中。

该类有三个公共成员函数:

LoadConfigFile:以配置文件路径为输入,逐行读取文件。它去除前导和尾随空格,忽略注释(以“#”开头)和空行,并提取键值对。它将键值对存储在无序映射中。
Load:以键作为输入,并从无序映射中返回相应键值对的值。如果键不存在,则返回空字符串。
Trim:以字符串为输入,并去除字符串的前导和尾随空格。
私有成员变量是:

m_configMap:存储键值对的无序映射。
RpcConfig类可在更大的框架中使用,从配置文件加载和访问配置项。

RPCApplication类

提供了一个rpc框架接口,可以通过加载配置文件来初始化应用程序。RpcApplication 类具有以下几个成员函数:

Init:初始化 RpcApplication,接受命令行参数 argc 和 argv 作为输入。从命令行参数中解析出配置文件的路径,然后调用 RpcConfig 类的 LoadConfigFile 函数加载配置文件,并打印出配置文件中的一些配置信息。
GetInstance:返回一个 RpcApplication 类的单例对象。
GetConfig:返回一个 RpcConfig 对象,它保存了应用程序的配置信息。
RpcApplication 类的私有成员变量如下:

m_config:一个 RpcConfig 对象,保存了应用程序的配置信息。
另外,还有一个名为 ShowArgsHelp 的函数,它用于打印命令行参数的使用方法。

ZkClient类

实现了一个ZooKeeper客户端。ZooKeeper是一个分布式协调服务,允许分布式应用程序进行同步和协调。该代码提供了一个ZkClient类,具有连接到ZooKeeper服务器、创建znode节点和获取znode值的方法。

ZkClient类有一个构造函数和析构函数,分别初始化和释放客户端的资源。Start方法连接客户端到ZooKeeper服务器,Create方法在服务器上创建一个znode节点。GetData方法检索指定znode节点的值。

global_watcher函数是一个回调函数,处理来自ZooKeeper服务器的通知。它用于在客户端成功连接到服务器时通知客户端。当客户端初始化时,观察器被注册,客户端在从Start方法返回之前等待来自服务器的通知。

protobuf的使用介绍

Protocol Buffers(简称protobuf)是一种语言无关的数据序列化格式,由Google开发,可用于将结构化数据序列化为二进制格式进行存储或传输。它支持多种编程语言,包括C++、Java、Python等。protobuf比XML和JSON等其他序列化格式更紧凑、更快、更易于使用。

使用protobuf,需要先定义数据结构的消息类型(message),并将其保存在.proto文件中。这个文件可以通过protobuf编译器生成相应的代码文件,用于序列化和反序列化消息。生成的代码文件提供了一种面向对象的API,可用于创建、修改和访问消息对象。代码文件还提供了其他实用功能,例如消息校验和扩展。

protobuf还支持向消息类型添加扩展(extension),这允许用户向现有消息类型添加自定义字段和操作。扩展可以在消息定义之外定义,并且可以动态加载和解析,这使得protobuf非常适合用于通信协议和数据存储方案。

总之,protobuf是一种高效、灵活的数据序列化格式,适用于各种应用场景。它通过提供简单的消息定义和强大的代码生成功能,降低了数据序列化和反序列化的复杂性,使得开发人员可以更专注于应用程序的业务逻辑。

以下是一个使用 Protocol Buffers 进行数据序列化和反序列化的简单示例:

假设我们想要定义一个包含个人信息的数据结构,并使用 Protocol Buffers 进行序列化和反序列化。首先,我们需要在一个 .proto 文件中定义消息类型:

syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}

这个文件定义了一个名为 Person 的消息类型,包含三个字段:name、age 和 email。每个字段都有一个唯一的编号,称为字段标识符,用于在序列化和反序列化过程中标识字段。

接下来,我们需要使用 Protocol Buffers 编译器生成相应的代码。假设我们使用 C++ 作为目标语言,我们可以使用以下命令生成对应的代码文件:

protoc --cpp_out=. person.proto

这将生成一个名为 person.pb.h 的 C++ 头文件和一个名为 person.pb.cc 的源文件。

现在我们可以在我们的 C++ 代码中使用生成的代码,创建和序列化 Person 对象:

#include "person.pb.h"

int main() {
  Person person;
  person.set_name("Alice");
  person.set_age(30);
  person.set_email("alice@example.com");

  // 将 Person 对象序列化为字节流
  std::string serialized_person = person.SerializeAsString();

  // ...
}

在这个示例中,我们使用 set_name、set_age 和 set_email 方法设置 Person 对象的字段值,并使用 SerializeAsString 方法将其序列化为一个字符串。

我们还可以使用反序列化方法 ParseFromString 从字节流中反序列化出一个 Person 对象:

#include "person.pb.h"

int main() {
  // ...

  // 将字节流反序列化为 Person 对象
  Person deserialized_person;
  deserialized_person.ParseFromString(serialized_person);

  // ...
}

在这个示例中,我们使用 ParseFromString 方法将序列化后的字符串转换回 Person 对象。现在我们可以使用 deserialized_person 对象来访问反序列化后的数据。

这只是一个简单的示例,Protocol Buffers 还有很多其他功能和用法。

使用protobuf注册远程调用方法

要注册远程调用方法,用户需要使用 protobuf 的服务定义语言来定义用户的服务和方法。这些定义将生成用户的代码中的接口和 stubs,使得用户可以轻松地在客户端和服务器之间进行远程调用。

以下是一个使用 protobuf 注册远程调用方法的简单示例:

首先,用户需要创建一个服务定义文件,例如 example_service.proto:

syntax = "proto3";

package example;

service ExampleService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

然后,使用 protoc 编译器来生成用户的代码。为了生成远程调用方法所需的代码,用户需要添加 --grpc_out 和 --plugin 标志,如下所示:

protoc example_service.proto --grpc_out=. --plugin=protoc-gen-grpc=/usr/local/bin/grpc_cpp_plugin

这将生成一个名为 example.grpc.pb.h 的头文件和一个名为 example.grpc.pb.cc 的源文件。

接下来,用户需要实现用户的服务。这包括实现用户在服务定义文件中定义的所有方法。对于我们的示例服务,我们可以创建一个名为 ExampleServiceImpl 的类:

#include <iostream>
#include <memory>
#include <string>

#include <grpcpp/grpcpp.h>

#include "example.grpc.pb.h"

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using example::ExampleService;
using example::HelloRequest;
using example::HelloResponse;

class ExampleServiceImpl final : public ExampleService::Service {
  Status SayHello(ServerContext* context, const HelloRequest* request,
                  HelloResponse* reply) override {
    std::string prefix("Hello, ");
    reply->set_message(prefix + request->name());
    return Status::OK;
  }
};

void RunServer() {
  std::string server_address("0.0.0.0:50051");
  ExampleServiceImpl service;

  grpc::ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);

  std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();
}

int main(int argc, char** argv) {
  RunServer();
  return 0;
}

在 ExampleServiceImpl 类中,我们实现了 SayHello 方法,它将收到的 HelloRequest 中的名称作为参数,并将其包含在 HelloResponse 的消息中返回。

最后,用户需要在客户端代码中调用用户的远程调用方法。对于我们的示例服务,我们可以创建一个名为 ExampleClient 的类:

#include <iostream>
#include <memory>
#include <string>

#include <grpcpp/grpcpp.h>

#include "example.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using example::ExampleService;
using example::HelloRequest;
using example::HelloResponse;

class ExampleClient {
 public:
  ExampleClient(std::shared_ptr<Channel> channel)
      : stub_(ExampleService::NewStub(channel)) {}

  std::string SayHello(const std

项目中的.pb.h文件

rpcheader.pb.h

由Protocol Buffer编译器生成的C++头文件rpcheader.pb.h。该头文件包含了RpcHeader类和相关辅助函数的定义。

rpcheader.proto文件是协议缓冲区编译器的输入文件,包含了RpcHeader消息类型的定义。rpcheader.pb.h中生成的C++代码提供了用于处理RpcHeader类型消息的C++ API。

RpcHeader消息类型包含有关RPC(远程过程调用)请求或响应的信息,例如被调用的方法名称、使用的协议版本以及与请求或响应相关的任何元数据。

rpcheader.pb.h中的代码将RpcHeader类定义为Message类的子类,Message类在Protocol Buffers库中定义。Message类提供了用于处理协议缓冲区消息的基本功能,例如序列化和反序列化、复制、合并和检查消息初始化等。RpcHeader类添加了用于处理RpcHeader类型消息所需的特定字段和方法。

头文件还包括来自Protocol Buffers库的几个其他类和定义,例如Descriptor、Reflection、Arena、ArenaString、RepeatedField、ExtensionSet和UnknownFieldSet。这些类提供了额外的功能,用于处理协议缓冲区消息,例如元数据、反射、内存管理和处理未知字段。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值