前言
在讨论gRPC之前我们得先知道RPC是什么。RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,它允许一个程序(客户端)通过网络向另一个程序(服务器)请求服务,而无需了解底层网络技术的细节。RPC 机制隐藏了网络通信的复杂性,使得开发者可以像调用本地函数一样调用远程函数。
gRPC是一个现代的、高性能、开源的和语言无关的通用 RPC 框架,基于 HTTP2 协议设计,序列化使用 Protocol Buffer,一种语言无关的高性能序列化框架,基于 HTTP2+Protocol Buffer保证了的高性能。
gRPC基于服务的思想。定义一个服务,描述这个服务的方法以及入参出参,服务器端有这个服务的具体实现,客户端保有一个存根(Stub),提供与服务端相同的服务。gRPC同时支持同步调用和异步调用,同步RPC调用时会一直阻塞直到服务端处理完成返回结果, 异步RPC是客户端调用服务端时不等待服务段处理完成返回,而是服务端处理完成后主动回调客户端告诉客户端处理完成。
数据封装与传输
什么是Protocol Buffer?那就要回到程序的初衷了,一个程序被设计出来,它的一生的使命就是做数据转换,而对于远程调用函数,我们得给函数提供数据,这个数据怎么封装呢?靠Protocol Buffer:Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。这种存储文件以.proto为后缀,如果我们想要获取它的信息,就要使用protoc命令将其转换为相应语言文件。
protoc --cpp_out=. your_file.proto#c++
protoc --java_out=. your_file.proto#java
Protocol Buffers 提供了跨平台和跨语言的数据交换格式,非常适合用于数据存储、通信协议等方面。生成的代码提供了一种类型安全的方式来处理定义的数据结构,并且可以自动处理诸如序列化、反序列化、合并、比较等操作。
此外grpc采用HTTP2.0,每一次请求对应一个流,有一个唯一ID,用来区分不同的 请求。基于流的概念,进一步提出了 帧 ,一个请求的数据会被分成多个帧,方便进行数据分割传输,每个帧都唯一属于某一个流ID,将帧按照流ID进行分组,即可分离出不同的请求。这样同一个TCP连接中就可以同时并发多个请求,不同请求的帧数据可穿插在一起,根据流ID分组即可。HTTP2.0基于这种二进制协议的乱序模式 (Duplexing),直接解决了HTTP1.1的核心痛点,通过这种复用TCP连接的方式,不用再同时建多个连接,提升了TCP的利用效率。
gRPC4种模式
一元模式
这是最基本和最常见的 RPC 模式,客户端向服务器发送一个请求并等待响应。这种模式类似于传统的 HTTP 请求-响应模型,适用于大多数请求-响应场景,如获取用户信息、查询订单状态等。
服务端流模式
在这种模式下,客户端发送一个请求给服务器后,服务器可以返回一个数据流,客户端可以逐个读取数据直到流结束。这种模式适用于服务器需要推送大量数据给客户端的场景,例如实时股票行情更新、大文件下载等。
客户端流模式
客户端可以连续发送多个请求给服务器,服务器在接收完所有请求后再返回一个响应。这种模式适用于需要客户端上传大量数据后再由服务器统一处理的场景,比如批量文件上传、实时数据分析等。
双向流模式
在这种模式下,客户端和服务器可以通过一个持续的连接同时发送和接收多个消息。这种模式适用于双方需要进行实时通信的场景,例如聊天应用、实时游戏等。
gRPC使用方法
定义服务: 使用 Protocol Buffers (protobuf) 语法在 .proto
文件中定义你的服务接口和消息类型。这包括指定 RPC 方法、请求和响应类型。
syntax = "proto3";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
生成 C++ 代码: 使用 protobuf 编译器 protoc
和 gRPC 插件为 .proto
文件中定义的服务生成 C++ 代码。这通常会产生 .pb.h
, .pb.cc
, .grpc.pb.h
, .grpc.pb.cc
四个文件,包含序列化和 RPC 框架代码。
protoc --cpp_out=. helloworld.proto
protoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` helloworld.proto
实现服务逻辑: 在服务端,你需要实现生成的接口类中定义的虚拟函数,以处理客户端的 RPC 请求。构建和启动 gRPC 服务器: 创建 ServerBuilder
对象,配置监听端口,注册服务,然后构建并启动服务器。
#include <grpcpp/grpcpp.h>
#include "helloworld.grpc.pb.h"
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using helloworld::Greeter;
using helloworld::HelloRequest;
using helloworld::HelloReply;
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;
}
};
void RunServer() {
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();
}
int main(int argc, char** argv) {
RunServer();
return 0;
}
创建客户端存根: 在客户端,使用生成的存根类创建一个客户端对象,该对象封装了与服务器通信的细节。调用 RPC 方法: 客户端使用存根对象调用服务方法,可以是简单 RPC、服务器端流式 RPC、客户端流式 RPC 或双向流式 RPC。
#include <grpcpp/grpcpp.h>
#include "helloworld.grpc.pb.h"
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using helloworld::Greeter;
using helloworld::HelloRequest;
using helloworld::HelloReply;
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
std::string SayHello(const std::string& user) {
HelloRequest request;
request.set_name(user);
HelloReply reply;
ClientContext context;
Status status = stub_->SayHello(&context, request, &reply);
if (status.ok()) {
return reply.message();
} else {
std::cerr << status.error_code() << ": " << status.error_message()
<< std::endl;
return "RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
int main(int argc, char** argv) {
GreeterClient greeter(grpc::CreateChannel(
"localhost:50051", grpc::InsecureChannelCredentials()));
std::string user("world");
std::string reply = greeter.SayHello(user);
std::cout << "Greeter received: " << reply << std::endl;
return 0;
}
处理响应: 服务器处理请求并发送响应,客户端接收并处理响应。
关闭连接: 通信完成后,客户端和服务器可以关闭连接。
编译和链接: 使用适当的编译器和链接器选项编译服务器和客户端代码,确保链接了 gRPC 和 Protocol Buffers 库。
gRPC使用场景
gRPC 适用于多种使用场景,尤其是在需要跨语言服务间通信时。
- 低延迟,高度可扩展的分布式系统
- 开发与云服务器通信的客户端
- 设计一个准确,高效,且与语言无关的新协议时
- 分层设计,以实现扩展,例如。身份验证,负载平衡,日志记录和监控等