grpc实战——客户端流式调用

作者: 程序员Sunny | 来源:发表于2018-06-16 21:32 被阅读177次
    本文地址:https://www.jianshu.com/p/53cd8f0fe6ce
    传送门:

    Sunny在之前和大家聊了grpc如何实现一个简单服务,应用在一个名称解析服务中;后来又写了一个服务端流式调用的文章,这两者总体来说难度不是很大,也都是同步调用。现在我们要说的是如何进行客户端流式调用,这里我们用异步调用,异步实现起来会较为麻烦一些。下面跟着Sunny来看看如何实现吧。

    这次我们的实例背景是做一个加法的服务,服务端接收客户端传过来的一系列的数字(int型),然后进行所有数字的和、数字数量和平均值的统计,将最终统计结果返回给调用者。

    实际实现的服务较为简单,因为我们更注重grpc调用过程本身而非具体实现的服务的复杂度。当然童鞋们如果希望Sunny实现什么比较有意思的调用,也可以联系我。本篇我们更关注代码本身,具体的操作内容部分在前两篇中已经说明的非常详细了。


    定义服务

    首先我直接放上来我定义的proto文件:

    syntax = "proto3";
    
    //生成多文件,可以将类都生成在一个文件中
    option java_multiple_files = true;
    //包名
    option java_package = "io.grpc.examples.addition";
    //生成的类名
    option java_outer_classname = "AddtitionProto";
    //object-c类名前缀避免类名冲突
    //option objc_class_prefix = "NS";
    
    package addition;
    
    // 定义服务
    service AdditionService {
        // 服务中的方法,传过来一个Value类型的流,返回一个Result类型
        rpc getResult (stream Value) returns (Result) {}
    }
    //定义Value消息类型,用于客户端消息
    message Value {
        int32 value = 1;
    }
    //定义Result消息类型,包含总和,数字数量和平均值,用于服务端消息返回
    message Result {
        int32 sum = 1;
        int32 cnt = 2;
        double avg = 3;
    }
    
    

    定义非常简单的两个消息类型Value和Result,Value中包含一个32位整型的value值;在Result中则包括了三个值,第一个用于记录传过来的数字的总和,第二个用于记录传过来数字的数量,第三个则记录的是平均值,前两个为32位整型,第三个是一个double类型。然后我们再看AdditionService服务,该服务里面包含一个getResult方法,用于接收客户端传过来的Value消息,并返回一个Result消息。另外,前两篇没有具体说明,这里服务之前的代码的内容,在这里Sunny特别增加了注释。第一行说明我们用的是protocol buffer3而非其他版本,比如说2。后面几行可以看作是配置项,java_multiple_files用于配置是否将生成的类放在多个文件里,默认为false;java_package则用于指定生成的包名,我们可以取一个更符合java习惯的包名;java_outer_classname则是生成的类型;objc_class_prefix是object-c中指定类名前缀避免冲突的,这个对java是无效的。想更多了解的童鞋可以去看一下文档或者其他教程。

    生成代码

    生成代码还是和前两篇的方法类似,为了简便,我们还是利用idea整合protobuf的maven插件来进行编译。编译后得到的代码结构如下图所示:


    图片.png

    构建项目依赖

    这次我们的项目整体结构如图所示:


    图片.png

    在一个总体项目grpcclientstream中包含了grpcclientcstream和grpcservercstream两个模块,分别为客户端和服务端。Sunny每次喜欢换一点做法,所以这次我们不直接在两个模块的pom.xml文件中分别添加依赖了,很多公用依赖我们直接添加在项目pom.xml中。

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.sunny</groupId>
        <artifactId>grpc-client-stream</artifactId>
        <packaging>pom</packaging>
        <version>1.0-SNAPSHOT</version>
        <modules>
            <module>grpcservercstream</module>
            <module>grpcclientcstream</module>
        </modules>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <java.version>1.8</java.version>
            <grpc.version>1.12.0</grpc.version>
            <protoc.version>3.5.1-1</protoc.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-all</artifactId>
                <version>${grpc.version}</version>
            </dependency>
        </dependencies>
    
        <build>
            <extensions>
                <extension>
                    <groupId>kr.motd.maven</groupId>
                    <artifactId>os-maven-plugin</artifactId>
                    <version>1.5.0.Final</version>
                </extension>
            </extensions>
            <plugins>
                <plugin>
                    <groupId>org.xolstice.maven.plugins</groupId>
                    <artifactId>protobuf-maven-plugin</artifactId>
                    <version>0.5.1</version>
                    <configuration>
                        <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
                        <pluginId>grpc-java</pluginId>
                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>compile</goal>
                                <goal>compile-custom</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

    大家可以看到,我们主要就是添加了grpc-all的依赖,另外就是protobuf的插件了,和之前两篇大同小异。所以现在两个模块中的pom.xml就很“干净”了,直接继承了整个项目的pom.xml。
    其中grpcclientcstream的pom.xml为:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>grpc-client-stream</artifactId>
            <groupId>com.sunny</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>grpc-client-cstream</artifactId>
    
    </project>
    

    grpcservercstream的pom.xml为:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>grpc-client-stream</artifactId>
            <groupId>com.sunny</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>grpc-server-cstream</artifactId>
    </project>
    

    构建服务端

    为什么说客户端流式调用较为复杂,复杂在哪里呢?可以说从服务端到客户端都和之前有较大的不同。当然,服务端还是有两个组成部分,一个负责接收客户端的请求,另外一个用于实现实际完成的服务,前者基本上差不多,后者和之前的做法完全不同。首先,我们还是贴出来接收客户端请求部分的代码:

    public class AdditionServer {
    
        private Logger logger = Logger.getLogger(AdditionServer.class.getName());
    
        private static final int DEFAULT_PORT = 8088;
    
        private int port;//服务端口号
    
        private Server server;
    
    
        public AdditionServer(int port) {
            this(port, ServerBuilder.forPort(port));
        }
    
        public AdditionServer(int port, ServerBuilder<?> serverBuilder){
    
            this.port = port;
    
            //构造服务器,添加我们实际的服务
            server = serverBuilder.addService(new AdditionServiceImplBaseImpl()).build();
    
        }
    
        private void start() throws IOException {
            server.start();
            logger.info("Server has started, listening on " + port);
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
    
                    AdditionServer.this.stop();
    
                }
            });
        }
    
        private void stop() {
    
            if(server != null)
                server.shutdown();
    
        }
    
        //阻塞到应用停止
        private void blockUntilShutdown() throws InterruptedException {
            if (server != null) {
                server.awaitTermination();
            }
        }
    
        public static void main(String[] args) {
    
            AdditionServer addtionServer;
    
            if(args.length > 0){
                addtionServer = new AdditionServer(Integer.parseInt(args[0]));
            }else{
                addtionServer = new AdditionServer(DEFAULT_PORT);
            }
    
            try {
                addtionServer.start();
                addtionServer.blockUntilShutdown();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    

    这部分大家已经较为熟悉了,Sunny就不再多说,我们主要来看看服务端在客户端流式调用中是如何进行实际的服务的。

    public class AdditionServiceImplBaseImpl extends AdditionServiceGrpc.AdditionServiceImplBase {
    
        private Logger logger = Logger.getLogger(AdditionServiceImplBaseImpl.class.getName());
    
        @Override
        public StreamObserver<Value> getResult(final StreamObserver<Result> responseObserver) {
            //如果处理过程较为复杂,可以考虑单独写一个类来实现。这里用匿名内部类的方式,在内部类中用到参数需要加final修饰符
            return new StreamObserver<Value>() {
    
                private int sum = 0;
                private int cnt = 0;
                private double avg;
    
                public void onNext(Value value) {
                    sum += value.getValue();
                    cnt++;
                }
    
                public void onError(Throwable throwable) {
                    logger.log(Level.SEVERE,throwable.getMessage());
                }
                public void onCompleted() {
                    avg = 1.0*sum/cnt;
                    responseObserver.onNext(Result.newBuilder().setSum(sum).setCnt(cnt).setAvg(avg).build());
                    responseObserver.onCompleted();
                }
            };
        }
    }
    

    可以看到代码不长,但是如果之前没有接触过的童鞋肯定有些困惑。这个方法的返回值是Value的StreamObserver,而参数是Result的StreamObserver,这样看起来真的很奇怪。现在先放下那些疑惑,听Sunny讲下去,待会Sunny讲到客户端实现的部分,大家就会恍然大悟了。我们先看看这个方法里面究竟做了什么,仔细一看,实际上这个方法只做了一件事,就是返回了一个StreamObserver<Value>类型的对象,但是实际上这个是一个接口,所以这里用了匿名内部类。好了,这个匿名内部类里面的内容才是我们真正关心的。先看看它的成员变量,sum、cnt和avg,这就是我们定义的Result的三个组成部分。在这个类中有三个方法,事实上也就是实现了的StreamObserver接口的所有三个方法。我们继续看,在onNext方法中,我们对用sum来对其参数Value类型的值进行累加,同时用cnt来计算有多少个Value。onError是在出错的时候才会调用的,我们这里较为简单,打印日志即可。onComplete是指示调用完成的,这里我们做了三件事,第一个计算了平均值,注意一下:

    avg = 1.0*sum/cnt;
    

    这里的1.0不能省略,后面是两个整型相除,会默认为整型的,前面乘以1.0是先将分母转化为浮点型。这里提一下题外话,Sunny本科时候第一次参加acm省赛的时候,因为这个坑调试了很久,大家平时码代码的时候要注意这些细节。
    第二件事就是用responseObserver这个变量的onNext方法来返回一个Result类型,第三件事则是调用responseObserver的onComplete方法来指示服务端应答结束。
    我们来总结一下,服务端这里利用传过来的responseObserver参数来进行服务调用结果的返回。然后利用返回类型StreamObserver<Value>的一个匿名内部类来进行接收客户端参数、处理错误和客户端在complete之后的操作——实际上,从代码里可以看出,客户端完成调用发送之后,服务端再进行服务调用结果的返回。

    构建客户端

    客户端部分和之前也有较大不同,我们这次写了两个类,第一个用于发起具体的请求的类AdditionClient,第二个则用来进行处理回调的类CallBack。
    我们先来看看AdditionClient类:

    public class AdditionClient {
    
        private static final String DEFAULT_HOST = "localhost";
    
        private static final int DEFAULT_PORT = 8088;
    
        private static final int VALUE_NUM = 10;
    
        private static final int VALUE_UPPER_BOUND = 10;
    
        private Logger logger = Logger.getLogger(AdditionClient.class.getName());
    
        private ManagedChannel managedChannel;
    
        //服务存根,用于客户端本地调用
        //private AdditionServiceGrpc.AdditionServiceBlockingStub additionServiceBlockingStub;
    
        //这里用异步请求存根
        private AdditionServiceGrpc.AdditionServiceStub additionServiceStub;
    
        public AdditionClient(String host, int port) {
    
            this(ManagedChannelBuilder.forAddress(host,port).usePlaintext(true).build());
    
        }
    
        public AdditionClient(ManagedChannel managedChannel) {
            this.managedChannel = managedChannel;
            this.additionServiceStub = AdditionServiceGrpc.newStub(managedChannel);
        }
    
    //    public void shutdown() throws InterruptedException {
    //        managedChannel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    //    }
    
        public void getResult(final CallBack callBack, List<Integer> nums){
    
            //判断调用状态。在内部类中被访问,需要加final修饰
            final CountDownLatch countDownLatch = new CountDownLatch(1);
    
            StreamObserver<Result> responseObserver = new StreamObserver<Result>() {
                public void onNext(Result result) {
                    //静态方法回调
                    //CallBack.callBackStatic(result);
    
                    //实例方法回调
                    callBack.setResult(result);
                    callBack.callBackInstance();
                }
    
                public void onError(Throwable throwable) {
                    logger.log(Level.SEVERE,throwable.getMessage());
                    countDownLatch.countDown();
                }
    
                public void onCompleted() {
                    logger.log(Level.INFO,"completed");
                    countDownLatch.countDown();
                }
    
            };
    
            StreamObserver<Value> requestObserver = additionServiceStub.getResult(responseObserver);
    
            for(int i=0;i<nums.size();i++){
                Value value = Value.newBuilder().setValue(nums.get(i)).build();
                requestObserver.onNext(value);
    
                //判断调用结束状态。如果整个调用已经结束,继续发送数据不会报错,但是会被舍弃
                if(countDownLatch.getCount() == 0){
                    return;
                }
            }
            //异步请求,无法确保onNext与onComplete的完成先后顺序
            requestObserver.onCompleted();
    
            try {
                //如果在规定时间内没有请求完,则让程序停止
                if(!countDownLatch.await(5,TimeUnit.MINUTES)){
                    logger.log(Level.WARNING,"not complete in time");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
    
        }
    
        public static void main(String[] args) {
    
            AdditionClient additionClient = new AdditionClient(DEFAULT_HOST,DEFAULT_PORT);
    
            //用于实例方法回调
            CallBack callBack = new CallBack();
    
            //生成value值
            List<Integer> list = new ArrayList<Integer>();
            Random random = new Random();
    
            for(int i=0; i<VALUE_NUM; i++){
                //随机数符合 0-VALUE_UPPER_BOUND 均匀分布
                int value = random.nextInt(VALUE_UPPER_BOUND);
    
                System.out.println(i + ":" + value);
    
                list.add(value);
            }
    
            System.out.println("*************************getting result from server***************************");
            System.out.println();
    
            additionClient.getResult(callBack, list);
    
        }
    
    }
    

    可以看到这个类中有很多的成员,也包括了不少常量,这些都不是重点,只是后面方法中会用到,其中比较值得一提的就是stub了,之前的简单调用和服务器端流式调用,我们都用的是阻塞存根ServiceBlockingStub,而这里因为需要发送异步请求,我们用的是ServiceStub。
    在这个类的方法中,有两个构造方法,基本上就是给成员变量赋值的;main方法是程序入口,我们在里面生成了一个ilst,里面的Integer类型的变量作为服务调用的参数。我们最最需要关心的是getResult方法,这才是真正执行服务调用请求和接收服务调用结果的方法。我们把它单独拿出来再仔细看看:

    public void getResult(final CallBack callBack, List<Integer> nums){
    
            //判断调用状态。在内部类中被访问,需要加final修饰
            final CountDownLatch countDownLatch = new CountDownLatch(1);
    
            StreamObserver<Result> responseObserver = new StreamObserver<Result>() {
                public void onNext(Result result) {
                    //静态方法回调
                    //CallBack.callBackStatic(result);
    
                    //实例方法回调
                    callBack.setResult(result);
                    callBack.callBackInstance();
                }
    
                public void onError(Throwable throwable) {
                    logger.log(Level.SEVERE,throwable.getMessage());
                    countDownLatch.countDown();
                }
    
                public void onCompleted() {
                    logger.log(Level.INFO,"completed");
                    countDownLatch.countDown();
                }
    
            };
    
            StreamObserver<Value> requestObserver = additionServiceStub.getResult(responseObserver);
    
            for(int i=0;i<nums.size();i++){
                Value value = Value.newBuilder().setValue(nums.get(i)).build();
                requestObserver.onNext(value);
    
                //判断调用结束状态。如果整个调用已经结束,继续发送数据不会报错,但是会被舍弃
                if(countDownLatch.getCount() == 0){
                    return;
                }
            }
            //异步请求,无法确保onNext与onComplete的完成先后顺序
            requestObserver.onCompleted();
    
            try {
                //如果在规定时间内没有请求完,则让程序停止
                if(!countDownLatch.await(5,TimeUnit.MINUTES)){
                    logger.log(Level.WARNING,"not complete in time");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    

    这里,我们可以看到有两个StreamObserver对象,根据我给定的变量名responseObserver和requestObserver,大家其实应该就能猜到哪个是负责干嘛的。requestObserver用于进行请求的发送,这里会用它的onNext方法发一串value给服务端,在发送完毕后,执行onComplete方法。

            StreamObserver<Value> requestObserver = additionServiceStub.getResult(responseObserver);
    
            for(int i=0;i<nums.size();i++){
                Value value = Value.newBuilder().setValue(nums.get(i)).build();
                requestObserver.onNext(value);
    
                //判断调用结束状态。如果整个调用已经结束,继续发送数据不会报错,但是会被舍弃
                if(countDownLatch.getCount() == 0){
                    return;
                }
            }
            //异步请求,无法确保onNext与onComplete的完成先后顺序
            requestObserver.onCompleted();
    

    这时候我们再回忆服务端的部分,里面我们返回值是一个StreamObserver,实际上就是这里的requestObserver,所以我们构造的匿名内部类的onNext方法是在这里被调用的,这里是传入了一个Value类型的参数;我们看一下服务端的onNext方法:

            public void onNext(Value value) {
                  sum += value.getValue();
                  cnt++;
            }
    

    所以在客户端的request每调用一次onNext方法,服务端都会接收到一个Value值,同样的onComplete也一样。
    然后我们来看客户端构造的responseObserver方法,里面定义了onNext、onError和onComplete方法:

            StreamObserver<Result> responseObserver = new StreamObserver<Result>() {
                public void onNext(Result result) {
                    //静态方法回调
                    //CallBack.callBackStatic(result);
    
                    //实例方法回调
                    callBack.setResult(result);
                    callBack.callBackInstance();
                }
    
                public void onError(Throwable throwable) {
                    logger.log(Level.SEVERE,throwable.getMessage());
                    countDownLatch.countDown();
                }
    
                public void onCompleted() {
                    logger.log(Level.INFO,"completed");
                    countDownLatch.countDown();
                }
    
            };
    

    实际上对应了服务端的参数responseObserver,因此这里的方法都是在服务端被调用的,我们看到服务端实际是在request的onComplete方法中调用了responseObserver的onNext方法返回调用结果,以及调用responseObserver的onComplete方法指示调用结束。
    其实整个调用过程,我们关心调用的方法:

    StreamObserver<Value> requestObserver = additionServiceStub.getResult(responseObserver);
    

    在这里responseObserver是在客户端定义的,而requestObserver是在服务端返回的,两者互相调用对方定义的方法。理解了这个,可以说你已经基本理解了调用过程。
    调用过程理解之后,我们再来说说回调,因为是异步请求,所以需要有一个回调函数。这里就有几种方式了:

    • 第一种:直接使用结果
      我们不用回调直接在responseObserver定义的onNext方法中使用获得到的服务端的返回结果,最最简单的就是打印了。
    • 第二种:静态方法回调
      我们在CallBack类中加入一个静态方法,用于调用结果的处理,我们这里也是选择直接打印结果,事实上在项目中可能会进行数据库的数据交互。
    • 第三种:实例方法回调
      我们在CallBack类中加入一个非静态方法,用于处理调用结果,但是要求我们在方法中传入相应的实例才可以。
      我们先看静态回调:
        public static void callBackStatic(Result result){
            System.out.println("get result from server...sum:" +
                    result.getSum() +
                    " count:" +
                    result.getCnt() +
                    " avg:" + result.getAvg());
        }
    

    传入一个Result,打印里面的变量。
    再看看实例方法回调:

    public void callBackInstance(){
            System.out.println("get result from server...sum:" +
                    result.getSum() +
                    " count:" +
                    result.getCnt() +
                    " avg:" + result.getAvg());
        }
    

    这里我们不用再传入Result了,而是将Result作为CallBack类的一个成员变量,在调用callBackInstance方法前,先调用setResult为成员变量赋值。查看responseObserver的定义的onNext方法,我们可以看到,我这里使用的是实例方法回调,静态方法回调已经被我注释掉。另外附上CallBack类:

    public class CallBack {
    
        private Result result;
    
        public Result getResult() {
            return result;
        }
    
        public void setResult(Result result) {
            this.result = result;
        }
    
        public static void callBackStatic(Result result){
            System.out.println("get result from server...sum:" +
                    result.getSum() +
                    " count:" +
                    result.getCnt() +
                    " avg:" + result.getAvg());
        }
    
        public void callBackInstance(){
            System.out.println("get result from server...sum:" +
                    result.getSum() +
                    " count:" +
                    result.getCnt() +
                    " avg:" + result.getAvg());
        }
    
    }
    

    运行测试

    首先启动服务端AdditionServer,得到提示:

    Jun 16, 2018 4:29:20 PM com.sunny.AdditionServer start
    信息: Server has started, listening on 8088a
    

    启动成功后,我们运行AdditionClient:

    0:9
    1:6
    2:2
    3:0
    4:1
    5:6
    6:9
    7:6
    8:8
    9:0
    *************************getting result from server***************************
    
    get result from server...sum:47 count:10 avg:4.7
    Jun 16, 2018 4:30:15 PM com.sunny.AdditionClient$1 onCompleted
    信息: completed
    
    Process finished with exit code 0
    

    在这里,我们首先打印了所有10个Value的值,这里生存的随机数符合[0,10]均匀分布。然后下面是从服务端得到的统计结果,这是由回调函数打印的。再下面就是responseObserver的onComplete中打印的提示信息。


    至此,本篇文章到此结束,喜欢的童鞋可以点个赞。
    欢迎转载,转载时请注明原文地址:https://www.jianshu.com/p/53cd8f0fe6ce
    童鞋们如果有疑问或者想和我交流的话有两种方式:

    第一种

    评论留言

    第二种

    邮箱联系:zsunny@yeah.net

    相关文章

      网友评论

        本文标题:grpc实战——客户端流式调用

        本文链接:https://www.haomeiwen.com/subject/kdbpeftx.html