文章目录
- gRPC知识归档
- gRPC原理
- 什么是gRPC
- gRPC的特性
- gRPC支持语言
- gRPC使用场景
- gRPC设计的动机和原则
- 数据封装和数据传输问题
- 网络传输中的内容封装和数据体积问题
- JSON
- Protobuf(微服务之间的服务器调用,一般采用二进制序列化,比如protobuf)
- 网络传输效率问题
- gRPC的4种模式
- 一元RPC模式
- 服务端流RPC模式
- 客户端流RPC
- 双向流RPC模式
- gRPC同步异步
- gRPC调用关系图
- proto文件
- Client
- server
- 异步相关概念
- 异步Client
- 异步Server
- 关系图
gRPC知识归档
gRPC原理
什么是gRPC
RPC是远程调用协议(Remote Procedure Call Protocol),可以让我们像调用本地对象一样发起远程调用。RPC凭借强大的治理功能,成为解决分布式系统问题的一大利器。
gRPC是一个现代的,高性能的,开源的和语言无关的通用RPC框架,HTTP2协议设计,序列化使用PB(Protocol Buffer)高性能序列化框架,基于HTTP2+PB保证高性能。
gRPC的Server和Client工作流程大致如下:
- tars兼容grpc
- brpc也兼容grpc
- grpc不兼容tars和brpc
gRPC的特性
- gRPC基于服务的思想:定义一个服务,描述这个服务的方法和出入参数,服务端有这个服务的具体实现,客户端保有一个存根,提供与服务端相同的服务。
- gRPC默认采用protobuf作为IDL(Interface Description)接口描述语言,服务之间通信数据序列化和反序列化也是基于protocol buffer的,因为protocol buffer的特殊性,所以gRPC框架是跨语言的通信框架,可就是说用jave开发的gRPC服务,可以用GoLang,C++等语言调用。
- gRPC同时支持同步和异步调用,同步RPC调用会一直阻塞到服务端处完成返回数据,异步RPC是客户端调用服务端时不等待服务端处理完成返回结果,而是服务端处理完成后主动回调客户端高速客户端处理完成。
- gRPC是基于HTTP2.0协议实现的,http2提供了很多新特性,并且在性能上相比http1提高了许多,所以gRPC的性能是非常好的。
- gRPC并没有直接实现负载均衡和服务发现的功能,但是已经提供了自己的设计思路。已经为命名解析和负载均衡提供了接口。
- 基于http2的协议特性,gRPC允许自定义4类服务方法
- 一元RPC
- 服务端流式RPC
- 客户端流式RPC
- 双向流式RPC
gRPC支持语言
C++,Java(包括安卓),OC,Python,Ruby,Go,C#,Node.js。
gRPC使用场景
- 低延迟,高可扩展性的分布式系统
- 开发与云服务器的客户端
- 设计一个准确,高效,与语言无关的新协议
- 分层设计,实现扩展,例如:身份验证,负载平衡,日志记录和监控。
gRPC设计的动机和原则
- 自由,开放:所有人,所有平台都可以使用,开源,跨平台,跨语言
- 协议可插拔:不同的服务需要使用不同的消息类型和编码机制,例如,JSON,XML和Thirft,所以协议应允许可插拔机制,还有负载均衡,日志,监控等都支持可插拔机制。
- 阻塞和非阻塞:支持客户端和服务器交换的消息序列的异步和同步处理。
- 取消和超时:一次RPC操作可能是持久并且昂贵的,应该允许客户设置取消RPC通信和对这次通信加上超时时间。
- 拒绝:必须允许服务器通过在继续处理请求的同时拒绝新请求的到来并且优雅地关闭
- 流处理:存储系统依靠流和流控制来表达大型数据集,其他服务,如语音到文本或者股票行情,依赖流来表达与时间相关的消息序列。
- 流控制:计算能力和网络容量在客户端和服务器之间通常是不平衡的。流控制允许更好的缓冲区管理,以及过度活跃的对等提供DOS保护。
- 元数据交换:认证或跟踪常见的跨领域问题依赖于不属于服务器的接口数据交换。依赖他们的将这些特性演进到服务,暴露API来提供能力。
- 标准化状态骂:客户端通常以有限的方式相应API调用返回的错误。应约束状态码名称空间,以使这些错误处理决策更加清晰。如果需要更加丰富的特定领域的状态,则可以使用元数据交换机制来提供状态。
- 互通性:报文协议必须遵循普通互联网服务基础框架
数据封装和数据传输问题
网络传输中的内容封装和数据体积问题
早期的RPCJSON的方式,目前的RPC基本上都采用类似的Protobuf的二进制序列化方式。
其差别在于:json的设计是给人看的,protobuf的设计是给机器看的
JSON
优点:在body中对JSON内容编码,极易跨语言,不需要约定特定的复杂编码格式和Stub文件。在版本兼容性上很好,扩展容易。
缺点:JSON难以表达复杂的参数类型,如结构体等;数据冗余和低压缩率使得传输特性能差。
Protobuf(微服务之间的服务器调用,一般采用二进制序列化,比如protobuf)
// XXXX.proto
// rpc服务的类 service关键字, Test服务类名
service Test {
// rpc 关键字,rpc的接口
rpc HowRpcDefine (Request) returns (Response) ; // 定义一个RPC方法
}
// message 类,c++ class
message Request {
//类型 | 字段名字| 标号
int64 user_id = 1;
string name = 2;
}
message Response {
repeated int64 ids = 1; // repeated 表示数组
Value info = 2; // 可嵌套对象
map<int, Value> values = 3; // 可输出map映射
}
message Value {
bool is_man = 1;
int age = 2;
}
网络传输效率问题
grpc采用HTTP2.0,相对于HTTP1.0在更快的传输
和更低的成本
上做了改进。有一下几个基本点:
- HTTP2未改变HTTP的语义(如GET/POST等),只是在传输上优化
- 引入帧,流的概念,在TCP连接中,可以区分多个request和response
- 一个域名只会有一个TCP连接,借助帧,流可以实现多路复用,降低资源消耗
- 引入二进制编码,降低header带来的空间占用
- HTTP1.1核心问题在于:在同一个TCP连接中,没办法区分response是属于哪个请求,一旦多个请求返回的文本内容混在一起,则没法区分数据归属于哪个请求,所以请求只能一个个串行排队发送。这直接导致了TCP资源的闲置。
- HTTP2为了解决这个问题,提出了 流 的概念,每一次请求对应一个流,有一个唯一ID,用来区分不同的请求。基于流的概念,进一步提出了 帧 ,一个请求的数据会被分成多个帧,方便进行数据分割传输,每个帧都唯一属于某一个流ID,将帧按照流ID进行分组,即可分离出不同的请求。这样同一个TCP连接中就可以同时并发多个请求,不同请求的帧数据可穿插在一起,根据流ID分组即可。HTTP2.0基于这种二进制协议的乱序模式 (Duplexing),直接解决了HTTP1.1的核心痛点,通过这种复用TCP连接的方式,不用再同时建多个连接,提升了TCP的利用效率。
gRPC的4种模式
一元RPC模式
一元RPC模式也被称为简单RPC模式
,当客户端调用服务端的远程方法时,客户端发送请求到服务端并且获取一个响应,与响应一起发送的还有状态细节以及trailer元数据。
服务端流RPC模式
在服务器端流RPC模式中,服务器端在接受到客户端额请求消息后,会发回一个响应的序列。这种多个响应所组成的序列被称为流
。在将所有的服务器端响应发送完毕之后,服务器端会以trailer元数据的形式将其状态发送给客户端,从而标记流结束。
客户端流RPC
在客户端流 RPC 模式中,客户端会发送多个请求给服务器端,而不再是单个请求。服务器端则会发送一个响应给客户端。但是,服务器端不一定要等到从客户端接收到所有消息后才发送响应。基于这样的逻辑,我们可以在接收到流中的一条消息或几条消息之后就发送响应,也可以在读取完流中的所有消息之后再发送响应。
双向流RPC模式
在双向流RPC模式中,客户端以消息流的形式发请求给服务端,服务端也以消息流的形式响应。调用必须有客户端发起,但在此之后,通信完全基于gRPC客户端和服务端的应用程序逻辑。
gRPC同步异步
gRPC调用关系图
上图列出了gRPC基础概念及其关系图。其中包括:Service(定义)、RPC、API、Client、Stub、Channel、Server、Service(实现)、ServiceBuilder等。
接下来,以官方提供的example/helloworld为例说明。
proto文件
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
Client
class Greeterclient
是Client,是对Stub封装;通过Stub可以真正调用RPC请求。
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
// Assembles the client's payload, sends it and presents the response back
// from the server.
std::string SayHello(const std::string& user) {
// Data we are sending to the server.
HelloRequest request;
request.set_name(user);
// Container for the data we expect from the server.
HelloReply reply;
// Context for the client. It could be used to convey extra information to
// the server and/or tweak certain RPC behaviors.
ClientContext context;
// The actual RPC.
Status status = stub_->SayHello(&context, request, &reply);
// Act upon its status.
if (status.ok()) {
return reply.message();
} else {
std::cout << status.error_code() << ": " << status.error_message()
<< std::endl;
return "RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
Channel提供一个特定gRPC server的主机和端口简历的连接。
Stub就是在Channel的基础上创建而成。
target_str = "localhost:50051";
auto channel =
grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials());
GreeterClient greeter(channel);
std::string user("world");
std::string reply = greeter.SayHello(user);
server
Server端需要实现对应的RPC,所有的RPC组成了Sevice
// Logic and data behind the server's behavior.
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;
}
};
Server的创建需要一个Builder,添加上监听的地址和端口,注册上该端口上绑定的服务,最后构建出Server并且启动。
ServerBuilder builder;
// Listen on the given address without any authentication mechanism.
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// Register "service" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *synchronous* service.
builder.RegisterService(&service);
// Finally assemble the server.
std::unique_ptr<Server> server(builder.BuildAndStart());
RPC和API的区别:RPC(Remote Procedure Call)是一次远程过程调用的整个动作,而API(Application Programming Interface)是不同语言实现RPC中的具体接口。一个RPC可能对应多种API,比如同步的,异步的,回调的。
异步相关概念
不管Client还是Server,异步gRPC都是利用CompletionQueue API进行异步操作。基本流程:
- 绑定一个CompletionQueue到一个RPC调用
- 利用唯一的void* Tag进行读写
- 调用CompletionQueue::Next()
等待操作完成,完成后通过唯一的Tag来判断对应什么请求/返回进行后续操作
异步Client
greeter_async_client.cc
中是异步Client的Demo,其中只有一次请求,逻辑简单
- 创建CompletionQueue
- 创建RPC(即
clientAsyncResponseReader<HelloReply>
),这里有两种方式:stub_->PrepareAsyncSayHello()
+rpc->StartCall()
stub_->AsyncSayHello()
- 调用
rpc->Finish()
设置请求消息reply和唯一的tag关联,将请求发送出去。 - 使用
cq.Next()
等待Completion Queue返回响应消息体,通过tag关联对应请求。
异步Server
RequestSayHello()
这个函数没有任何说明。只是说:“we request that the system start processing SayHello requset.”也没有说和cq_->Next(&tag, &ok);
的关系。我来说一下流程:
- 创建一个CallData,初始构造列表中将状态设置为CREATE
- 构造函数中,调用Process()成员函数,调用service_->RequestSayHello()
后,状态变更为PROCESS:
- 传入ServerContext ctx_
- 传入HelloRequest request_
- 传入ServerAsyncResponsewriter<HelloReply>
responder_
- 传入ServerCompletionQueue* cq_
- 将对象自身的地址作为tag
传入
- 该动作,能将事件加入事件循环,可以在CompletionQueue中等待
- 收到请求,cp->Next()
的阻塞结束并且返回,得到tag,即上次传入的CallData对象
- 调用tag对应CallData对象Process()
,此时状态Process
- 创建新的CallData对象以接受新请求
- 处理消息并且设置reply_
- 将状态设置为FINISH
- 调用responder_.Finish()
将返回发送给客户端
- 该动作,能将事件加入到循环,可以将CompletionQueue中等待
- 发送完毕,cp->Next()
的阻塞结束并且返回,得到tag。现实中,如果发送有异常应当有其他处理。
- 调用tag对应的CallData对象proceed
,此时状态为FINISH,delete this
清理自己,一条消息处理完成。