返回 登录
0

通过实例带你学习RPC框架gRPC

阅读14677

声明: CSDN原创投稿,未经许可,禁止任何形式的转载。
作者:赵昌峻,码农、全栈工程师,长期关注云计算、大数据、移动互联网相关技术,从事IT行业十载,有5年的企业级应用开发经历、3年的云平台(openstack)研发经历,目前从事移动APP的研发,坚持每天快乐的编程。

什么是gRPC?

在知乎上有这样一个问题:谁能用通俗的语言解释一下什么是 RPC 框架?,各路大神讲的都很到位,这里我就不详细解释了,gRPC就是其中的一种RPC框架。

gRPC.png

如上图所示,在gRPC中,客户端应用程序可以就像调用本地对象方法一样直接调用不同服务器上的应用程序方法,使您更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,定义可以远程调用的方法,包括方法的参数和返回类型。在服务器端,服务器实现此接口并运行一个gRPC服务器来处理客户端调用。在客户端,客户端有一个“存根stub”(简称为某些语言的客户端),提供与服务器相同的方法。所有的数据传输都使用protobuf。

gRPC服务的定义

我在Protocol buffers(protobuf)入门简介及性能分析中所用到的protobuf数据结构里增加如下内容:

syntax = "proto3";

option go_package = "user";
option java_package = "com.ylifegroup.protobuf";

enum PhoneType {
    HOME = 0;
    WORK = 1;
    OTHER = 2;
}

message ProtobufUser {
  int32 id = 1;
  string name = 2;
  message Phone{
  PhoneType phoneType = 1;
  string phoneNumber = 2;
  }
  repeated Phone phones = 3;
}

message AddPhoneToUserRequest{
  int32 uid = 1;
  PhoneType phoneType = 2;
  string phoneNumber = 3;
}

message AddPhoneToUserResponse{
  bool result = 1;
}

service PhoneService {
  rpc addPhoneToUser(AddPhoneToUserRequest) returns (AddPhoneToUserResponse);
}

增加的内容很简单,定义了一个关于电话本的服务PhoneBookService,服务包括一个把电话增加到指定联系人的方法addPhoneToUser,同时定义了方法的参数AddPhoneToUserRequest和返回值AddPhoneToUserResponse。

gRPC服务的服务器端实现

为了方便我们后面讲解微服务架构,从本节开始我们使用Java作为我们的开发语言。

我们的Demo将使用Gradle自动化构建工具,这里不会深入讲解Gradle的用法,建议没有使用过Gradle的可以从官方网站开始:https://gradle.org

可以使用下面的命令生成Java项目的基本结构:

mkdir gRPCDemo
cd gRPCDemo
gradle init --type=java-library

初始化后,目录结构如下所示:

└── gRPCDemo
    ├── build.gradle
    ├── gradle
    │   └── wrapper
    │       ├── gradle-wrapper.jar
    │       └── gradle-wrapper.properties
    ├── gradlew
    ├── gradlew.bat
    ├── settings.gradle
    └── src
        ├── main
        │   └── java
        │       └── Library.java
        └── test
            └── java
                └── LibraryTest.java

先删除没用的Library.javaLibraryTest.java

然后修改配置文件build.gradle,如下所示:

/*
 * This build file was auto generated by running the Gradle 'init' task
 * by 'ChangjunZhao' at '16-12-27 下午3:25' with Gradle 3.1
 *
 * This generated file contains a sample Java project to get you started.
 * For more details take a look at the Java Quickstart chapter in the Gradle
 * user guide available at https://docs.gradle.org/3.1/userguide/tutorial_java_projects.html
 */

// Apply the java plugin to add support for Java
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'com.google.protobuf'

repositories {
    // Use 'jcenter' for resolving your dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.0'
    }
}

sourceSets {
    main {
        java{
            srcDir 'gen/main/java'
            srcDir 'gen/main/grpc'
        }
        proto {
            srcDir 'src/main/proto'
        }
    }
}

jar {
    from {
        configurations.runtime.collect {
            it.isDirectory() ? it : zipTree(it)
        }
        configurations.compile.collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
    manifest {
        attributes 'Main-Class': 'com.ylifegroup.protobuf.server.GRpcServer'
    }
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.1.0"
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.0.3'
        }
    }
    generatedFilesBaseDir = "$projectDir/gen/"
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

