微信号:infoqchina

介绍:有内容的技术社区媒体

gRPC客户端创建和调用原理解析

2018-03-14 08:00 李林锋

 点击关注 InfoQ,置顶公众号

接收程序员的 8 点技术早餐

作者|李林锋
编辑|张浩

本文选自“深入浅出 gRPC”系列文章(6 篇共 45000 字),更多 gRPC 相关内容请点击下方图片详细了解。


背景

gRPC 是在 HTTP/2 之上实现的 RPC 框架,HTTP/2 是第 7 层(应用层)协议,它运行在 TCP(第 4 层 - 传输层)协议之上,相比于传统的 REST/JSON 机制有诸多的优点:

  1. 基于 HTTP/2 之上的二进制协议(Protobuf 序列化机制);

  2. 一个连接上可以多路复用,并发处理多个请求和响应;

  3. 多种语言的类库实现;

  4. 服务定义文件和自动代码生成(.proto 文件和 Protobuf 编译工具)。

此外,gRPC 还提供了很多扩展点,用于对框架进行功能定制和扩展,例如,通过开放负载均衡接口可以无缝的与第三方组件进行集成对接(Zookeeper、域名解析服务、SLB 服务等)。

一个完整的 RPC 调用流程示例如下:


gRPC 的 RPC 调用与上述流程相似,下面我们一起学习下 gRPC 的客户端创建和服务调用流程。

客户端调用总体流程

gRPC 的客户端调用总体流程如下图所示:


gRPC 的客户端调用流程如下:

  1. 客户端 Stub(GreeterBlockingStub) 调用 sayHello(request),发起 RPC 调用;

  2. 通过 DnsNameResolver 进行域名解析,获取服务端的地址信息(列表),随后使用默认的 LoadBalancer 策略,选择一个具体的 gRPC 服务端实例;

  3. 如果与路由选中的服务端之间没有可用的连接,则创建 NettyClientTransport 和 NettyClientHandler,发起 HTTP/2 连接;

  4. 对请求消息使用 PB(Protobuf)做序列化,通过 HTTP/2 Stream 发送给 gRPC 服务端;

  5. 接收到服务端响应之后,使用 PB(Protobuf)做反序列化;

  6. 回调 GrpcFuture 的 set(Response) 方法,唤醒阻塞的客户端调用线程,获取 RPC 响应。

需要指出的是,客户端同步阻塞 RPC 调用阻塞的是调用方线程(通常是业务线程),底层 Transport 的 I/O 线程(Netty 的 NioEventLoop)仍然是非阻塞的。

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

gRPC 客户端底层基于 Netty4.1 的 HTTP/2 协议栈框架构建,以便可以使用 HTTP/2 协议来承载 RPC 消息,在满足标准化规范的前提下,提升通信性能。

gRPC HTTP/2 协议栈(客户端)的关键实现是 NettyClientTransport 和 NettyClientHandler,客户端初始化流程如下所示:


流程关键技术点解读:

  1. NettyClientHandler 的创建: 级联创建 Netty 的 Http2FrameReader、Http2FrameWriter 和 Http2Connection,用于构建基于 Netty 的 gRPC HTTP/2 客户端协议栈。

  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 客户端)。

  3. WriteQueue 创建:Netty 的 NioSocketChannel 初始化并向 Selector 注册之后(发起 HTTP 连接之前),立即由 NettyClientHandler 创建 WriteQueue,用于接收并处理 gRPC 内部的各种 Command,例如链路关闭指令、发送 Frame 指令、发送 Ping 指令等。

HTTP/2 Client 创建完成之后,即可由客户端根据协商策略发起 HTTP/2 连接。如果连接创建成功,后续即可复用该 HTTP/2 连接,进行 RPC 调用。

RPC 请求消息发送流程

gRPC 默认基于 Netty HTTP/2 + PB 进行 RPC 调用,请求消息发送流程如下所示:


流程关键技术点解读:

  1. ClientCallImpl 的 sendMessage 调用,主要完成了请求对象的序列化(基于 PB)、HTTP/2 Frame 的初始化;

  2. ClientCallImpl 的 halfClose 调用将客户端准备就绪的请求 Frame 封装成自定义的 SendGrpcFrameCommand,写入到 WriteQueue 中;

  3. WriteQueue 执行 flush() 将 SendGrpcFrameCommand 写入到 Netty 的 Channel 中,调用 Channel 的 write 方法,被 NettyClientHandler 拦截到,由 NettyClientHandler 负责具体的发送操作;

  4. NettyClientHandler 调用 Http2ConnectionEncoder 的 writeData 方法,将 Frame 写入到 HTTP/2 Stream 中,完成请求消息的发送。

