vlambda博客
学习文章列表

gRPC 基础概念与传输协议

本文介绍 gRPC 中的基础概念,描绘了它们之间的关系;然后介绍了gRPC的异步相关、流相关的概念;最后介绍了 gRPC 传输使用的具体协议,并且通过 Wireshark 抓包来观察其中具体的内容。

欢迎关注订阅号,接收后续精彩内容
老白码农在奋斗
中年程序员的奋发图强之路
18篇原创内容
Official Account

一、基本概念概览

gRPC 基础概念与传输协议

上图中列出了 gRPC 基础概念及其关系图。其中包括: Service(定义)、RPC、API、Client、Stub、Channel、Server、Service(实现)、ServiceBuilder 等。
接下来,以官方提供的  example/helloworld 为例进行说明。
.proto 文件定义了 服务  Greeter 和  API  SayHello


// helloworld.proto
// The greeting service definition.
service Greeter { // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}}


class GreeterClient 是 Client,是对 Stub 封装;通过 Stub 可以真正的调用RPC请求。


class GreeterClient {
 public:
  GreeterClient(std::shared_ptr<Channel> channel)
      : stub_(Greeter::NewStub(channel)) {}
  
  std::string SayHello(const std::string& user) {
//...
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 端需要实现对应的 RPC,所有的 RPC 组成了 Service:


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;
  }
};



ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<Server> server(builder.BuildAndStart());


RPC 和 API 的区别:RPC 是一次 Call,而 API 是一个函数/接口。一个 RPC 可能对应多种 API,比如同步的、异步的、回调的。一次 RPC 是对某个 API 的一次调用,比如:


std::unique_ptr<ClientAsyncResponseReader<HelloReply> > rpc(
    stub_->PrepareAsyncSayHello(&context, request, &cq));


不管是哪种类型 RPC,都是由 Client 发起请求。

二、异步相关概念

不管是 Client 还是 Server,异步 gRPC 都是利用 CompletionQueue API 进行异步操作。基本的流程:
  • 绑定一个 CompletionQueue 到一个 RPC 调用

  • 利用唯一的 void* Tag 进行读写

  • 调用 CompletionQueue::Next() 等待操作完成,完成后通过唯一的 Tag 来判断对应什么请求/返回进行后续操作

官方文档 Asynchronous-API tutorial 中有上边的介绍,并介绍了异步 client 和 server 的解释,对应着  greeter_async_client.cc 和  greeter_async_server.cc 两个文件。
Client 看文档可以理解,但 Server 的代码复杂,文档和注释中的解释并不是很好理解,接下来会多做一些解释。

1. 异步 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 关联对应的请求

2. 异步 Server

RequestSayHello() 这个函数没有任何的说明。只说是:“we request that the system start processing SayHello requests.” 也没有说跟  cq_->Next(&tag, &ok); 的关系。我这里通过加上一些日志打印,来更清晰的展示 Server 的逻辑:

gRPC 基础概念与传输协议

  • 创建一个CallData,初始构造列表中将状态设置为 CREATE

  • 构造函数中,调用Process()成员函数,调用 service_->RequestSayHello()后,状态变更为 PROCESS:

    • 传入 ServerContext ctx_

    • 传入 HelloRequest request_

    • 传入 ServerAsyncResponseWriter<HelloReply> responder_

    • 传入 ServerCompletionQueue* cq_

    • 该动作,能将事件加入事件循环,可以在CompletionQueue中等待

  • 调用tag对应CallData对象的 Proceed(),此时状态为 Process

    • 创建新的CallData对象以接收新请求

    • 处理消息体并设置 reply_

    • 将状态设置为 FINISH

    • 调用 responder_.Finish() 将返回发送给客户端

    • 该动作,能将事件加入到事件循环,可以在CompletionQueue中等待

  • 发送完毕,cq->Next()的阻塞结束并返回,得到tag。现实中,如果发送有异常应当有其他相关的处理

  • 调用tag对应CallData对象的 Proceed(),此时状态为 FINISH,delete this 清理自己,一条消息处理完成

3. 关系图

将上边的异步 Client 和异步 Server 的逻辑通过关系图进行展示。右侧 RPC 为创建的对象中的内存容,左侧使用相同颜色的小块进行代替。

gRPC 基础概念与传输协议

以下 CallData 并非 gRPC 中的概念,而是异步 Server 在实现过程中为了方便进行的封装,其中的 Status 也是在异步调用过程中自定义的、用于转移的状态。

gRPC 基础概念与传输协议

4. 异步 Client 2

在  example/cpp/helloworld 中还有另外一个异步 Client,对应文件名为  greeter_async_client2.cc。这个例子中使用了 两个线程去分别进行发送请求和处理返回,一个线程批量发出 100 个 SayHello 的请求,另外一个不断的通过  cq_.Next() 来等待返回。
无论是 Client 还是 Server, 在以异步方式进行处理时,都要预先分配好一定的内存/对象,以存储异步的请求或返回。

5. 回调方式的异步调用

在 example/cpp/helloworld 中,还提供了 callback 相关的 Client 和 Server。

使用回调方式简介明了,结构上与同步方式相差不多,但是并发有本质的区别。可以通过文件对比,来查看其中的差异。


cd examples/cpp/helloworld/
vimdiff greeter_callback_client.cc greeter_client.cc
vimdiff greeter_callback_server.cc greeter_server.cc


其实,回调方式的异步调用属于实验性质的,不建议直接在生产环境使用,这里也只做简单的介绍:

Notice: This API is EXPERIMENTAL and may be changed or removed at any time.

5.1 回调 Client

发送单个请求,在调用  SayHello 时,除了传入 Request、 Reply 的地址之外,还需要传入一个接收 Status 的回调函数。
例子中只有一个请求,因此在  SayHello 之后,就直接通过  condition_variable 的 wait函数等待回调结束,然后进行后续处理。这样其实不能进行并发,跟同步请求差别不大。如果要进行大规模的并发,还是需要使用额外的对象进行封装一下。


stub_->async()->SayHello(&context, &request, &reply,
                         [&mu, &cv, &done, &status](Status s) {
                           status = std::move(s);
                           std::lock_guard<std::mutex> lock(mu);
                           done = true;
                           cv.notify_one();
                         });


上边函数调用函数声明如下,很明显这是实验性(experimental)的接口:


void Greeter::Stub::experimental_async::SayHello(
    ::grpc::ClientContext* context, const ::helloworld::HelloRequest* request,
    ::helloworld::HelloReply* response, std::function<void(::grpc::Status)> f);


5.2 回调 Server

与同步Server不同的是:
  • 服务的实现是继承 Greeter::CallbackService

  • SayHello 返回的不是状态,而是 ServerUnaryReactor 指针

  • 通过 CallbackServerContext 获得 reactor

  • 调用 reactor 的 Finish 函数处理返回状态

三、流相关概念

可以按照 Client 和 Server 一次发送/返回的是单个消息还是多个消息,将 gRPC 分为:

  • Unary RPC

  • Server streaming RPC

  • Client streaming RPC

  • Bidirectional streaming RPC

1. Server 对 RPC 的实现

Server 需要实现 proto 中定义的 RPC,每种 RPC 的实现都需要将 ServerContext 作为参数输入。


// rpc GetFeature(Point) returns (Feature) {}
Status GetFeature(ServerContext* context, const Point* point, Feature* feature);


如果涉及到流,则会用Reader 或/和 Writer 作为参数,读取流内容。如 ServerStream 模式下,只有Server端产生流,这时对应的 Server 返回内容,需要使用作为参数传入的 ServerWriter。这类似于以 'w' 打开一个文件,持续的往里写内容,直到没有内容可写关闭。


// rpc ListFeatures(Rectangle) returns (stream Feature) {}
Status ListFeatures(ServerContext* context,
                    const routeguide::Rectangle* rectangle,
                    ServerWriter<Feature>* writer)
;


另一方面,Client 来的流,Server 需要使用一个 ServerReader 来接收。这类似于打开一个文件,读其中的内容,直到读到 EOF 为止类似。


// rpc RecordRoute(stream Point) returns (RouteSummary) {}
Status RecordRoute(ServerContext* context, ServerReader<Point>* reader,
                   RouteSummary* summary)
;


如果 Client 和 Server 都使用流,也就是 Bidirectional-Stream 模式下,输入参数除了 ServerContext 之外,只有一个 ServerReaderWriter 指针。通过该指针,既能读 Client 来的流,又能写 Server 产生的流。

例子中,Server 不断地从 stream 中读,读到了就将对应的写过写到 stream 中,直到客户端告知结束;Server 处理完所有数据之后,直接返回状态码即可。


// rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
Status RouteChat(ServerContext* context,
                 ServerReaderWriter<RouteNote, RouteNote>* stream)
;


2. Client 对 RPC 的调用


// rpc GetFeature(Point) returns (Feature) {}
Status GetFeature(ClientContext* context, const Point& request,
                  Feature* response)
;



Client 在调用 ServerStream RPC 时,不会得到状态,而是返回一个 ClientReader 的指针:


// rpc ListFeatures(Rectangle) returns (stream Feature) {}
unique_ptr<ClientReader<Feature>> ListFeatures(ClientContext* context,
                                               const Rectangle& request);


Reader 通过不断的  Read(),来不断的读取流,结束时  Read() 会返回  false;通过调用  Finish() 来读取返回状态。

调用 ClientStream RPC 时,则会返回一个 ClientWriter 指针:


// rpc RecordRoute(stream Point) returns (RouteSummary) {}
unique_ptr<ClientWriter<Point>> RecordRoute(ClientContext* context,
                                            Route Summary* response);


Writer 会不断的调用  Write() 函数将流中的消息发出;发送完成后调用  WriteDone() 来说明发送完毕;调用  Finish() 来等待对端发送状态。

而双向流的RPC时,会返回 ClientReaderWriter:


// rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
unique_ptr<ClientReaderWriter<RouteNote, RouteNote>> RouteChat(
    ClientContext* context);


前面说明了 Reader 和 Writer 读取和发送完成的函数调用。因为 RPC 都是 Client 请求而后 Server 响应,双向流也是要 Client 先发送完自己流,才有 Server 才可能结束 RPC。所以对于双向流的结束过程是:

  • stream->WriteDone()

  • stream->Finish()

示例中创建了单独的一个线程去发送请求流,在主线程中读返回流,实现了一定程度上的并发。

3. 流是会结束的

并不像长连接,建立上之后就一直保持,有消息的时候发送。(是否有通过建立一个流 RPC 建立推送机制?)

  • Client 发送流,是通过 Writer->WritesDone() 函数结束流

  • Server 发送流,是通过结束 RPC 函数并返回状态码的方式来结束流

  • 流接受者,都是通过 Reader->Read() 返回的 bool 型状态,来判断流是否结束

Server 并没有像 Client 一样调用 WriteDone(),而是在消息之后,将 status code、可选的 status message、可选的 trailing metadata 追加进行发送,这就意味着流结束了。

四、通信协议

本节通过介绍 gRPC 协议文档描述和对 helloworld 的抓包,来说明 gRPC 到底是如何传输的。

官方文档《gRPC over HTTP2》中有描述 gRPC 基于 HTTP2 的具体实现,主要介绍的就是协议,也就是 gRPC 的请求和返回是如何基于 HTTP 协议构造的。如果不熟悉 HTTP2 可以阅读一下 RFC 7540。

1. ABNF 语法

ABNF 语法是一种描述协议的标准,gRPC 协议也是使用 ABNF 语法描述,几种常见的运算符在第三节中有介绍:
3. Operators
3.1. Concatenation: Rule1 Rule2
3.2. Alternatives: Rule1 / Rule2
3.3. Incremental Alternatives: Rule1 =/ Rule2
3.4. Value Range Alternatives: %c##-##
3.5. Sequence Group: (Rule1 Rule2)
3.6. Variable Repetition: *Rule
3.7. Specific Repetition: nRule
3.8. Optional Sequence: [RULE]
3.9. Comment: ; Comment
3.10. Operator Precedence

2. 请求协议

*<element> 表示 element 会重复多次(最少0次)。知道这个就能理解概况里的描述了:
Request → Request-Headers *Length-Prefixed-Message EOS
Request-Headers → Call-Definition *Custom-Metadata

这表示  Request 是由 3 部分组成,首先是  Request-Headers,接下来是可能多次出现的  Length-Prefixed-Message,最后以一个  EOS 结尾(EOS表示 End-Of-Stream)。

2.1 Request-Headers

根据上边的协议描述,  Request-Headers 是由一个  Call-Definition 和若干  Custom-Metadata 组成。
[] 表示最多出现一次,比如  Call-Definition 有很多组成部分,其中  Message-Type 等是选填的:
Call-Definition → Method Scheme Path TE [Authority] [Timeout] Content-Type [Message-Type] [Message-Encoding] [Message-Accept-Encoding] [User-Agent]

通过 Wireshark 抓包可以看到请求的 Call-Definition 中共有所有要求的 Header,还有额外可选的,比如user-agent:

gRPC 基础概念与传输协议

因为 helloworld 的示例比较简单,请求中没有填写自定义的元数据(Custom-Metadata)

2.2 Length-Prefixed-Message

传输的 Length-Prefixed-Message 也分为三部分:
Length-Prefixed-Message → Compressed-Flag Message-Length Message

同样的,Wireshark 抓到的请求中也有这部分信息,并且设置 .proto 文件的搜索路径之后可以自动解析 PB:

gRPC 基础概念与传输协议

其中第一个红框(Compressed-Flag)表示不进行压缩,第二个红框(Message-Length)表示消息长度为 7,蓝色反选部分则是 Protobuf 序列化的二进制内容,也就是 Message。
在 gRPC 的核心概念介绍时提到,gRPC 默认使用 Protobuf 作为接口定义语言(IDL),也可以使用其他的 IDL 替代 Protobuf:

By default, gRPC uses protocol buffers as the Interface Definition Language (IDL) for describing both the service interface and the structure of the payload messages. It is possible to use other alternatives if desired.

这里 Length-Prefixed-Message 中传输的可以是 PB 也可以是 JSON,须通过 Content-Type 头中描述告知。

2.3 EOS

End-Of-Stream 并没有单独的数据去描述,而是通过 HTTP2 的数据帧上带一个 END_STREAM 的 flag 来标识的。比如 helloworld 中请求的数据帧,也携带了 END_STREAM 的标签:

3. 返回协议

() 表示括号中的内容作为单个元素对待, / 表示前后两个元素可选其一。Response 的定义说明,可以有两种返回形式,一种是消息头、消息体、Trailer,另外一种是只带 Trailer:
Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only

这里需要区分 gRPC 的 Status 和 HTTP 的 Status 两种状态。

Response-Headers → HTTP-Status [Message-Encoding] [Message-Accept-Encoding] Content-Type *Custom-Metadata
Trailers-Only → HTTP-Status Content-Type Trailers
Trailers → Status [Status-Message] *Custom-Metadata

不管是哪种形式,最后一部分都是 Trailers,其中包含了gRPC的状态码、状态信息和额外的自定义元数据。

同样地,使用 END_STREAM 的 flag 标识最后 Trailer 的结束。

4. 与 HTTP/2 的关系

The libraries in this repository provide a concrete implemnetation of the gRPC protocol, layered over HTTP/2.

gRPC 关系 HTTP/2
Bi-directional stream Mapped to Stream
Call Header
[Initial-Metadata]
Sent as Headers + HPACK
[Payload Messages]
(Serialized byte stream)
Fragmented into Frame
Status
[Status-Metadata(Trailing-Metadata)]
Sent as Trailing headers

参考资料

  • https://grpc.io/docs/what-is-grpc/core-concepts/

  • https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md

  • https://grpc.io/blog/wireshark/




往期推荐