【夜读源码】grpc-java 概览

·

3 min read

【夜读源码】grpc-java 概览

Photo by Callum Shaw on Unsplash

gRPC 是 Google 开源的一套语言无关基于HTTP2协议的 RPC 框架,其中 Java 的实现称为 grpc-java,代码地址在 github.com/grpc/grpc-java

RPC (remote process call)的含义是程序执行本地的一个方法(看上去像本地执行),实际上会在远程程序代码内执行,这个过程会涉及到网络通讯和传输协议。gRPC作为 RPC 一种实现,传输的网络协议是HTTP2,传输的内容按照 Protocol Buffer 进行编码,并且支持各种语言的实现。

image.png

概述

grpc-java 是一个 RPC 库和框架,运行在 JDK 8 上,项目包括两部分:代码生成和运行库。 代码生成由C++编写,用于生成一部分Java 代码, 生成的部分,称为 stub,包括 client stub 和 server stub。 运行库由Java 编写,Java 项目可以引用这些运行库。

image.png

在了解 stub 和 运行库的内容之前,运行一个完整的例子,把它运行起来。

gRPC 实例

创建一个 Maven 作为项目管理工具的 Java 项目。

添加依赖

把 grpc 的 运行库 都加进来

<dependencies>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty-shaded</artifactId>
    <version>1.48.1</version>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.48.1</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.48.1</version>
  </dependency>
  <dependency> <!-- necessary for Java 9+ -->
    <groupId>org.apache.tomcat</groupId>
    <artifactId>annotations-api</artifactId>
    <version>6.0.53</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

代码生成插件

为了能 生成代码,也就是生成Stub, 添加 maven 插件

<build>
  <extensions>
    <extension>
      <groupId>kr.motd.maven</groupId>
      <artifactId>os-maven-plugin</artifactId>
      <version>1.6.2</version>
    </extension>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>com.google.protobuf:protoc:3.21.1:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier}</pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

grpc 接口描述

需要写一个描述文件 src/main/proto/hello.proto,按照protocol buffer的语法,定义 方法的名称和参数的数据格式。 代码生成插件,读取 hello.proto,生成 stub

syntax = "proto3";

option java_package = "org.shalk.grpc";

message Info {
  int32 id = 1;
  string name = 2;
  bool ready = 3;
}

service Hello{
  rpc echo(Info) returns (Info);
  rpc clientStream (Info) returns (stream Info);
  rpc serverStream (stream Info) returns (Info);
  rpc biStream (stream Info) returns (stream Info);
}

生成代码

mvn compile

编写服务端代码

服务端需要写两部分代码,server stub implement (服务端 stub 实现)和 server start (服务端启动) 服务端 stub 实现,就是对rpc echo(Info) returns (Info); 的实现。 服务端启动,是启动一个端口,对外提供网络服务,这样客户端就可以通过网络请求把请求数据发过来,服务端在执行完请求后,把执行结果发送回去。

//  服务端 stub 实现
public class HelloImpl extends HelloGrpc.HelloImplBase {
    @Override
    public void echo(HelloOuterClass.Info request, StreamObserver<HelloOuterClass.Info> responseObserver) {
        System.out.printf("request info Id:%d\n", request.getId());
        responseObserver.onNext(request);
        responseObserver.onCompleted();
    }
}

服务端启动

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;

public class SimpleServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(8080).addService(new HelloImpl()).build();
        server.start();
        System.out.println("server started");
        server.awaitTermination();
        System.out.println("server stoped");

    }
}

编写客户端代码

客户端需要些两部分代码,client stub initial (客户端 stub 初始化) 和 client stub call (客户端 stub 调用)


import io.grpc.Channel;
import io.grpc.ManagedChannelBuilder;

