gRPC--从入门到使用

gRPC--其实很简单

Posted by panlei on May 27, 2017

gRPC–其实很简单

简介

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

1.gRPC是什么?

gRPC 是 Google 开源的一款高性能 RPC 框架,前两天发布了 1.0 版本。RPC (Remote Procedure Call) 即远程过程调用,通过 RPC ,客户端的应用程序可以方便地调用另外一台机器上的服务端程序,因而常被应用于分布式系统中。

“在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。”

RPC 框架通常使用 IDL (Interface Description Language) 定义客户端和服务端进行通信的数据结构,服务端提供的服务等,然后编译生成相应的代码供客户端和服务端使用。RPC 框架一般都具备跨语言的特性,这样客户端和服务端可以分别基于不同的语言进行实现。

Alt text

2.开发者:do it

本文以一个简单实例介绍gRPC的使用流程,客户端和服务端均使用java语言,实例很简单,就是客户端输入要查询的歌手编号,服务端返回编号对应的歌手名和歌曲。 开发者需要做的事情:

  1. 需要使用protobuf定义接口,即.proto文件

  2. 然后使用compile工具生成特定语言的执行代码,比如JAVA、C/C++、Python等。类似于thrift,为了解决跨语言问题。

  3. 启动一个Server端,server端通过侦听指定的port,来等待Client链接请求,通常使用Netty来构建,GRPC内置了Netty的支持。

  4. 启动一个或者多个Client端,Client也是基于Netty,Client通过与Server建立TCP长链接,并发送请求;Request与Response均被封装成HTTP2的stream Frame,通过Netty Channel进行交互。

Alt text

2.1 定义服务接口(写*.proto文件)

gRPC 使用 Protocol Buffers 作为 IDL 和底层的序列化工具。 Protocol Buffers 也是非常有名的开源项目,主要用于结构化数据的序列化和反序列化。

在 .proto 文件中定义通信的数据结构和服务接口。关于 Protocol Buffers 的 IDL 的具体细节参考 Language Guide (proto3) 。本例子中定义的服务接口如下:

syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.jr.JRService";
option java_outer_classname = "JRProto";
package JRService;

//service definition
service JRService {
  rpc ListSongs (SingerId) returns (SongList) {}
  //using stream
  rpc GetSongs (SingerId) returns (stream Song) {}
}

message SingerId {
  int32 id = 1;
}

message Singer {
  int32 id = 1;
  string name = 2;
}

message Song {
  int32 id = 1;
  string name = 2;
  Singer singer = 3;
}

message SongList {
  repeated Song songs= 1;
}

注意: 在 Protocol Buffers 服务接口的方法定义中是不能使用基本类型的,方法参数和返回值都必须是自定义的 message 类型。

2.2 生成gRPC代码

使用protoc工具生成

在定义了接口描述文件后,就可以使用 Protocol Buffers 编译器生成相应编程语言的代码了。

下载 Protoc Buffer 后,编译定义的 proto 文件生成相应的代码。以 Java 为例:

$ protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/jr.proto

查看生成的代码可以发现,Protoco Buffers 为每一个 message 都生成了相应的接口和类,可供客户端和服务端代码直接使用。

目前还只是生成了消息对象和序列化及反序列相关的代码。为了使用 gRPC 构建 RPC 服务,还要使用 protoc-gen-grpc-java 插件来生成通信部分的代码。protoco-gen-grpc-java插件可以 自行编译 ,或者从 这里 下载。使用 protoc-gen-grpc-java 插件生成通信服务相关的接口类及接口。

$protoc --plugin=protoc-gen-grpc-java=/path/to/protoc-gen-grpc-java --grpc-java_out=$DST_DIR --proto_path=$SRC_DIR $SRC_DIR/jr.proto

运行上述命令后会生成 JRServiceGrpc.java ,后面 RPC 的服务端和客户端就依赖该类进行构建。

使用IDEA工具生成

本文使用maven构建项目,就简单的多了。创建maven项目自行百度,给个链接吧. 在pom.xml中添加依赖关系和构建插件等

<dependencies>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-netty</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-protobuf</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>io.grpc</groupId>
        <artifactId>grpc-stub</artifactId>
        <version>1.3.0</version>
    </dependency>
</dependencies>

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.4.1.Final</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.5.0</version>
            <configuration>
                <!--
                  The version of protoc must match protobuf-java. If you don't depend on
                  protobuf-java directly, you will be transitively depending on the
                  protobuf-java version that grpc depends on.
                -->
                <protocArtifact>com.google.protobuf:protoc:3.0.0-beta-2:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.3.0:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

在src/main/下面创建proto文件夹,将2.1中编写的*.proto文件复制到该目录下。如:/src/main/proto/jr.proto。 在idea中打开该文件,智能的idea会弹出提示让你加载protobuf插件。如果没有提示,请自行添加:在settings>plugins>中搜索proto。 ok,这时候点击右侧maven窗口中的compile按钮,第一次运行会有点久哦,运行完之后,在target/generated-sources/protbuf/中生成gRPC代码。

Alt text

2.3 服务端代码

服务端代码的实现主要分为两部分:

  • 实现服务接口需要完成的实际工作:主要通过继承生成的基本服务类,并重写相应的 RPC 方法来完成具体的工作。

  • 运行一个 gRPC 服务,监听客户端的请求并返回响应。

实现服务类

自定义一个内部类,继承自生成的 JRServiceGrpc.JRServiceImplBase 抽象类。在 JRServiceImpl 中重写服务方法来完成具体的工作。

private static class JRServiceImpl extends JRServiceGrpc.JRServiceImplBase {
  //...
}

先来看一下 ListSongs 的实现。该方法接受一个 SingerId 请求,并返回一个 SongList 。注意 SongList 的定义, SongList 由一个或多个 Song 构成。

public void listSongs(SingerId request, StreamObserver<SongList> responseObserver) {
  SongList list = SongList.newBuilder().addAllSongs(genFakeSongs(request)).build();
  responseObserver.onNext(list);
  responseObserver.onCompleted();
}

可以看到, listSongs() 方法接受两个参数:

SingerId , 这个是请求 StreamObserver<SongList`> , 用于处理响应和关闭通道 这个方法中首先构建了 SongList 对象,然后使用 responseObserver 的 onNext() 方法返回响应,并调用 onCompleted() 方法表明已经处理完毕。

至于 GetSongs 的实现,基本和 ListSongs 一致。不同点在于,由于定义 RPC 方法时指定了响应是 stream Song ,因而可以多次返回响应。

public void getSongs(SingerId request, StreamObserver<Song> responseObserver) {
  List<Song> songs = genFakeSongs(request);
  for (Song song: songs) {
    responseObserver.onNext(song);
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      responseObserver.onError(e);
    }
  }
  responseObserver.onCompleted();
}

这里多次调用 responseObserver 的 onNext() 方法返回相应,每次间隔 1s ,调用 onCompleted() 方法表明经处理完毕。

启动服务端监听

private int port = 50051;
private Server server;

private void start() throws IOException{
  server = ServerBuilder.forPort(port).addService(new JRServiceImpl()).build();
  server.start();

  Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
      JRServiceServer.this.stop();
    }
  });
}

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

使用 ServerBuilder 来创建一个 Server ,主要分为三步:

  • 指定服务监听的端口
  • 创建具体的服务对象,并注册给 ServerBuilder
  • 创建 Server 并启动。

###2.3 客户端代码

为了调用服务端的方法,需要创建 stub 。有两种类型的 stub :

  • blocking/synchronous stub : 阻塞,客户端发起 RPC 调用后一直等待服务端的响应
  • non-blocking/asynchronous stub : 非阻塞,异步响应,通过 StreamObserver 在响应时进行回调

为了创建 stub , 首先要创建 channel , 需要指定服务端的主机和监听的端口。然后按序创建阻塞或者非阻塞的 stub 。

阻塞式客户端(blockingStub)

private final ManagedChannel channel;
private final JRServiceGrpc.JRServiceBlockingStub blockingStub;
private final JRServiceGrpc.JRServiceStub asyncStub;

public JRServiceClient(String hots, int port) {
  channel = ManagedChannelBuilder.forAddress(hots, port)
      // Channels are secure by default (via SSL/TLS). For the example we disable TLS to avoid
      // needing certificates.
      .usePlaintext(true)
      .build();
  blockingStub = JRServiceGrpc.newBlockingStub(channel);
  asyncStub = JRServiceGrpc.newStub(channel);
}
通过 stub 来调用发起 RPC 调用,直接在 stub 上调用同名方法。

public void getSongList() {
  SingerId request = SingerId.newBuilder().setId(1).build();
  SongList songList = blockingStub.listSongs(request);
  for (Song song : songList.getSongsList()) {
    logger.info(song.toString());
  }
}

构造请求对象并传递给 listSongs(request) 方法。看上去是调用本地方法进行处理,实际上中间涉及到网络的通信。

对于 stream Song 的响应,返回的是一个迭代器 Iterator

public void getSongsUsingStream() {
  SingerId request = SingerId.newBuilder().setId(1).build();
  Iterator<Song> iterator = blockingStub.getSongs(request);
  while (iterator.hasNext()) {
    logger.info(iterator.next().toString());
  }
}

非阻塞式客户端(asyncStub)

对于异步的 stub,则需要一个 StreamObserver 对象来完成回调处理:

public void getSongsUsingAsyncStub() throws InterruptedException {

  SingerId request = SingerId.newBuilder().setId(1).build();
  final CountDownLatch latch = new CountDownLatch(1); // using CountDownLatch

  StreamObserver<Song> responseObserver = new StreamObserver<Song>() {
    @Override
    public void onNext(Song value) {
      logger.info("get song :" + value.toString());
    }

    @Override
    public void onError(Throwable t) {
      Status status = Status.fromThrowable(t);
      logger.info("failed with status : " + status );
      latch.countDown();
    }

    @Override
    public void onCompleted() {
      logger.info("finished!");
      latch.countDown();
    }
  };

  asyncStub.getSongs(request, responseObserver);

  latch.await();
}

创建了一个实现了 StreamObserver 接口的匿名内部类对象 responseObserver 用于回调处理,每一次在收到一个响应的 Song 对象时会触发 onNext() 方法,RPC 调用完成或出错时则会调用 onCompleted() 和 onError() 。这里还用到了一个 CountDownLatch ,等待响应全部接受完毕后才从方法返回。

3.小结

总的来说,使用 gRPC 构建 RPC 分为三步:

  1. 使用 IDL 定义服务接口及通信消息对象;
  2. 使用 Protocol Buffers 和 gRPC 工具生成序列化/反序列化和 RPC 通信的代码;
  3. 基于生成的代码创建服务端和客户端应用。

gRPC 在数据交换格式上使用了自家的 Protocol Buffers,已经被证明是非常高效序列化框架;在传输协议上 gRPC 支持 HTTP 2.0 标准化协议,比 HTTP 1.1 有更好的性能。

RPC 的实现原理其实是基于 C/S 架构的,通过网络将客户端的请求传输给服务端,服务端对请求进行处理后将结果返回给客户端。在很多情况下使用 JSON 进行数据传输的 REST 服务和 RPC 实现的效果差不多,都是跨网络进行数据的交换,但是 RPC 中客户端在进行方法调用的时候更加便捷,底层是完全透明的,看上去就像是调用本地方法一样。

之前也简单地用过一点 FaceBook 开源 RPC 框架的 Thrift,感觉 gRPC 和 Thrift 在使用上还是比较接近的,不知道两者的性能对比如何^_^