dependencies {
    compile 'io.grpc:grpc-netty:1.0.3'
    compile 'io.grpc:grpc-protobuf:1.0.3'
    compile 'io.grpc:grpc-stub:1.0.3'
    // The production code uses the SLF4J logging API at compile time
    compile 'org.slf4j:slf4j-api:1.7.21'

    // Declare the dependency for your favourite test framework you want to use in your tests.
    // TestNG is also supported by the Gradle Test task. Just change the
    // testCompile dependency to testCompile 'org.testng:testng:6.8.1' and add
    // 'test.useTestNG()' to your build script.
    testCompile 'junit:junit:4.12'
}

eclipse {
  classpath {
    defaultOutputDir = file('build/eclipse/bin')
  }
}

clean {
    delete protobuf.generatedFilesBaseDir
}

我们使用了grpc官方提供的gradle插件com.google.protobuf,它可是相当牛B,可以帮我们自动生成grpc相关的代码。

为了完成服务器端的开发,增加了grpc相关的三个依赖:

io.grpc:grpc-netty:1.0.3
io.grpc:grpc-protobuf:1.0.3
io.grpc:grpc-stub:1.0.3

在protobuf的配置部分,我们使用grpc代码生成的插件,以便自动帮我们生成grpc服务器端的代码(接口),具体的实现还需要我们自己去搞定。

在源文件配置部分sourceSets,我们指定了proto文件的目录src/main/proto,同时把protobuf自动生成代码所在两个目录(默认)gen/main/java,gen/main/grpc增加到Java的源文件目录,这样在编译的时候才会编译这两个目录下的Java类。

OK,基本配置就这些,把上一节的gRPC服务定义文件phonebook.proto放到我们项目的src/main/proto目录(如果不存在自己手动创建)下,执行如下命令:

./gradlew build

gradle会帮你自动生了protobuf和grpc相关的文件到如下目录:

├── gen
│   └── main
│       ├── grpc
│       │   └── com
│       │       └── ylifegroup
│       │           └── protobuf
│       │               └── PhoneServiceGrpc.java
│       └── java
│           └── com
│               └── ylifegroup
│                   └── protobuf
│                       └── Phonebook.java

OK,剩下的工作大部分需要手动完成了,执行如下命令,生成Eclipse相关的配置文件:

./gradlew eclipse

然后用Eclipse打开gRPCDemo项目。

我们新建三个package:

com.ylifegroup.protobuf.service //用于放gRPC服务的实现类
com.ylifegroup.protobuf.server //用于放gRPC服务器的实现类
com.ylifegroup.protobuf.client //用于放gRPC客户端demo的相关类。

首先我们需要写代码来实现将电话增加到用户的逻辑,我们在com.ylifegroup.protobuf.service包下新建一个类叫PhoneServiceImp,它只需要继承protoc-gen-grpc插件帮我们自己生成的一个grpc实现类PhoneServiceGrpc.PhoneServiceImplBase,并实现相关方法即可,所有代码如下所示:

package com.ylifegroup.protobuf.service;

import com.ylifegroup.protobuf.PhoneServiceGrpc;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserRequest;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserResponse;

import io.grpc.stub.StreamObserver;

public class PhoneServiceImp extends PhoneServiceGrpc.PhoneServiceImplBase{

    @Override
    public void addPhoneToUser(AddPhoneToUserRequest request, StreamObserver<AddPhoneToUserResponse> responseObserver) {
        // TODO Auto-generated method stub
        AddPhoneToUserResponse response = null;
        if(request.getPhoneNumber().length() == 11 ){
            System.out.printf("uid = %s , phone type is %s, nubmer is %s\n", request.getUid(), request.getPhoneType(), request.getPhoneNumber());
            response = AddPhoneToUserResponse.newBuilder().setResult(true).build();
        }else{
            System.out.printf("The phone nubmer %s is wrong!\n",request.getPhoneNumber());
            response = AddPhoneToUserResponse.newBuilder().setResult(false).build();
        }
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

}

代码很简单,我们只是检查手机号是不是11位,如果是把客户端的请求参数打印出来,给客户端返回true,如果不是11位,提示手机号错误,给客户端返回false。

代码很简单,这里就不详细解释了。

接下来,我们需要将我们实现的服务发布成grpc服务,这里有很多实现方法,我们就使用grpc官方提供的用netty实现服务器代码,还记得在gradle的配置文件里我们增加了io.grpc:grpc-netty:1.0.3依赖吗?就是用在这的。

在com.ylifegroup.protobuf.server包下新建GRpcServer类,代码如下所示(大部分代码参考grpc官方的helloworld):

package com.ylifegroup.protobuf.server;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.logging.Logger;
import com.ylifegroup.protobuf.service.PhoneServiceImp;

public class GRpcServer {
    private static final Logger logger = Logger.getLogger(GRpcServer.class.getName());

