最近准备着手学习下gRPC,就先以一个小demo作为开始吧。

创建Maven项目

刚开始学习就没必要着急与SpringBoot结合了,先试试最传统的Java项目。其pom.xml内容如下:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<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>org.example</groupId>
<artifactId>rpcdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>rpcdemo</name>
<url>http://maven.apache.org</url>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<protobuf.version>3.21.12</protobuf.version>
<grpc.version>1.53.0</grpc.version>
</properties>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<!-- 如果是Java8以上的,需要添加这个依赖,看issue,似乎grpc也没得办法 -->
<!-- https://github.com/grpc/grpc-java/issues/8086 -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</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>
<pluginId>grpc-java</pluginId>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
<!-- proto文件放置的目录 -->
<protoSourceRoot>src/main/resources/proto</protoSourceRoot>
<!-- 生成文件的目录, 可以让其在源码目录生成 -->
<!-- <outputDirectory>${project.basedir}/src/main/java</outputDirectory>-->
<!-- 生成文件前是否把目标目录清空,这个最好设置为false,以免误删项目文件 -->
<!-- <clearOutputDirectory>false</clearOutputDirectory>-->
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>

</plugins>
</build>
</project>

创建.proto文件

根据pom.xml中指定的路径,在src/main/resources/proto中创建一个名为helloworld.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
//在非空非注释的第一行指定当前文件使用的是proto3的语法,默认proto2
syntax = "proto3";
//package与java_package有些不同,java_package是定义编译生成的java文件所在的目录,而package是对应的java类的命名空间
package example;
option java_package = "cn.net.dev.grpc";
//要生成Java类的名称
option java_outer_classname = "HelloWorldServiceProto";
//编译后会生成多个Message类,并没有被包含在HelloWorldServiceProto.java文件中,反之,生成单一HelloWorldServiceProto.java文件
option java_multiple_files = true;

//定义服务端接口类
service Greeter {
// //服务端接口方法 Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// //请求参数 基于序号的协议字段映射,所以字段可以乱序,可缺段 The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// //响应参数 The response message containing the greetings
message HelloReply {
string message = 1;
}

在IDE中,点击MAVEN侧边栏->plugins->protobuf->protobuf:compile,protobuf:compile-custom,或者直接在控制台中输入mvn compile,mvn compile-custom

protobuf:compile // 编译消息对象

protobuf:compile-custom // 依赖消息对象,生成接口服务

具体可参考 Maven Protocol Buffers Plugin – protobuf:compile (xolstice.org)

编译完成后,会在target/generated-sources/protobuf/java/cn/net/dev/grpc文件夹中生成helloworld.proto对应消息的Java代码,在target/generated-sources/protobuf/grpc-java/cn/net/dev/grpc文件夹中生成helloworld.proto对应服务的Java代码。如果想把这些代码的生成路径放在源码(src目录)中,则打开outputDirectory注释就行,注意clearOutputDirectory要设置为false,避免清空其他代码。我看了很多例子,很少由把代码生成到源码目录的,所以我也打算保持这个习惯。

创建Server端

先创建一个gRPC Server

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
package cn.net.dev.grpc.server;

import cn.net.dev.grpc.GreeterGrpc;
import cn.net.dev.grpc.HelloReply;
import cn.net.dev.grpc.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 HelloWorldServer {
private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName());
private int port = 50051;
private Server server;

public HelloWorldServer() {
}

private void start() throws IOException {
this.server = ServerBuilder.forPort(this.port).addService(new GreeterImpl()).build().start();
logger.info("Server started, listening on " + this.port);
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
System.err.println("*** shutting down gRPC server since JVM is shutting down");
HelloWorldServer.this.stop();
System.err.println("*** server shut down");
}
});
}

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

}

private void blockUntilShutdown() throws InterruptedException {
if (this.server != null) {
this.server.awaitTermination();
}

}

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

static class GreeterImpl extends GreeterGrpc.GreeterImplBase {
GreeterImpl() {
}

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

创建Client端

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
package cn.net.dev.grpc.client;

import cn.net.dev.grpc.GreeterGrpc;
import cn.net.dev.grpc.HelloReply;
import cn.net.dev.grpc.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 HelloWorldClient {
private static final Logger logger = Logger.getLogger(HelloWorldClient.class.getName());
//客户端与服务器的通信channel
private final ManagedChannel channel;
//客户端与服务器的通信channel
private final GreeterGrpc.GreeterBlockingStub blockingStub;

public HelloWorldClient(String host, int port) {
//指定grpc服务器地址和端口初始化通信channel
this.channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
//根据通信channel初始化客户端存根节点
this.blockingStub = GreeterGrpc.newBlockingStub(this.channel);
}

public void shutdown() throws InterruptedException {
this.channel.shutdown().awaitTermination(5L, 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 = this.blockingStub.sayHello(request);
} catch (StatusRuntimeException var5) {
logger.log(Level.WARNING, "RPC failed: {0}", var5.getStatus());
return;
}

logger.info("Greeting: " + response.getMessage());
}

public static void main(String[] args) throws Exception {
HelloWorldClient client = new HelloWorldClient("localhost", 50051);

try {
String user = "world";
if (args.length > 0) {
user = args[0];
}

client.greet(user);
} finally {
client.shutdown();
}

}
}

由于protobuf的代码生产到target目录了,所以刚开始我一直找不到包,但可以点击过去,在我重启idea以后这个问题解决了,有这类问题的可以试试这个办法。

然后先执行服务端,再执行客户端,客户端会收到如下信息:

1
2
3
4
5
6
7
8
Connected to the target VM, address: '127.0.0.1:61011', transport: 'socket'
3月 19, 2023 11:16:59 上午 cn.net.dev.grpc.client.HelloWorldClient greet
信息: Will try to greet world ...
3月 19, 2023 11:17:00 上午 cn.net.dev.grpc.client.HelloWorldClient greet
信息: Greeting: Hello world
Disconnected from the target VM, address: '127.0.0.1:61011', transport: 'socket'

Process finished with exit code 0

项目结构

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
D:.
│ pom.xml
├─src
│ ├─main
│ │ ├─java
│ │ │ ├─cn
│ │ │ │ └─net
│ │ │ │ └─dev
│ │ │ │ └─grpc
│ │ │ │ ├─client
│ │ │ │ │ HelloWorldClient.java
│ │ │ │ │
│ │ │ │ └─server
│ │ │ │ HelloWorldServer.java
│ │ │ │
│ │ │ └─org
│ │ │ └─example
│ │ │ App.java
│ │ │
│ │ └─resources
│ │ └─proto
│ │ helloworld.proto
│ │
│ └─test
│ └─java
│ └─org
│ └─example
│ AppTest.java

└─target

├─generated-sources
│ ├─annotations
│ └─protobuf
│ ├─grpc-java
│ │ └─cn
│ │ └─net
│ │ └─dev
│ │ └─grpc
│ │ GreeterGrpc.java
│ │
│ └─java
│ └─cn
│ └─net
│ └─dev
│ └─grpc
│ HelloReply.java
│ HelloReplyOrBuilder.java
│ HelloRequest.java
│ HelloRequestOrBuilder.java
│ HelloWorldServiceProto.java


└─test-classes
└─org
└─example
AppTest.class