客户端源码分析

gRPC 客户端调用原理并不复杂,但是代码却相对比较繁杂。下面围绕关键的类库,对主要功能点进行源码分析。

ProtocolNegotiator 功能和源码分析

ProtocolNegotiator 用于 HTTP/2 连接创建的协商,gRPC 支持三种策略并有三个实现子类:


gRPC 的 ProtocolNegotiator 实现类完全遵循 HTTP/2 相关规范,以 PlaintextUpgradeNegotiator 为例,通过设置 Http2ClientUpgradeCodec,用于 101 协商和协议升级,相关代码如下所示(PlaintextUpgradeNegotiator 类):

public Handler newHandler(GrpcHttp2ConnectionHandler handler) {
      Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(handler);
      HttpClientCodec httpClientCodec = new HttpClientCodec();
      final HttpClientUpgradeHandler upgrader =
          new HttpClientUpgradeHandler(httpClientCodec, upgradeCodec, 1000);
      return new BufferingHttp2UpgradeHandler(upgrader);
    }


RPC 请求调用源码分析

请求调用主要有两步:请求 Frame 构造和 Frame 发送,请求 Frame 构造代码如下所示(ClientCallImpl 类):

public void sendMessage(ReqT message) {
    Preconditions.checkState(stream != null, "Not started");
    Preconditions.checkState(!cancelCalled, "call was cancelled");
    Preconditions.checkState(!halfCloseCalled, "call was half-closed");
    try {
      InputStream messageIs = method.streamRequest(message);
      stream.writeMessage(messageIs);
...


使用 PB 对请求消息做序列化,生成 InputStream,构造请求 Frame:

private int writeUncompressed(InputStream message, int messageLength) throws IOException {
    if (messageLength != -1) {
      statsTraceCtx.outboundWireSize(messageLength);
      return writeKnownLengthUncompressed(message, messageLength);
    }
    BufferChainOutputStream bufferChain = new BufferChainOutputStream();
    int written = writeToOutputStream(message, bufferChain);
    if (maxOutboundMessageSize >= 0 && written > maxOutboundMessageSize) {
      throw Status.INTERNAL
          .withDescription(
              String.format("message too large %d > %d", written , maxOutboundMessageSize))
          .asRuntimeException();
    }
    writeBufferChain(bufferChain, false);
    return written;
}


Frame 发送代码如下所示:

public void writeFrame(WritableBuffer frame, boolean endOfStream, boolean flush) {
      ByteBuf bytebuf = frame == null ? EMPTY_BUFFER : ((NettyWritableBuffer) frame).bytebuf();
      final int numBytes = bytebuf.readableBytes();
      if (numBytes > 0) {
        onSendingBytes(numBytes);
        writeQueue.enqueue(
            new SendGrpcFrameCommand(transportState(), bytebuf, endOfStream),
            channel.newPromise().addListener(new ChannelFutureListener() {
              @Override
              public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                  transportState().onSentBytes(numBytes);
                }
              }
            }), flush);


NettyClientHandler 接收到发送事件之后,调用 Http2ConnectionEncoder 将 Frame 写入 Netty HTTP/2 协议栈(NettyClientHandler 类):

private void sendGrpcFrame(ChannelHandlerContext ctx, SendGrpcFrameCommand cmd,
      ChannelPromise promise) {
    encoder().writeData(ctx, cmd.streamId(), cmd.content(), 0, cmd.endStream(), promise);
  }
作者介绍

李林锋 《Netty 权威指南》和《分布式服务框架原理与实践》作者。有多年 Java NIO、平台中间件、PaaS 平台、API 网关设计和开发经验。精通 Netty、Mina、分布式服务框架、云计算等,目前从事软件公司的 API 开放相关的架构和设计工作。

我模拟了一个用浏览器挖矿的代码


 
InfoQ 更多文章 在滴滴,我们是怎么做运维的? 美媒评论:中国在技术方面正在迎头追赶美国 作为部门领导,自己天天撸代码忙成狗,下属却没事干,怎么办?丨管理课推荐 爆苹果技术顾问窃取用户信息并敲诈勒索;币安遭黑客攻击,数字货币全盘大跌;百度开源项目ECharts首进Apache孵化器丨Q新闻 Java EE:更名实属无奈,未来路在何方?
猜您喜欢 我是北漂程序员,没有假装在生活 上传图片攻略全解 理查德·斯托曼经典语录集锦 10月22日第五届iWeb峰会成都站报名开启! 【福利】百度hadoop架构师教你学习大数据