深入浅出gRPC
李林锋
《Netty 权威指南》、《分布式服务框架原理与实践》作者。
立即订阅
11236 人已学习
课程目录
已更新 6 讲 / 共 6 讲
01 | gRPC 入门及服务端创建和调用原理
02 | 客户端创建和调用原理
03 | gRPC 线程模型分析
04 | gRPC 服务调用原理
05 | gRPC 安全性设计
06 | gRPC 序列化机制
深入浅出gRPC
登录|注册

02 | 客户端创建和调用原理

李林锋 2018-03-13

1. gRPC 客户端创建流程

1.1 背景

gRPC 是在 HTTP/2 之上实现的 RPC 框架,HTTP/2 是第 7 层(应用层)协议,它运行在 TCP(第 4 层 - 传输层)协议之上,相比于传统的 REST/JSON 机制有诸多的优点:
基于 HTTP/2 之上的二进制协议(Protobuf 序列化机制);
一个连接上可以多路复用,并发处理多个请求和响应;
多种语言的类库实现;
服务定义文件和自动代码生成(.proto 文件和 Protobuf 编译工具)。
此外,gRPC 还提供了很多扩展点,用于对框架进行功能定制和扩展,例如,通过开放负载均衡接口可以无缝的与第三方组件进行集成对接(Zookeeper、域名解析服务、SLB 服务等)。
一个完整的 RPC 调用流程示例如下:
gRPC 的 RPC 调用与上述流程相似,下面我们一起学习下 gRPC 的客户端创建和服务调用流程。

1.2 业务代码示例

