RPC调用
RPC(Remote Procedure Call),即远程过程调用,它允许像调用本地服务一样调用远程服务。是一种服务器-客户端(Client/Server)模式。
那“远程过程调用”,就是:可以跨过一段网络,调用另外一个网络节点上的方法。以上就是对远程过程调用的简单理解。
远程过程调用(Remote Procedure Call,RPC)是一种允许计算机程序像调用本地函数一样调用远程服务器上的函数的机制。RPC 隐藏了底层的网络通信细节,使开发者能够专注于业务逻辑,而不必关心数据传输的具体实现。以下是 RPC 调用的详细讲解:
1. RPC 的基本概念
- 客户端和服务器:在 RPC 模型中,有客户端和服务器两个角色。客户端发起 RPC 调用,服务器执行实际的处理并返回结果。
- Stub:Stub 是客户端和服务器的代理,它隐藏了网络通信的复杂性。客户端 Stub 提供了与服务器相同的方法接口,当客户端调用这些方法时,Stub 负责将调用转换为网络请求发送到服务器。服务器 Stub 接收请求并调用实际的服务方法。
- 序列化和反序列化:在客户端和服务器之间传输的数据需要序列化成二进制格式以通过网络传输,接收方再进行反序列化恢复为原始数据结构。
2. RPC 调用的工作流程
-
方法调用:
- 客户端程序调用 Stub 中的一个方法,例如
stub_->AppendEntries(&controller, args, response, nullptr)
。Stub 实现与服务器的通信细节,并将调用转发给服务器。
- 客户端程序调用 Stub 中的一个方法,例如
-
序列化:
- Stub 将方法调用的参数序列化为二进制数据。序列化的过程包括将复杂的对象结构转换为可以通过网络传输的字节流。
-
请求发送:
- 序列化后的数据通过网络协议(如 HTTP/2)发送到远程服务器。这个过程通常包括建立连接、发送请求、等待响应等。
-
服务器处理:
- 服务器接收到请求后,通过服务器 Stub 进行反序列化,恢复为原始的参数数据结构。
- 服务器 Stub 调用实际的服务实现(即远程方法),并将结果返回给服务器 Stub。
-
反序列化:
- 服务器 Stub 将结果序列化为二进制数据并通过网络传回给客户端。
- 客户端 Stub 接收到返回数据后进行反序列化,恢复为客户端可以处理的对象或原始数据结构。
-
结果返回:
- 客户端 Stub 将反序列化后的结果返回给调用方法的客户端程序,从客户端的角度来看,这与调用本地方法几乎没有区别。
3. RPC 框架
- 常用的 RPC 框架包括 gRPC、Thrift、Dubbo 等。这些框架通常提供了编写接口定义文件的工具(如 gRPC 的 .proto 文件),根据接口定义文件生成客户端和服务器代码,自动处理序列化、反序列化以及网络通信细节。
4. 关键技术
- 序列化技术:常用的序列化协议包括 Protocol Buffers(gRPC 使用)、Thrift、JSON、XML 等。
- 传输协议:RPC 通常使用 TCP/IP 作为底层传输协议。现代 RPC 框架如 gRPC 使用 HTTP/2,提供更高效的双向通信和流控。
- 安全性:为了保护数据安全,RPC 通信可以使用 SSL/TLS 加密来保护传输中的数据。
5. RPC 的优缺点
- 优点:
- 简化远程调用:RPC 隐藏了底层的网络通信,提供了像调用本地方法一样简单的接口。
- 易于扩展:通过定义接口文件,客户端和服务器可以独立开发和部署。
- 缺点:
- 隐藏了网络的不可靠性:尽管 RPC 封装了网络通信,但它不能避免网络故障、延迟等问题。开发者需要考虑重试机制、超时处理等。
- 性能开销:序列化、反序列化以及网络传输都会带来性能开销。
总结
RPC 提供了一种简洁的方式来实现分布式系统中的服务调用,允许程序像调用本地函数一样调用远程服务。通过使用序列化、网络通信、Stub 等技术,RPC 极大地简化了分布式系统的开发。不过,使用 RPC 时仍然需要考虑网络延迟、错误处理、性能优化等实际问题。
gRPC调用
gRPC是由Google开发的一个高性能、开源的远程过程调用(RPC)框架。它基于HTTP/2协议,并使用Protocol Buffers(Protobuf)作为接口描述语言和序列化格式。gRPC支持多种编程语言,适合构建高效的分布式系统,特别是在微服务架构中得到了广泛的应用。下面是对gRPC的详细讲解:
1. gRPC的基本概念
- 远程过程调用(RPC):gRPC的核心是RPC机制,使客户端可以像调用本地函数一样调用位于远程服务器上的函数。这种调用可以跨越不同的语言和平台,简化了分布式系统的开发。
- HTTP/2:gRPC基于HTTP/2协议,支持双向流、头部压缩、多路复用连接等特性,提供了比HTTP/1.x更好的性能和效率。
- Protocol Buffers(Protobuf):gRPC使用Protobuf作为默认的接口描述语言(IDL)和序列化格式。Protobuf是一种高效的二进制序列化格式,能够在不同的编程语言之间进行数据的高效传输。
2. gRPC的工作流程
-
定义服务:
- 使用Protobuf定义gRPC服务。Protobuf文件描述了服务的接口和消息格式。
- 例如,在Protobuf文件中定义一个简单的服务:
syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
- 这里定义了一个
Greeter
服务,它包含一个SayHello
方法,接收HelloRequest
请求并返回HelloReply
响应。
-
生成代码:
- 使用
protoc
编译器将Protobuf文件编译成不同语言的客户端和服务器端代码。这些生成的代码包含了消息类型以及gRPC调用的客户端和服务器端存根。
- 使用
-
实现服务:
- 在服务器端实现定义的gRPC服务接口。例如,使用C++实现
SayHello
方法:class GreeterServiceImpl final : public Greeter::Service { Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override { std::string prefix("Hello "); reply->set_message(prefix + request->name()); return Status::OK; } };
- 在服务器端实现定义的gRPC服务接口。例如,使用C++实现
-
启动服务器:
- 创建并启动gRPC服务器,注册服务并监听特定端口:
std::string server_address("0.0.0.0:50051"); GreeterServiceImpl service; ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr<Server> server(builder.BuildAndStart()); server->Wait();
- 创建并启动gRPC服务器,注册服务并监听特定端口:
-
调用服务:
- 在客户端调用远程服务。例如,使用C++实现客户端调用
SayHello
方法:std::unique_ptr<Greeter::Stub> stub = Greeter::NewStub(grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials())); HelloRequest request; request.set_name("world"); HelloReply reply; ClientContext context; Status status = stub->SayHello(&context, request, &reply); if (status.ok()) { std::cout << "Greeter received: " << reply.message() << std::endl; }
- 在客户端调用远程服务。例如,使用C++实现客户端调用
3. gRPC通信模式
gRPC支持以下几种通信模式:
- 简单RPC(Unary RPC):客户端发送一个请求,服务器返回一个响应。这是最常见的RPC模式。
- 服务端流式RPC(Server Streaming RPC):客户端发送一个请求,服务器返回一个数据流。客户端从流中读取消息,直到所有消息都被读取。
- 客户端流式RPC(Client Streaming RPC):客户端发送一个数据流到服务器,服务器处理完流后返回一个响应。
- 双向流式RPC(Bidirectional Streaming RPC):客户端和服务器都可以发送消息流,两个流是独立的,客户端和服务器可以以任何顺序读写消息。
4. gRPC的主要特性
- 多语言支持:gRPC支持多种编程语言,如C++、Java、Python、Go、C#、Node.js等,适用于不同的技术栈。
- 双向流通信:基于HTTP/2,gRPC支持双向流式通信,使得实时数据传输更为便捷。
- 负载均衡和命名解析:gRPC内置了负载均衡和命名解析机制,适用于大规模分布式系统。
- 拦截器(Interceptor):gRPC支持拦截器机制,允许在RPC调用前后进行一些通用处理,如日志记录、认证、监控等。
- 认证与安全:gRPC支持多种安全机制,包括TLS/SSL加密、Token认证等。
5. gRPC的应用场景
- 微服务架构:gRPC非常适合微服务之间的通信,因其高效的二进制协议和强类型的接口定义。
- 实时数据传输:gRPC的流式RPC模式非常适合实时数据传输场景,如视频流、音频流、股票行情等。
- 跨语言通信:gRPC的多语言支持使得不同语言的服务之间可以无缝通信,降低了跨语言开发的复杂度。
- 高性能要求的系统:由于gRPC基于HTTP/2协议,并使用高效的二进制序列化格式Protobuf,在高性能和低延迟需求的系统中表现优异。
6. gRPC的优缺点
优点:
- 高效性:使用Protobuf作为序列化格式,比JSON或XML更紧凑。
- 强类型接口:通过IDL定义接口,编译器生成代码,减少了接口实现不一致的问题。
- 多语言支持:方便不同语言之间的互操作。
- 双向流支持:通过HTTP/2的特性,实现复杂的双向通信模式。
缺点:
- 学习曲线:开发者需要学习和掌握Protobuf和gRPC的概念和工具链。
- 调试复杂度:由于是二进制协议,调试时不如HTTP+JSON直观。
- 浏览器支持:gRPC基于HTTP/2,因此在浏览器中直接使用gRPC比较困难,通常需要gRPC-Web来支持。
gRPC是一种功能强大且高效的RPC框架,尤其适用于需要高性能和跨语言支持的分布式系统。通过gRPC,开发者可以轻松实现不同服务之间的远程调用,同时享受强类型接口、流式通信和内置的负载均衡等特性。
上述概念讲解完成后,需要结合本题的案例来进行详细分析gRPC是如果处理自定义远程过程调用的
使用 gRPC 的时候进行自定义处理
- 需要使用protobuf来定义生成我们的消息头消息,该消息是用于在数据在网络中传递时将我们需要调用的方法和参数序列化到该消息中,然后传递给服务端;
- 在使用RPC调用时,我们需要首先继承RPCChannel并重写里面的callmethod方法;
myRpcChannel类的定义及作用
我们定义一个myRpcChannel类这个类继承了RPCChannel类,这个类的作用作用就是实现底层通信的,我们将底层通信封装在这个类里面;
myRpcChannel类的作用有以下几点:
- 继承RPCChannel并实现底层网络通信(这里使用的是TCP连接),可以延迟连接和连接失败重连;
- 声明bool newConnect函数来完成底层网络通信
- 重写callmethod来实现远程过程调用,这里面的具体实现是:
- 首先获取到RPC服务的服务描述符,从服务描述符中获取到服务名和方法名;
- 将request对象进行序列化并获得其长度,这个request包含了远程调用方法的参数;
- 定义一个rpcHeader protc对象,将服务名和方法名和参数大小填充到该对象中,并将该对象进行序列化;
- 上述完成之后,我们的服务名和方法名和参数长度已经都存在于rpcHeader中了;
- 我们在一个发送的数据里面需要标注rpcHeader的大小,因此使用StringOutputStream和CodedOutputStream来填充rpcHeader的大小和rpcHeader本身
- 最后再将request的序列化的数据填充到发送数据中,并发送;
- 接收response响应数据,并将二进制数据反序列化成respnose对象;
上述操作完成后,我们就基本完成了对RPCChannel类的重写和实现,现在就可以使用myRpcChannel来进行远程过程调用;
myRpcController类的定义和作用
我们定义这个类使用来实现从配置文件中读取连接信息的,因此我们需要定义三个函数
- 加载配置文件的方法,会将配置文件中的信息经过处理后加入到unordered_map集合中去(需要进行一定的处理):
- 按行来读取文件,如果读取为空和读取的字符串开头是#则跳过,继续读取下一行;
- 读取到有效数据,我们需要去除掉首位的空格,然后根据"="来获取键值;
- 获取到键值后,我们需要去除掉键值的空格后,然后再放入到map中;
- 根据键去map中查找值;
- 去除空格的函数Trim;
myRpcController类的定义和作用
myRpcController类是继承至RPC框架中的RPCController的,RPCController
的介绍如下:
RPCController
是在 Google 的 Protocol Buffers (protobuf) RPC 框架中定义的一个抽象类,用于控制 RPC 调用的状态和行为。它的作用主要包括以下几个方面:
-
错误管理:
RPCController
允许在 RPC 调用过程中设置和查询错误状态。如果在 RPC 调用中出现错误(例如,网络问题、请求超时等),可以通过RPCController
来设置错误消息,并在客户端查询错误状态。
-
取消操作:
- 在某些情况下,你可能需要取消一个正在进行的 RPC 调用。
RPCController
提供了取消操作的机制,允许客户端在必要时终止一个 RPC 请求。
- 在某些情况下,你可能需要取消一个正在进行的 RPC 调用。
-
流量控制:
RPCController
还可以用来控制和监控 RPC 调用的流量,例如设置和查询超时参数。通过这种方式,可以对每个 RPC 调用进行更细粒度的控制。
-
进度跟踪:
- 在某些实现中,
RPCController
可以用于跟踪 RPC 调用的进度,方便应用程序了解请求的处理状态。
- 在某些实现中,
在使用 RPCController
时,通常会在发起 RPC 调用时创建一个 RPCController
实例,并将其传递给 RPC 调用方法。不同的 RPC 框架可能会提供具体的 RPCController
实现,允许开发者在实现过程中定制这些控制功能。
我们在使用这个RPCController,我们的自定义类根据需求可能需求来重写以下方法:
Reset()
:重置控制器状态,使其可以重用;Failed()
:检查RPC调用是否失败;ErrorText()
:获取错误消息文本;StartCancel
:请求消息正在进行的RPC调用;SetFailed(const std::string &reason)
:设置调用失败的原因IsCanceled()
:检查RPC调用是否已经取消;NotifyOnCancel(Closure *callback)
: 注册一个回调函数,当 RPC 调用被取消时执行该回调。
函数原型如下
class RPCController {
public:
// 析构函数,通常被子类覆盖
virtual ~RPCController() {}
// 重置控制器的状态,使其可以重用。通常会清除之前的错误状态。
virtual void Reset() = 0;
// 返回调用是否失败。如果失败,可以通过 ErrorText() 获取失败原因。
virtual bool Failed() const = 0;
// 如果调用失败,返回一个描述错误的字符串。调用成功则返回空字符串。
virtual std::string ErrorText() const = 0;
// 手动设置调用失败,并提供失败的原因。
virtual void SetFailed(const std::string& reason) = 0;
// 如果服务器端发现调用已经被取消,它应该尝试尽快终止执行,并且可以随时丢弃 RPC。
virtual bool IsCanceled() const = 0;
// 请求取消正在进行的 RPC 调用。通常由客户端调用。
virtual void StartCancel() = 0;
// 注册一个回调函数,在调用被取消时执行。如果调用已经被取消,则此回调会立即被调用。
virtual void NotifyOnCancel(google::protobuf::Closure* callback) = 0;
};
RPCProvider类的定义和作用
RpcProvider类是用于向网络中的实体发布RPC调用的类,是基于Muduo 网络库的 C++ 程序中发布和管理 RPC(远程过程调用)服务。该类结合了 Google Protocol Buffers 来处理消息的序列化和反序列化,同时利用 Muduo 网络库来管理网络连接和事件循环。
该类的主要功能有以下几个:
- 网络通信服务:网络通信服务使用muduo网络库来实现
- RPC服务注册
- 服务节点启动
- RPC服务调用
- RPC服务响应
函数实现细节讲解
服务注册函数
NotifyService(google::protobuf::Service *service)
:
- 根据service来获取服务名:
- 获取服务描述符google::protobuf::ServiceDescriptor,使用service->GetDescriptor();
- 使用服务描述符来获取服务名称:name()方法
- 根据服务描述符来获取服务中方法的个数: method_count()方法;
- 通过for循环,将方法的方法描述符和方法名放入到结构体struct ServiceInfo的map集合中去保存起来;
- 最后将service_name放入到结构体struct ServiceInfo中;
struct ServiceInfo {
google::protobuf::Service *m_service; // 保存服务对象
std::unordered_map<std::string, const google::protobuf::MethodDescriptor *> m_methodMap; // 保存服务方法
};
服务节点启动函数
Run(int nodeIndex, short port):
实现细节:
- 使用
ifaddr
结构体和getifaddrs
函数,从本地网络接口中获取可用的 IP 地址。 - 将获取的 IP 地址与
nodeIndex
和port
一起写入到文件中。 - 创建
muduo
服务器,并绑定连接回调函数和接收消息后执行的回调函数。 - 设置服务器的线程数,以处理并发连接。
- 启动网络服务,并启动事件循环以处理网络事件。
OnConnection(const muduo::net::TcpConnectionPtr &conn)回调函数:
如果是新连接就什么都不做,如果是连接断开,则断开;
RPC服务调用
OnMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp)
- 从网络中接收到数据后,使用
retrieveAllAsString
从缓冲区中获取数据; - 然后使用protobuf字符输入流从数据中获取到rpcHeader的长度:
- 使用protobuf的低级流来处理字符串
google::protobuf::io::ArrayInputStream
; - 通过低级流来初始化高级流来从其中获取到变长编码的头部长度:
google::protobuf::io::CodedInputStream
- 使用高级流的
ReadVarint32
来获取数据中的消息头的长度,因为编码时我们也是这么处理的,通过字符流将长度写入到数据的前面;
- 使用protobuf的低级流来处理字符串
- 根据长度来完整的读取rpcHeader;我们使用限制的方式,来限制读取的数据长度,通过使用
pushLimit
方法来实现,读取完成后需要还原PopLimit
- 将rpcHeader序列化成rpcHeader对象,并获取里面的服务名、方法名、参数长度;
- 根据参数长度来读取参数,然后根据服务名和方法名,来获取服务的描述符和方法的描述符;
- 根据服务描述符和方法的描述符来生成messsage对象
service->GetResponsePrototype(method).New()
这种方法通常动态获取与某个RPC方法对应的请求消息类型; - 将参数字符串序列化成该对象,然后生成response Message对象;
- 设置RPC远程调用执行完毕之后的回调函数,该函数会在执行完远程过程调用后执行,使用
google::protobuf::NewCallback
方法; - 执行远程过程调用
service->CallMethod(method, nullptr, request, response, done)
。必须详细说一下为什么执行这个就是执行客户端请求执行的方法:这里面调用的是我们在protoc里面使用service关键字写的服务,使用protoc生成的.pb.h和.pb.cc文件中的服务类的CallMethod方法;这个方法会根据method的方法描述符去找相应的方法,去调用,但是讷这个相应的方法在这个文件中是虚函数所以我们需要定义我们自己的类去继承生产的这个基类,去重写这些需要远程调用的方法,这样才能完成远程过程调用注意不要和myRPCChannel类的CallMethod混淆,它的作用是客户端stub发送远程调用请求的底层实现;
RPC服务响应
SendRpcResponse( const muduo::net::TcpConnectionPtr &conn, google::protobuf::Message *response )
这里的实现简单:就是将response序列化成字符串发送出去就行;
标签:调用,服务,实现,gRPC,RPC,序列化,客户端 From: https://www.cnblogs.com/wuhaiqiong/p/18577020