本文介绍gRPC的基础概念。首先通过关系图直观展示这些基础概念之间关联,介绍异步gRPC的Server和Client的逻辑;然后介绍RPC的类型,阅读和抓包分析gRPC的通信过程协议,gRPC上下文;最后分析grpc.pb.h文件的内容,包括Stub的能力、Service的种类以及与核心库的关系。
之所以谓之基础,是这些内容基本不涉及gRPCCore的内容。
上图中列出了gRPC基础概念及其关系图。其中包括:Service(定义)、RPC、API、Client、Stub、Channel、Server、Service(实现)、ServiceBuilder等。
接下来,以官方提供的example/helloworld为例进行说明。
.proto文件定义了服务Greeter和APISayHello:
classGreeterClient是Client,是对Stub封装;通过Stub可以真正的调用RPC请求。
Channel提供一个与特定gRPCserver的主机和端口建立的连接。
Stub就是在Channel的基础上创建而成的。
Server端需要实现对应的RPC,所有的RPC组成了Service:
Server的创建需要一个Builder,添加上监听的地址和端口,注册上该端口上绑定的服务,最后构建出Server并启动:
RPC和API的区别:RPC(RemoteProcedureCall)是一次远程过程调用的整个动作,而API(ApplicationProgrammingInterface)是不同语言在实现RPC中的具体接口。一个RPC可能对应多种API,比如同步的、异步的、回调的。一次RPC是对某个API的一次调用,比如:
不管是哪种类型RPC,都是由Client发起请求。
Client看文档可以理解,但Server的代码复杂,文档和注释中的解释并不是很好理解,接下来会多做一些解释。
1.异步Client
greeter_async_client.cc中是异步Client的Demo,其中只有一次请求,逻辑简单。
stub_->PrepareAsyncSayHello()+rpc->StartCall()
stub_->AsyncSayHello()
2.异步Server
RequestSayHello()这个函数没有任何的说明。只说是:"we*request*thatthesystemstartprocessingSayHellorequests."也没有说跟cq_->Next(&tag,&ok);的关系。我这里通过加上一些日志打印,来更清晰的展示Server的逻辑:
上边绿色的部分为创建的第一个CallData对象地址,橙色的为第二个CallData的地址。
传入ServerContextctx_
传入HelloRequestrequest_
传入ServerAsyncResponseWriter
传入ServerCompletionQueue*cq_
将对象自身的地址作为tag传入
该动作,能将事件加入事件循环,可以在CompletionQueue中等待
创建新的CallData对象以接收新请求
处理消息体并设置reply_
将状态设置为FINISH
调用responder_.Finish()将返回发送给客户端
该动作,能将事件加入到事件循环,可以在CompletionQueue中等待
3.关系图
将上边的异步Client和异步Server的逻辑通过关系图进行展示。右侧RPC为创建的对象中的内存容,左侧使用相同颜色的小块进行代替。
以下CallData并非gRPC中的概念,而是异步Server在实现过程中为了方便进行的封装,其中的Status也是在异步调用过程中自定义的、用于转移的状态。
4.异步Client2
在example/cpp/helloworld中还有另外一个异步Client,对应文件名为greeter_async_client2.cc。这个例子中使用了两个线程去分别进行发送请求和处理返回,一个线程批量发出100个SayHello的请求,另外一个不断的通过cq_.Next()来等待返回。
无论是Client还是Server,在以异步方式进行处理时,都要预先分配好一定的内存/对象,以存储异步的请求或返回。
5.回调方式的异步调用
使用回调方式简介明了,结构上与同步方式相差不多,但是并发有本质的区别。可以通过文件对比,来查看其中的差异。
其实,回调方式的异步调用属于实验性质的,不建议直接在生产环境使用,这里也只做简单的介绍:
5.1回调Client
发送单个请求,在调用SayHello时,除了传入Request、Reply的地址之外,还需要传入一个接收Status的回调函数。
例子中只有一个请求,因此在SayHello之后,就直接通过condition_variable的wait函数等待回调结束,然后进行后续处理。这样其实不能进行并发,跟同步请求差别不大。如果要进行大规模的并发,还是需要使用额外的对象进行封装一下。
5.2回调Server
与同步Server不同的是:
可以按照Client和Server一次发送/返回的是单个消息还是多个消息,将gRPC分为:
1.Server对RPC的实现
Server需要实现proto中定义的RPC,每种RPC的实现都需要将ServerContext作为参数输入。
如果是一元(Unary)RPC调用,则像调用普通函数一样。将Request和Reply的对象地址作为参数传入,函数中将根据Request的内容,在Reply的地址上写上对应的返回内容。
如果涉及到流,则会用Reader或/和Writer作为参数,读取流内容。如ServerStream模式下,只有Server端产生流,这时对应的Server返回内容,需要使用作为参数传入的ServerWriter。这类似于以'w'打开一个文件,持续的往里写内容,直到没有内容可写关闭。
另一方面,Client来的流,Server需要使用一个ServerReader来接收。这类似于打开一个文件,读其中的内容,直到读到EOF为止类似。
如果Client和Server都使用流,也就是Bidirectional-Stream模式下,输入参数除了ServerContext之外,只有一个ServerReaderWriter指针。通过该指针,既能读Client来的流,又能写Server产生的流。
例子中,Server不断地从stream中读,读到了就将对应的写过写到stream中,直到客户端告知结束;Server处理完所有数据之后,直接返回状态码即可。
2.Client对RPC的调用
Client在调用一元(Unary)RPC时,像调用普通函数一样,除了传入ClientContext之外,将Request和Response的地址,返回的是RPC状态:
Client在调用ServerStreamRPC时,不会得到状态,而是返回一个ClientReader的指针:
Reader通过不断的Read(),来不断的读取流,结束时Read()会返回false;通过调用Finish()来读取返回状态。
调用ClientStreamRPC时,则会返回一个ClientWriter指针:
Writer会不断的调用Write()函数将流中的消息发出;发送完成后调用WriteDone()来说明发送完毕;调用Finish()来等待对端发送状态。
而双向流的RPC时,会返回ClientReaderWriter,:
前面说明了Reader和Writer读取和发送完成的函数调用。因为RPC都是Client请求而后Server响应,双向流也是要Client先发送完自己流,才有Server才可能结束RPC。所以对于双向流的结束过程是:
示例中创建了单独的一个线程去发送请求流,在主线程中读返回流,实现了一定程度上的并发。
3.流是会结束的
并不似长连接,建立上之后就一直保持,有消息的时候发送。(是否有通过建立一个流RPC建立推送机制?)
Server并没有像Client一样调用WriteDone(),而是在消息之后,将statuscode、可选的statusmessage、可选的trailingmetadata追加进行发送,这就意味着流结束了。
本节通过介绍gRPC协议文档描述和对helloworld的抓包,来说明gRPC到底是如何传输的。
1.ABNF语法
2.请求协议
*
这表示Request是由3部分组成,首先是Request-Headers,接下来是可能多次出现的Length-Prefixed-Message,最后以一个EOS结尾(EOS表示End-Of-Stream)。
2.1Request-Headers
根据上边的协议描述,Request-Headers是由一个Call-Definition和若干Custom-Metadata组成。
[]表示最多出现一次,比如Call-Definition有很多组成部分,其中Message-Type等是选填的:
通过Wireshark抓包可以看到请求的Call-Definition中共有所有要求的Header,还有额外可选的,比如user-agent:
因为helloworld的示例比较简单,请求中没有填写自定义的元数据(Custom-Metadata)
2.2Length-Prefixed-Message
传输的Length-Prefixed-Message也分为三部分:
同样的,Wireshark抓到的请求中也有这部分信息,并且设置.proto文件的搜索路径之后可以自动解析PB:
其中第一个红框(Compressed-Flag)表示不进行压缩,第二个红框(Message-Length)表示消息长度为7,蓝色反选部分则是Protobuf序列化的二进制内容,也就是Message。
这里Length-Prefixed-Message中传输的可以是PB也可以是JSON,须通过Content-Type头中描述告知。
2.3EOS
End-Of-Stream并没有单独的数据去描述,而是通过HTTP2的数据帧上带一个END_STREAM的flag来标识的。比如helloworld中请求的数据帧,也携带了END_STREAM的标签:
3.返回协议
()表示括号中的内容作为单个元素对待,/表示前后两个元素可选其一。Response的定义说明,可以有两种返回形式,一种是消息头、消息体、Trailer,另外一种是只带Trailer:
这里需要区分gRPC的Status和HTTP的Status两种状态。
不管是哪种形式,最后一部分都是Trailers,其中包含了gRPC的状态码、状态信息和额外的自定义元数据。
同样地,使用END_STREAM的flag标识最后Trailer的结束。
4.与HTTP/2的关系
ThelibrariesinthisrepositoryprovideaconcreteimplemnetationofthegRPCprotocol,layeredoverHTTP/2.
gRPC支持上下文的传递,其主要用途有:
客户端添加自定义的metadatakey-value对没有特别的区分,而服务端添加的,则有inital和trailing两种metadata的区分。这也分别对应这ClientContext只有一个添加Metadata的函数:
而ServerContext则有两个:
还有一种CallbackServer对应的上下文叫做CallbackServerContext,它与ServerContext继承自同一个基类,功能基本上相同。区别在于:
1.Stub
.proto中的一个service只有一个Stub,该类中会提供对应每个RPC所有的同步、异步、回调等方式的函数都包含在该类中,而该类继承自接口类StubInterface。
为什么需要一个StubInterface来让Stub继承,而不是直接产生Stub?别的复杂的proto会有多个Stub继承同一个StubInterface的情况?不会,因为每个RPC对应的函数名是不同。
Greeter中唯一一个函数是用于创建Stub的静态函数NewStub:
Stub中同步、异步方式的函数是直接作为Stub的成员函数提供,比如针对一元调用:
回调方式的RPC调用是通过一个experimental_async的类进行了封装(有个async_stub_的成员变量),所以回调Client中提到,回调的调用方式用法是stub_->async()->SayHello(...)。
experimental_async类定义中将Stub类作为自己的友元,自己的成员可以被Stub直接访问,而在StubInterface中也对应有一个experimental_async_interface的接口类,规定了要实现哪些接口。
2.Service
有几个概念都叫Service:proto文件中RPC的集合、proto文件中service产生源文件中的Greeter::Service类、gRPC框架中的::grpc::Service类。本小节说的Service就是helloworld.grpc.pb.h中的Greeter::Service。
2.1Service是如何定义的
helloworld.grpc.pb.h文件中共定义了7种Service,拿出最常用的Service和AsyncService两个定义来说明下Service的定义过程:通过类模板链式继承。
Service跟其他几种Service不同,直接继承自grpc::Service,而其他的Service都是由类模板构造出来的,而且使用类模板进行嵌套,最基础的类就是这里的Service。
Service有以下特点:
所以Service类中的所有RPCAPI都是同步的。
再看AsyncService的具体定义:
所以AsyncService的含义就是继承自Service,加上了WithAsyncMethod_SayHello的新功能:
通过gRPC提供的route_guide.proto例子能更明显的理解这点:
这里RouteGuide服务中有4个RPC,GetFeature、ListFeatures、RecordRoute、RouteChat,通过4个WithAsyncMethod_{RPC_name}的类模板嵌套,能将4个API都设置成ApiType::ASYNC、添加上对应的RequestXXX()函数、禁用同步函数。
2.2Service的种类
helloworld.grpc.pb.h文件中7种Service中,有3对Service的真正含义都相同(出于什么目的使用不同的名称?),实际只剩下4种Service。前三种在前边的同步、异步、回调Server的介绍中都有涉及。
其实这些不同类型的Service是跟前边提到的api_type_有关。使用不同的::grpc::Service::MarkMethodXXX设置不同的ApiType会产生不同的API模板类,所有API模板类级联起来,就得到了不同的Service。这三者的关系简单列举如下:
另外还有两种模板是通过设置其他属性产生的,这里暂时不做介绍:
3.与::grpc核心库的关系
Stub类中主要是用到gRPCChannel和不同类型RPC对应的方法实现
Service类则继承自::grpc::Service具备其父类的能力,需要自己实现一些RPC方法具体的处理逻辑。其它Service涉及到gRPC核心库的联系有:
AsyncService::RequestSayHello()调用::grpc::Service::RequestAsyncUnary。
CallbackService::SayHello()函数返回的是::grpc::ServerUnaryReactor指针。
CallbackService::SetMessageAllocatorFor_SayHello()函数中调用
::grpc::internal::CallbackUnaryHandler::SetMessageAllocator()函数设置RPC方法的回调的消息分配器。