以 gRPC 入门级的 helloworld Demo 为例,客户端发起 RPC 调用的代码主要包括如下几部分:
根据 hostname 和 port 创建 ManagedChannelImpl;
根据 helloworld.proto 文件生成的 GreeterGrpc 创建客户端 Stub,用来发起 RPC 调用;
使用客户端 Stub(GreeterBlockingStub)发起 RPC 调用,获取响应。
相关示例代码如下所示(HelloWorldClient 类):
HelloWorldClient(ManagedChannelBuilder<?> channelBuilder) {
channel = channelBuilder.build();
blockingStub = GreeterGrpc.newBlockingStub(channel);
futureStub = GreeterGrpc.newFutureStub(channel);
stub = GreeterGrpc.newStub(channel);
}
public void blockingGreet(String name) {
logger.info("Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
try {
HelloReply response = blockingStub
.sayHello(request);
...

1.3 RPC 调用流程

gRPC 的客户端调用主要包括基于 Netty 的 HTTP/2 客户端创建、客户端负载均衡、请求消息的发送和响应接收处理四个流程。

1.3.1 客户端调用总体流程

gRPC 的客户端调用总体流程如下图所示:
gRPC 的客户端调用流程如下:
客户端 Stub(GreeterBlockingStub) 调用 sayHello(request),发起 RPC 调用;
通过 DnsNameResolver 进行域名解析,获取服务端的地址信息(列表),随后使用默认的 LoadBalancer 策略,选择一个具体的 gRPC 服务端实例;
如果与路由选中的服务端之间没有可用的连接,则创建 NettyClientTransport 和 NettyClientHandler,发起 HTTP/2 连接;
对请求消息使用 PB(Protobuf)做序列化,通过 HTTP/2 Stream 发送给 gRPC 服务端;
接收到服务端响应之后,使用 PB(Protobuf)做反序列化;
回调 GrpcFuture 的 set(Response) 方法,唤醒阻塞的客户端调用线程,获取 RPC 响应。
需要指出的是,客户端同步阻塞 RPC 调用阻塞的是调用方线程(通常是业务线程),底层 Transport 的 I/O 线程(Netty 的 NioEventLoop)仍然是非阻塞的。

1.3.2 ManagedChannel 创建流程

ManagedChannel 是对 Transport 层 SocketChannel 的抽象,Transport 层负责协议消息的序列化和反序列化,以及协议消息的发送和读取。
ManagedChannel 将处理后的请求和响应传递给与之相关联的 ClientCall 进行上层处理,同时,ManagedChannel 提供了对 Channel 的生命周期管理(链路创建、空闲、关闭等)。
ManagedChannel 提供了接口式的切面 ClientInterceptor,它可以拦截 RPC 客户端调用,注入扩展点,以及功能定制,方便框架的使用者对 gRPC 进行功能扩展。
ManagedChannel 的主要实现类 ManagedChannelImpl 创建流程如下:
流程关键技术点解读:
使用 builder 模式创建 ManagedChannelBuilder 实现类 NettyChannelBuilder,NettyChannelBuilder 提供了 buildTransportFactory 工厂方法创建 NettyTransportFactory,最终用于创建 NettyClientTransport;
初始化 HTTP/2 连接方式:采用 plaintext 协商模式还是默认的 TLS 模式,HTTP/2 的连接有两种模式,h2(基于 TLS 之上构建的 HTTP/2)和 h2c(直接在 TCP 之上构建的 HTTP/2);
创建 NameResolver.Factory 工厂类,用于服务端 URI 的解析,gRPC 默认采用 DNS 域名解析方式。
ManagedChannel 实例构造完成之后,即可创建 ClientCall,发起 RPC 调用。

1.3.3 ClientCall 创建流程

完成 ManagedChannelImpl 创建之后,由 ManagedChannelImpl 发起创建一个新的 ClientCall 实例。ClientCall 的用途是业务应用层的消息调度和处理,它的典型用法如下:
call = channel.newCall(unaryMethod, callOptions);
call.start(listener, headers);
call.sendMessage(message);
call.halfClose();
call.request(1);
// wait for listener.onMessage()
ClientCall 实例的创建流程如下所示:
流程关键技术点解读:
ClientCallImpl 的主要构造参数是 MethodDescriptor 和 CallOptions,其中 MethodDescriptor 存放了需要调用 RPC 服务的接口名、方法名、服务调用的方式(例如 UNARY 类型)以及请求和响应的序列化和反序列化实现类。
CallOptions 则存放了 RPC 调用的其它附加信息,例如超时时间、鉴权信息、消息长度限制和执行客户端调用的线程池等。
设置压缩和解压缩的注册类(CompressorRegistry 和 DecompressorRegistry),以便可以按照指定的压缩算法对 HTTP/2 消息做压缩和解压缩。
ClientCallImpl 实例创建完成之后,就可以调用 ClientTransport,创建 HTTP/2 Client,向 gRPC 服务端发起远程服务调用。

1.3.4 基于 Netty 的 HTTP/2 Client 创建流程

gRPC 客户端底层基于 Netty4.1 的 HTTP/2 协议栈框架构建,以便可以使用 HTTP/2 协议来承载 RPC 消息,在满足标准化规范的前提下,提升通信性能。
gRPC HTTP/2 协议栈(客户端)的关键实现是 NettyClientTransport 和 NettyClientHandler,客户端初始化流程如下所示:
流程关键技术点解读:
NettyClientHandler 的创建:级联创建 Netty 的 Http2FrameReader、Http2FrameWriter 和 Http2Connection,用于构建基于 Netty 的 gRPC HTTP/2 客户端协议栈。
HTTP/2 Client 启动:仍然基于 Netty 的 Bootstrap 来初始化并启动客户端,但是有两个细节需要注意:
NettyClientHandler(实际被包装成 ProtocolNegotiator.Handler,用于 HTTP/2 的握手协商)创建之后,不是由传统的 ChannelInitializer 在初始化 Channel 时将 NettyClientHandler 加入到 pipeline 中,而是直接通过 Bootstrap 的 handler 方法直接加入到 pipeline 中,以便可以立即接收发送任务。
客户端使用的 work 线程组并非通常意义的 EventLoopGroup,而是一个 EventLoop:即 HTTP/2 客户端使用的 work 线程并非一组线程(默认线程数为 CPU 内核 * 2),而是一个 EventLoop 线程。这个其实也很容易理解,一个 NioEventLoop 线程可以同时处理多个 HTTP/2 客户端连接,它是多路复用的,对于单个 HTTP/2 客户端,如果默认独占一个 work 线程组,将造成极大的资源浪费,同时也可能会导致句柄溢出(并发启动大量 HTTP/2 客户端)。
WriteQueue 创建:Netty 的 NioSocketChannel 初始化并向 Selector 注册之后(发起 HTTP 连接之前),立即由 NettyClientHandler 创建 WriteQueue,用于接收并处理 gRPC 内部的各种 Command,例如链路关闭指令、发送 Frame 指令、发送 Ping 指令等。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《深入浅出gRPC》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(12)

  • Xg huang
    请教一下作者,grpc 是否可以由服务端主动下发消息给客户端?

    作者回复: 不可以,可以一个请求,服务端不断推送响应,但是如果没请求是不能直接推送的

    2018-03-28
    1
    3
  • 宋磊
    请问grpc client 如何控制请求超时时间?

    作者回复: 通过stub的withDeadlineAfter方法设置超时

    2018-06-06
    1
  • 阿里
    没有语言吗?

    作者回复: 语音吗?没有,这个不是音频的

    2018-03-14
    1
  • Frank
    原代碼無法下載
    2019-09-21
  • Brave Shine
    grpc还可以由uds实现
    2019-09-08
  • 三日月之舞
    请教下,如果grpc服务部署在k8s中,无需客户端负载。针对客户端而言,多个请求使用同一连接,换句话说,请求就不会被负载均衡,只会指向同一个pod?
    2019-05-27
  • jayyi
    请问下作者 客户端的channel需要做池化处理吗
    2019-01-29
  • jayyi
    请问下作者 客户端channel需要做池化处理吗
    2019-01-29
  • 黄河
    pickfirst也应该是有负载均衡的,依赖的是dns解析记录返回顺序,一般都是随机的,所以pickfirst也可以负载均衡

    作者回复: 是的,gRPC这块儿开放了LB能力,给应用自己做定制

    2018-06-06
  • 宋磊
    请问 grpc client 如何控制超市时间?

    作者回复: 通过stub的withDeadlineAfter方法可以设置超时时间

    2018-06-06
  • Len
    请问老师:
    如果是在客户端做负载均衡,那么客户端也只需要一个 Netty 的 NioEventLoop 就够了吗?

    作者回复: 如果服务端是集群,通常客户端也是有多个NioEventLoop来处理多个链路的,理论上1个也可以,不过高并发场景肯定不行

    2018-05-27
  • billow
    感觉对netty的使用有点太反常规了😇

    作者回复: 哈哈,还好吧,整体上还是比较正常,不过消息头和体的并行处理有点意思

    2018-04-25
收起评论
12
返回
顶部