    private Server server;

    private void start() throws IOException {
        /* The port on which the server should run */
        int port = 50051;
        server = ServerBuilder
                .forPort(port)
                .addService(new PhoneServiceImp())
                .build()
                .start();
        logger.info("Server started, listening on " + port);
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.err.println("*** shutting down gRPC server since JVM is shutting down");
                GRpcServer.this.stop();
                System.err.println("*** server shut down");
            }
        });
    }

    private void stop() {
        if (server != null) {
            server.shutdown();
        }
    }

    /**
     * Await termination on the main thread since the grpc library uses daemon
     * threads.
     */
    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }

    /**
     * Main launches the server from the command line.
     */
    public static void main(String[] args) throws IOException, InterruptedException {
        final GRpcServer server = new GRpcServer();
        server.start();
        server.blockUntilShutdown();
    }
}

详细的我就不解释了,一看代码就能明白,只需要把我们的电话本服务PhoneServiceImp增加到grpc服务器,然后就能对外提供电话本的服务了。

gRPC服务的客户端实现

客户端的实现也很简单,在com.ylifegroup.protobuf.client包下新建GRpcClient类,代码如下:

package com.ylifegroup.protobuf.client;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.StatusRuntimeException;

import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.ylifegroup.protobuf.PhoneServiceGrpc;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserRequest;
import com.ylifegroup.protobuf.Phonebook.AddPhoneToUserResponse;
import com.ylifegroup.protobuf.Phonebook.PhoneType;

public class GRpcClient {

    private static final Logger logger = Logger.getLogger(GRpcClient.class.getName());

    private final ManagedChannel channel;

    private final PhoneServiceGrpc.PhoneServiceBlockingStub blockingStub;

    /** Construct client connecting to gRPC server at {@code host:port}. */
    public GRpcClient(String host, int port) {
        ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress(host, port).usePlaintext(true);
        channel = channelBuilder.build();
        blockingStub = PhoneServiceGrpc.newBlockingStub(channel);
    }

    public void shutdown() throws InterruptedException {
        channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
    }

    /** add phone to user. */
    public void addPhoneToUser(int uid, PhoneType phoneType, String phoneNubmer) {
        logger.info("Will try to add phone to user " + uid);
        AddPhoneToUserRequest request = AddPhoneToUserRequest.newBuilder().setUid(uid).setPhoneType(phoneType)
                .setPhoneNumber(phoneNubmer).build();
        AddPhoneToUserResponse response;
        try {
            response = blockingStub.addPhoneToUser(request);
        } catch (StatusRuntimeException e) {
            logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
            return;
        }
        logger.info("Result: " + response.getResult());
    }

    public static void main(String[] args) throws Exception {
        GRpcClient client = new GRpcClient("localhost", 50051);
        try {
            client.addPhoneToUser(1, PhoneType.WORK, "13888888888");
        } finally {
            client.shutdown();
        }
    }
}

代码也很简单,自己看吧,总结下来调用gRPC服务包括以下几步:

  • 指定服务器端的ip和端口,构建一个ManagedChannel。
  • 构造gRPC请求相关参数。
  • 创建blockingStub(回到开始去看看那张图,就更好理解stub了)来调用相关的远程方法。
  • 接收服务器端返回结果。

这其中大部分的代码protoc-gen-grpc都已经帮您生成,使用起来很方便。

让它跑起来

我们在项目根目录执行如下命令:

./gradlew jar

会在build/libs/目录下生成一个可直接运行的jar包:gRPCDemo.jar。

我们执行如下命令即可启动我们的gRPC服务:

java -jar build/libs/gRPCDemo.jar

如下图所示:

gRPC_server.png

我们在eclipse中运行GRpcClient类,您将看到:

phone_service_true.png

看到了吗?服务器端返回”true”。

把代码中的8去掉一个,再次执行,如下图所示:

phone_service_false.png

服务器端返回了false,说明我们的服务还是靠谱的,哈哈。

这只是我们的第一个grpc版本,以方便你更好的理解grpc,后面我将结合spring boot讲解微服务架构,那就更牛B了。

OK,今天就到这了,本文所涉及的全部代码见:github:https://github.com/ChangjunZhao/gRPCDemo

评论