一、gRPC的介绍
在gRPC中,客户机应用程序可以直接调用不同机器上的服务器应用程序上的方法,就像它是本地对象一样,使您更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现这个接口,并运行gRPC服务器来处理客户端调用。在客户端,客户端有一个存根(在某些语言中称为客户端),它提供与服务器相同的方法。
gRPC客户端和服务器可以在各种环境中运行并相互通信 - 从Google内部的服务器到您自己的桌面 - 并且可以用任何gRPC支持的语言编写。因此,例如,您可以轻松地用Java创建gRPC服务器,用Go、Python或Ruby创建客户端。此外,最新的Google api将提供gRPC版本的接口,让您可以轻松地将Google功能构建到应用程序中。
gRPC的官网地址如下:
https://grpc.io/docs/
gRPC 使用 proto buffers 作为服务定义语言,编写 proto 文件,即可完成服务的定义。
二、前置准备
在resources目录下创建proto文件夹,根据protobuf协议编写 message.proto 文件和 file.proto 文件,server和client端都要编写。
file.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_package = "com.example.demo.protos";
message File {
string name = 1;
int32 size = 2;
}
message.proto 文件的内容如下:
syntax = "proto3";
package protocol;
import "file.proto";
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message User {
reserved 6 to 7;
reserved "userId2";
int32 userId = 1;
string username = 2;
oneof msg {
string error = 3;
int32 code = 4;
}
string name = 8;
UserType userType = 9;
repeated int32 roles = 10;
protocol.File file = 11;
map<string, string> hobbys = 12;
}
enum UserType {
UNKNOW = 0;
ADMIN = 1;
BUSINESS_USER = 2;
};
service UserService {
rpc getUser (User) returns (User) {}
rpc getUsers (User) returns (stream User) {}
}
service FileService {
rpc getFile(User) returns(File) {}
}
接着根据下述地址去官网中下载Protoc生成Java业务代码插件(protoc-gen-grpc-java),此处我选择的是1.68.0版本(protoc-gen-grpc-java-1.68.0-windows-x86_64.exe):
https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/
使用PowerShell切换到proto的bin路径下,使用下述命令生成model类:
.\protoc.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --java_out=E:\Code\Java\demo\src\main\java message.proto
.\protoc.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --java_out=E:\Code\Java\demo\src\main\java file.proto
接着再利用插件生成其对应的Service类:
.\protoc.exe --plugin=protoc-gen-grpc-java=E:\Protoc\bin\protoc-gen-grpc-java-1.68.0-windows-x86_64.exe --proto_path=E:\Code\Java\demo\src\main\resources\proto --grpc-java_out=E:\Code\Java\demo\src\main\java message.proto
执行上述三个命令后,可以看到大量的类文件(server和client端都要生成):
三、语法介绍
proto3语法的官方文档如下所示:
https://developers.google.com/protocol-buffers/docs/proto3
3.1 方法声明
grpc使用下述关键字来描述一个grpc服务:
关键字 | 说明 |
---|---|
service | 申明定义的是一个grpc的Service |
rpc | 申明这一行定义的是服务下的一个远程调用方法 |
returns | 声明本行定义的rpc 的返回值形式 |
stream | 声明这个数据是个流数据 |
3.2 枚举
使用 enum 关键字来定义数组,student.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
string name = 2;
int32 age = 3;
Sex sex = 4;
}
enum Sex {
NONE = 0;
MAN = 1;
WOMAN = 2;
}
Sex的第一个枚举值(NONE)必须为0,因为0是默认值,保持和proto2的语法兼容。
import com.example.demo.protos.Sex;
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;
public class Demo {
public static void main(String[] args) {
Student student =Student.newBuilder()
.setId(1)
.setName("张三")
.setAge(25)
.setSex(Sex.MAN)
.build();
String result = TextFormat.printer().escapingNonAscii(false).printToString(student);
System.out.println(result);
}
}
执行上述代码,其输出结果如下:
id: 1
name: "张三"
age: 25
sex: MAN
3.3 数组
使用 repeated 关键字来定义数组,student.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
string name = 2;
repeated string cellPhones = 3;
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;
public class Demo {
public static void main(String[] args) {
Student student =Student.newBuilder()
.setId(1)
.setName("张三")
.addCellPhones("10086")
.addCellPhones("10010")
.build();
String result = TextFormat.printer().escapingNonAscii(false).printToString(student);
System.out.println(result);
}
}
执行上述代码,其输出结果如下:
id: 1
name: "张三"
cellPhones: "10086"
cellPhones: "10010"
3.4 map类型
使用 map 关键字来定义集合,student.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
string name = 2;
map<string,string> otherMap = 3;
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;
public class Demo {
public static void main(String[] args) {
Student student =Student.newBuilder()
.setId(1)
.setName("张三")
.putOtherMap("address","北京市海淀区中央电视台")
.putOtherMap("phone","10086")
.build();
String result = TextFormat.printer().escapingNonAscii(false).printToString(student);
System.out.println(result);
}
}
执行上述代码,其输出结果如下:
id: 1
name: "张三"
otherMap {
key: "address"
value: "北京市海淀区中央电视台"
}
otherMap {
key: "phone"
value: "10086"
}
注意:map字段前面不能是repeated
3.5 嵌套对象
以下为嵌套对象的定义示例,student.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
string name = 2;
OtherMsg otherMsg = 3;
// 嵌套对象
message OtherMsg {
string ext1 = 1;
string ext2 = 2;
}
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;
public class Demo {
public static void main(String[] args) {
Student.OtherMsg otherMsg = Student.OtherMsg.newBuilder()
.setExt1("扩展信息1")
.setExt2("扩展信息2")
.build();
Student student = Student.newBuilder()
.setId(1)
.setName("张三")
.setOtherMsg(otherMsg)
.build();
String result = TextFormat.printer().escapingNonAscii(false).printToString(student);
System.out.println(result);
}
}
执行上述代码,其输出结果如下:
id: 1
name: "张三"
otherMsg {
ext1: "扩展信息1"
ext2: "扩展信息2"
}
3.6 oneof
oneof 是 Protocol Buffers (Proto) 语言中的一个关键特性,它允许你在定义数据结构时,在同一个消息中定义一组字段,但是每次只能设置其中的一个字段。这意味着如果你在一个 oneof 组内设置了多个字段,最后设置的字段会覆盖之前设置的字段值。oneof 的这种特性使得数据结构更加灵活,同时也可以用来节省存储空间,因为只有一个字段会被存储。
oneof 的使用场景包括但不限于:
- 可选字段:当一个消息中的多个字段是互斥的,即在任何给定时间只有一个字段会被设置。
- 节省空间:在存储或传输时,只有被设置的字段占用空间,这对于资源受限的环境非常有用。
- 类型安全的联合:oneof 可以看作是一种类型安全的联合体(union),确保了类型的正确性和使用的安全性。
以下为oneof语法的使用示例,student.proto 文件的内容如下:
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
oneof test_oneof{
string name =2;
string nickname = 3;
}
}
import com.example.demo.protos.Student;
import com.google.protobuf.TextFormat;
public class Demo {
public static void main(String[] args) {
Student student = Student.newBuilder()
.setId(1)
.setName("张三")
.setNickname("法外狂徒")
.build();
String result = TextFormat.printer().escapingNonAscii(false).printToString(student);
System.out.println(result);
}
}
执行上述代码,其输出结果如下:
id: 1
nickname: "法外狂徒"
在上面的例子中,test_oneof 是一个 oneof 组,它包含了两个字段:name和nickName。在任何给定的时间,Student只能包含这两个字段中的一个。
注意事项:
- 当解析一个包含 oneof 字段的消息时,如果有多个 oneof 组内的字段被设置,则只有最后一个被设置的字段会被保留。
- 在使用 oneof 时,虽然可以提高数据结构的灵活性和存储效率,但也要注意正确处理逻辑,确保数据的一致性和完整性。
3.7 reserved
reserved 关键字用于保留字段编号和字段名,以确保这些编号和名称在未来的版本更新中不会被重新使用。这主要用于向后兼容性,防止在移除字段后,该字段的编号被新添加的字段使用,从而导致数据解析错误。
在.proto文件中,可以使用reserved来保留字段编号和字段名,例如:
- 保留字段编号:reserved 3, 5 to 6;
- 保留字段名:reserved "sex","address";
syntax = "proto3";
package protocol;
option java_multiple_files = true;
option java_package = "com.example.demo.protos";
message Student {
int32 id = 1;
string name = 2;
reserved 3, 5 to 6;
string nickname = 4;
reserved "sex","address";
}
这些保留的字段编号和名称在后续的版本更新中不会被使用,从而保证了数据的兼容性。
使用reserved关键字的主要目的是确保数据的向后兼容性。在开发过程中,如果需要移除某个字段,直接删除或注释掉该字段可能会导致问题,因为其它部分可能还在使用这个字段的编号或名称。
四、服务端
4.1 引入依赖
特别说明当前Spring Boot的版本为2.1.3,gRPC服务端与Spring Boot整合时,需要引入下述依赖:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
4.2 项目配置文件
在resources目录下新建一个名为application.yml的文件,其配置信息如下所示:
server:
port: 8080
grpc:
server:
port: 9090
这里 grpc.server 表示是服务端的配置 ,此处服务端的端口为9090。
4.3 服务端业务
import com.example.demo.protos.User;
import com.example.demo.protos.UserServiceGrpc;
import com.example.demo.protos.UserType;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(User request, StreamObserver<User> responseObserver) {
User user;
if (request.getUserTypeValue() == 1) {
// 模拟根据条件从数据库查询数据
user = User.newBuilder()
.setUserId(1)
.setName("张三")
.setCode(1)
.setUserType(UserType.ADMIN)
.build();
} else {
// 模拟根据条件从数据库查询数据
user = User.newBuilder()
.setUserId(2)
.setName("李四")
.setCode(1)
.setUserType(UserType.BUSINESS_USER)
.build();
}
responseObserver.onNext(user);
responseObserver.onCompleted();
}
@Override
public void getUsers(User request, StreamObserver<User> responseObserver) {
// 模拟根据条件(request.getCode() == 1)从数据库查询数据
User user1 = User.newBuilder()
.setUserId(1)
.setName("张三")
.setCode(1)
.setUserType(UserType.ADMIN)
.build();
User user2 = User.newBuilder()
.setUserId(2)
.setName("李四")
.setCode(1)
.setUserType(UserType.BUSINESS_USER)
.build();
User user3 = User.newBuilder()
.setUserId(3)
.setName("王五")
.setCode(1)
.setUserType(UserType.UNKNOW)
.build();
User user4 = User.newBuilder()
.setUserId(4)
.setName("赵六")
.setCode(1)
.setUserType(UserType.ADMIN)
.build();
responseObserver.onNext(user1);
responseObserver.onNext(user2);
responseObserver.onNext(user3);
responseObserver.onNext(user4);
responseObserver.onCompleted();
}
}
上述UserServiceImpl.java中有几处需要注意:
- 使用@GrpcService注解,再继承UserServiceImplBase,这样就可以借助grpc-server-spring-boot-starter库将getUser暴露为gRPC服务
- UserServiceImplBase是前面根据proto自动生成的java代码,在grpc-lib模块中
- getUser方法中处理完毕业务逻辑后,调用responseObserver.onNext方法填入返回内容
- 调用responseObserver.onCompleted方法表示本次gRPC服务完成
五、客户端
5.1 引入依赖
特别说明当前Spring Boot的版本为2.1.3,gRPC客户端与Spring Boot整合时,需要引入下述依赖:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.15.0.RELEASE</version>
</dependency>
5.2 项目配置文件
在resources目录下新建一个名为application.yml的文件,其配置信息如下所示:
server:
port: 8088
spring:
application:
name: demo
grpc:
client:
userClient:
negotiationType: PLAINTEXT
address: static://localhost:9090
这里 grpc.client 表示是客户端的配置,userClient 具有特殊的含义,可以理解为gRPC调用服务端的一组配置项,可任意取名。negotiationType 表示的是文本传输配置,此处值为PLAINTEXT(文本传输)。address 表示的是gRPC服务端的地址和端口配置。
5.3 客户端测试
此处 @GrpcClient 注解中的属性值为userClient,表示的是UserServiceGrpc.UserServiceBlockingStub采用userClient配置项调用服务端,这也就和前面的yml文件中的配置形成了呼应。
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import com.example.demo.protos.User;
import com.example.demo.protos.UserServiceGrpc;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Iterator;
@RestController
@RequestMapping("/grpc")
public class GrpcClientController {
@GrpcClient("userClient")
private UserServiceGrpc.UserServiceBlockingStub userService;
@GetMapping("/getUser")
public String getUser() {
User ro = User.newBuilder().setUserTypeValue(1).build();
User user = userService.getUser(ro);
JSONObject jsonObject = new JSONObject(4,true);
jsonObject.putOpt("userId",user.getUserId());
jsonObject.putOpt("name", user.getName());
jsonObject.putOpt("code",user.getCode());
jsonObject.putOpt("userType",user.getUserType().getNumber());
return jsonObject.toString();
}
@GetMapping("/getUsers")
public String getUsers() {
User ro = User.newBuilder().setCode(1).build();
Iterator<User> iterator = userService.getUsers(ro);
JSONArray jsonArray =new JSONArray();
while (iterator.hasNext()){
User user =iterator.next();
JSONObject jsonObject = new JSONObject(4,true);
jsonObject.putOpt("userId",user.getUserId());
jsonObject.putOpt("name", user.getName());
jsonObject.putOpt("code",user.getCode());
jsonObject.putOpt("userType",user.getUserType().getNumber());
jsonArray.add(jsonObject);
}
return jsonArray.toString();
}
}
上述GrpcClientController类有几处要注意的地方:
- 用@GrpcClient修饰UserServiceBlockingStub,这样就可以通过grpc-client-spring-boot-starter库发起gRPC调用,被调用的服务端信息来自名为userClient的配置
- UserServiceBlockingStub来自前面根据proto文件生成的java代码
- UserServiceBlockingStub.getUser方法会远程调用userClient应用的gRPC服务
调用 /grpc/getUser 接口,其返回结果如下所示:
调用 /grpc/getUsers 接口,其返回结果如下所示: