gRPC 基础教程之一

前言

本文将介绍 gRPC、Protocol Buffers 的概念,同时会给出 Protocol Buffers 代码生成器的使用教程,还有编写第一个基于 gRPC 的服务提供者与服务消费者的示例程序。

相关站点

gRPC 简介

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java、Go 语言版本,分别是:grpc、grpc-java、grpc-go,其中 C 版本支持 C、C++、Node.js、Python、Ruby、Objective-C、PHP、C#。gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特性。在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。值得说明的是,gRPC 客户端和服务端可以在多种环境中运行和交互,支持用任何 gRPC 支持的语言来编写,所以可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。

grpc-1

使用 Protocol Buffers

gRPC 默认使用 Protocol Buffers,这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。当使用 proto files 创建 gRPC 服务,用 Protocol Buffers 消息类型来定义方法参数和返回类型。尽管 Protocol Buffers 已经存在了一段时间,官方的示例代码种使用了一种名叫 proto3 的新风格的 Protocol Buffers,它拥有轻量简化的语法、一些有用的新功能,并且支持更多新语言。当前针对 Java 和 C++ 发布了 beta 版本,针对 JavaNano(即 Android Java)发布 alpha 版本,在 Protocol Buffers Github 源码库里有 Ruby 支持, 在 Github 源码库里还有针对 Go 语言的生成器, 对更多语言的支持正在开发中。虽然可以使用 proto2 (当前默认的 Protocol Buffers 版本), 通常建议在 gRPC 里使用 proto3,因为这样可以使用 gRPC 支持全部范围的的语言,并且能避免 proto2 客户端与 proto3 服务端交互时出现的兼容性问题,反之亦然。

本地编译安装 Protocol Buffers(可选)

参考自 gRPC-Java、Protobuf 编译构建的官方教程,一般情况下不需要构建 gRPC-Java,只有在对 gRPC-Java 的源码进行了更改或测试使用 gRPC-Java 库的非发布版本(例如 master 分支)时才需要构建。若本地安装了 Protobuf,则可以直接通过命令的方式调用 Protobuf 的代码生成器,无需再依赖额外的 IDE 插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 系统环境
CentOS Linux release 7.6.1810 (Core)
Linux develop 3.10.0-957.21.3.el7.x86_64 #1 SMP Tue Jun 18 16:35:19 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

# 拉取源码
# git clone https://github.com/google/protobuf.git

# 进入源码目录
# cd protobuf

# 切换至需要编译的版本的分支
# git checkout v3.7.1

# 查看当前所在的分支信息
# git branch -v

# 检测安装环境
# ./autogen.sh
# ./configure --disable-shared

# 编译安装
# make -j 8
# make install

# 如果/usr/local/lib不在库搜索路径中,可以通过运行以下命令添加
# sh -c 'echo /usr/local/lib >> /etc/ld.so.conf'

# 使添加的库搜索路径生效
# ldconfig

# 查看protobuf安装的版本号
# protoc --version

# 编写.proto文件,使用protobuf的代码生成器自动生成Java代码,命令格式如下
# protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

# 默认安装路径:/usr/local
# 指定安装目录可以使用此命令: ./configure --disable-shared --prefix=/usr/local/protobuf-3.7.1

Eclipse 项目中添加 Protobuf 自动生成代码的 Maven 插件与 Protobuf 依赖

Protobuf 的原型文件和一些适合的插件,默认放在 src/main/proto 和 src/test/proto 目录中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.21.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.21.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.21.0</version>
</dependency>

<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:3.7.1:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.21.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

往 Gradle 构建的项目添加 Protobuf 自动生成代码的插件与 Protobuf 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
plugins {
id 'com.google.protobuf' version '0.8.8'
}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.7.1"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}

dependencies {
compile 'io.grpc:grpc-stub:1.21.0'
compile 'io.grpc:grpc-protobuf:1.21.0'
compile 'io.grpc:grpc-netty-shaded:1.21.0'
testCompile group: 'junit', name: 'junit', version: '4.12'
}

编写 Proto 文件(定义服务),执行编译后自动生成 Java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 创建gradle工程grpc-demo-provider,目录结构如下:
grpc-demo-provider/
├── build.gradle
└── src
├── main
│   ├── java
│   ├── proto
│   │   └── helloworld.proto
│   └── resources
└── test
├── java
├── proto
└── resources

# 进入工程目录
# cd grpc-demo-provider

# 编辑build.gradle文件,添加protobuf插件与依赖,可参考上面给出的gradle配置内容

# 创建proto文件
# mkdir -p src/main/proto
# vim src/main/proto/helloworld.proto

syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.grpc.demo.generate";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

# 执行编译,自动生成Java文件
# gradle clean build

# 查看自动生成的文件目录结构,默认生成文件所在的目录是:$buildDir/generated/source/proto,其中Message在main/java目录下,Service在目录main/grpc下
# tree build/generated/source/proto
main
├── grpc
│   └── com
│   └── grpc
│   └── demo
│   └── generate
│   └── GreeterGrpc.java
└── java
└── com
└── grpc
└── demo
└── generate
├── HelloReply.java
├── HelloReplyOrBuilder.java
├── HelloRequest.java
├── HelloRequestOrBuilder.java
└── HelloWorldProto.java

Gradle 指定 Protobuf 代码自动生成的目录位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 指定Message代码的生成位置,最终生成位置在src/main/java目录下
protobuf {
generatedFilesBaseDir = "src"
}

// 指定Service代码的生成位置,最终生成位置在src/main/java目录下
protobuf {
generateProtoTasks {
all()*.plugins {
grpc {
outputSubDir = 'java'
}
}
}
}

// 完整的写法,同时指定Message、Service代码生成的目录位置为src/main/java
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.7.1"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.21.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {
outputSubDir = 'java'
}
}
}
generatedFilesBaseDir = 'src'
}

RPC 服务提供者的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.grpc.demo.provider.service;

import com.grpc.demo.generate.GreeterGrpc;
import com.grpc.demo.generate.HelloReply;
import com.grpc.demo.generate.HelloRequest;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

import java.io.IOException;
import java.util.logging.Logger;

public class HelloWorldProvider {

private Server server;
private static final Logger logger = Logger.getLogger(HelloWorldProvider.class.getName());

private void start() throws IOException {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new GreeterImpl())
.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");
HelloWorldProvider.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();
}
}

public static void main(String[] args) throws IOException, InterruptedException {
final HelloWorldProvider server = new HelloWorldProvider();
server.start();
server.blockUntilShutdown();
}

static class GreeterImpl extends GreeterGrpc.GreeterImplBase {

@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}

RPC 服务消费者的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.grpc.demo.consumer.service;

import com.grpc.demo.generate.GreeterGrpc;
import com.grpc.demo.generate.HelloReply;
import com.grpc.demo.generate.HelloRequest;
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;

public class HelloWorldConsumer {

private final ManagedChannel channel;
private final GreeterGrpc.GreeterBlockingStub blockingStub;
private static final Logger logger = Logger.getLogger(HelloWorldConsumer.class.getName());

public HelloWorldConsumer(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port)
.usePlaintext()
.build());
}

HelloWorldConsumer(ManagedChannel channel) {
this.channel = channel;
blockingStub = GreeterGrpc.newBlockingStub(channel);
}

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

public void greet(String name) {
logger.info("==> Will try to greet " + name + " ...");
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e) {
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("==> Greeting: " + response.getMessage());
}

public static void main(String[] args) throws Exception {
HelloWorldConsumer client = new HelloWorldConsumer("localhost", 50051);
try {
String user = "World";
client.greet(user);
} finally {
client.shutdown();
}
}
}

先后启动 Provider、Consumer 应用,最终输出的日志信息如下图所示

Provider 应用的日志信息:
grpc-provider-log

Consumer 应用的日志信息:
grpc-consumer-log