public class SimpleClient {
    public static void main(String[] args) {
        // 客户端 stub  初始化
        Channel channel = ManagedChannelBuilder.forAddress("localhost", 8080).usePlaintext().build();
        HelloGrpc.HelloBlockingStub helloBlockingStub = HelloGrpc.newBlockingStub(channel);
        // 客户端 stub 调用
        for (int i = 0; i < 100; i++) {
            HelloOuterClass.Info request = HelloOuterClass.Info.newBuilder().setId(i).setName("hi").setReady(true).build();
            HelloOuterClass.Info response = helloBlockingStub.echo(request);
            System.out.printf("get response A:%d\n", response.getId());
        }
    }

}

运行

服务端先启动,客户端运行可以看到结果

get response A:96
get response A:97
get response A:98
get response A:99

整体架构

整体架构,纵向上,gRPC可以分为 服务端和客户端,横向上,可以划分为传输层,通道层 和 Stub 层三层。第一层,Stub 层 是提供给开发者的接口和数据模型,这个和proto 文件描述的是一致的。第二层,通道层(Channel)是在传输层之上,提供给Stub 层用的。主要作用是做一些拦截器和装饰器,一般通信传输会做一些拦截,可以做 日志,权限和监控等等。第三层,传输层是用于接收和发送数据的层次,它的接口一般不对外暴露。

image.png

一次调用的时序

回顾前面的示例代码,客户端 helloBlockingStub 的一次调用,服务端响应请求。第一步,请求从客户端的Stub层,依次穿过客户端的通道层,传输层,到达网络上。其次,数据通过网络到达服务端的传输层,依次穿过服务端的通道层,最后到达Stub 层 ,也就是服务端的实现。最后,服务端的实现中Stub 响应结果,逆向把数据传输到客户端的Stub 层。

客户端发送请求

  1. stub 执行echo 方法
  2. echo 方法 执行静态方法blockingUnarycall
  3. blockingUsnaryCall中,构造了一个ClientCall,ClientCall 准备了了一个listenablefuture,等Listenablefuture 状态是Done,就把结果从ListenableFuture 中返回。
  4. 准备listenableFuture时,DelayedClientCall 里准备了 四个pendingRunnables 的任务等待执行。 5 等待状态时Done的时候,exector.waitAndDrain 把这四个任务拿到,分别执行一下。
  5. 任务1 realCall.start(finalListener, headers);
  6. 任务2 realCall.request(numMessages);
  7. 任务3 realCall.sendMessage(message);
  8. 任务4 realCall.halfClose();
  9. DelayedClientCall 代理 realCall, realCall 是ClientCallImpl 11 realCall 代理 ClientStream , clientStream 有接口 void start(ClientStreamListener listener); void request(int numMessages); void writeMessage(InputStream message);halfClose() 12 ClientCallImpl 中又把这四个方法,代理给 Stream,Stream的类型是 RetriableStream 13 RetriableStream 又执行过程创建DelayStream,代理给DelayStream 14 DelayStream 又创建四个任务。 (request,writeMessage,flush 和 halfCLose) 15 DelayStream 把任务代理给realStream,这个realStream 是 NettyClientStream

整理来看 Stub 把请求传给了ClientCall, ClientCall 传给了 ClientStream

ClientStream 有Netty 的实现,内部会把数据传输到网络上。

这里面Transport 和 Framer 的逻辑还没明白,第一篇大概就先看到这里。

image.png

服务端接收请求

类似的,服务端也有 ServerCall 和ServerStream, 并且把deramer 解数据的任务扔到线程池。 deframer 把解开的数据,找到listener,执行到 Stub 的实现里,数据送达。

ServerCall => ServerStream => Deframer

服务端回复响应

类似于客户端发请求

客户端接收响应

类似于服务端收请求

Next

一次调用中,客户端和服务端处理请求的过程,可以按照层次和时许,把过程梳理更清晰一点。 下一篇文章,将深入客户端发送请求,把客户端中 ClientCall 的作用,ClientStream的作用,时序线程池,以及Netty 怎么把数据发送出去,以及数据包的格式 细致的展开写一写。