SpringBoot基于gRPC进行RPC调用

SpringBoot基于gRPC进行RPC调用

  • 一、gRPC
    • [1.1 什么是gRPC?](#1.1 什么是gRPC?)
    • [1.2 如何编写proto](#1.2 如何编写proto)
    • [1.3 数据类型及对应关系](#1.3 数据类型及对应关系)
    • [1.4 枚举](#1.4 枚举)
    • [1.5 数组](#1.5 数组)
    • [1.6 map类型](#1.6 map类型)
    • [1.7 嵌套对象](#1.7 嵌套对象)
  • [二、SpringBoot gRPC](#二、SpringBoot gRPC)
    • [2.1 工程目录](#2.1 工程目录)
    • [2.2 jrpc-api](#2.2 jrpc-api)
      • [2.2.1 引入gRPC依赖](#2.2.1 引入gRPC依赖)
      • [2.2.2 编写 .proto 文件](#2.2.2 编写 .proto 文件)
      • [2.2.3 使用插件机制生产proto相关文件](#2.2.3 使用插件机制生产proto相关文件)
    • [2.2 jrpc-server](#2.2 jrpc-server)
      • [2.2.1 引入 `jrpc-api` 依赖](#2.2.1 引入 jrpc-api 依赖)
      • [2.2.2 编写impl](#2.2.2 编写impl)
      • [2.2.3 编写Config](#2.2.3 编写Config)
      • [2.2.4 yaml](#2.2.4 yaml)
    • [2.3 jrpc-client](#2.3 jrpc-client)
      • [2.3.1 引入 `jrpc-api` 依赖](#2.3.1 引入 jrpc-api 依赖)
      • [2.3.2 编写config](#2.3.2 编写config)
      • [2.3.3 yaml](#2.3.3 yaml)
      • [2.3.4 测试验证](#2.3.4 测试验证)
  • 三、gRPC高级应用

一、gRPC

1.1 什么是gRPC?

In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services. As in many RPC systems, gRPC is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub (referred to as just a client in some languages) that provides the same methods as the server.

在 gRPC 中,客户端应用程序可以直接调用服务器应用程序上的方法 在另一台机器上,就好像它是本地对象一样,使你更容易 创建分布式应用程序和服务。与许多 RPC 系统一样,gRPC 是 基于定义服务的思想,指定可以 使用其参数和返回类型进行远程调用。在服务器端, server 实现此接口并运行 gRPC 服务器来处理客户端调用。 在客户端,客户端有一个存根(在某些客户端中称为客户端 languages),它提供与服务器相同的方法。

gRPC 客户端和服务器可以在各种 环境 - 从 Google 内部的服务器到您自己的桌面 - 并且可以 用 gRPC 支持的任何语言编写。因此,例如,您可以轻松地 在 Java 中创建一个 gRPC 服务器,客户端使用 Go、Python 或 Ruby。另外 最新的 Google API 将具有其接口的 gRPC 版本,让您 轻松将 Google 功能构建到您的应用程序中。

gRPC 使用 proto buffers 作为服务定义语言,编写 proto 文件,即可完成服务的定义

1.2 如何编写proto

java 复制代码
syntax = "proto3";

option java_multiple_files = true;
// 生成位置
option java_package = "com.lizq.jrpc.api";
option java_outer_classname = "UserService";

package user;

service User {
    rpc SayHello (UserRequest) returns (UserResponse) {}
}

message UserRequest {
    string name = 1;
    int32 age = 2;
    string addr = 3;
}

message UserResponse {
    string name = 1;
    int32 age = 2;
    string addr = 3;
    OtherMsg otherMsg = 4;
    map<string, string> otherMap = 5;

    // 嵌套对象
    message OtherMsg {
        string ext1 = 1;
        string ext2 = 2;
    }
}
  • syntax = "proto3";:指定使用的protobuf版本;
  • option java_multiple_files = true;:如果为 false,则只会.java为此文件生成一个.proto文件,以及所有 Java 类/枚举/等。为顶级消息、服务和枚举生成的将嵌套在外部类中。如果为 true,.java将为每个 Java 类/枚举/等生成单独的文件。为顶级消息、服务和枚举生成,并且为此.proto文件生成的包装 Java 类将不包含任何嵌套类/枚举/等。 如果不生成 Java 代码,则此选项无效。
  • package user;:定义本服务的包名,避免不同服务相同消息类型产生冲突;
  • option java_package = "com.lizq.jrpc.api";:生成java文件包名;
  • option java_outer_classname = "UserService";:生成java文件类名称。如果文件中没有明确 java_outer_classname指定,.proto则将通过将.proto文件名转换为驼峰式来构造类名(因此 foo_bar.proto变为FooBar.java)
  • message UserResponse:定义服务的接口名称;
  • rpc SayHello (UserRequest) returns (UserResponse) {}:远程调用方法名,参数及响应类型;
  • message XXXXX{}:定义数据类型;

1.3 数据类型及对应关系

.proto类型 Notes C++ Type Java/Kotlin Python
double double double float
float float float float
int32 使用可变长度编码。对负数进行编码效率低下------如果您的字段可能有负值,请改用 sint32。 int32 int int
int64 使用可变长度编码。对负数进行编码效率低下------如果您的字段可能有负值,请改用 sint64。 int64 long int/long
uint32 使用可变长度编码。 uint32 int int/long
uint64 使用可变长度编码。 uint64 long int/long
sint32 使用可变长度编码。带符号的 int 值。这些比常规 int32 更有效地编码负数。 int32 int int
sint64 使用可变长度编码。带符号的 int 值。这些比常规 int64 更有效地编码负数。 int64 long int/long
fixed32 总是四个字节。如果值通常大于 228,则比 uint32 更有效 uint32 int int/long
fixed64 总是八个字节。如果值通常大于 256,则比 uint64 更有效。 uint64 long int/long
sfixed32 总是四个字节。 int32 int int
sfixed64 总是八个字节。 int64 long int/long
bool bool boolean bool
string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且不能超过 232。 string String str/unicode
bytes 可能包含不超过 2 32的任意字节序列。 string ByteString str (Python 2)、bytes (Python 3)

1.4 枚举

java 复制代码
enum Sex {
  NONE = 0;
  MAN = 1;
  WOMAN = 2;
}

message UserRequest {
  string name = 1;
  int32 age = 2;
  string addr = 3;
  Sex sex = 4;
}

**注意:**第一个枚举的值必须为0,因为0 是默认值,0 必须是第一个,保持和proto2 兼容

1.5 数组

使用 repeated 关键字来定义数组。

java 复制代码
message UserRequest {
  string name = 1;
  int32 age = 2;
  string addr = 3;
  Sex sex = 4;
  // 定义一个数组
  repeated string cellphones = 5;
}

1.6 map类型

在开发的过程中经常需要使用关联字段,很自然的想到使用map,protobuf也提供了map的类型。

java 复制代码
message UserResponse {
  string name = 1;
  map<string, string> otherMap = 2;
}

注意: map 字段前面不能是repeated

1.7 嵌套对象

java 复制代码
message UserResponse {
  string name = 1;
  int32 age = 2;
  string addr = 3;
  OtherMsg otherMsg = 4;
  map<string, string> otherMap = 5;

  // 嵌套对象
  message OtherMsg {
    string ext1 = 1;
    string ext2 = 2;
  }
}

二、SpringBoot gRPC

2.1 工程目录

2.2 jrpc-api

2.2.1 引入gRPC依赖

xml 复制代码
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-all</artifactId>
    <version>1.28.1</version>
</dependency>

2.2.2 编写 .proto 文件

java 复制代码
syntax = "proto3";

option java_multiple_files = true;
// 生成位置
option java_package = "com.lizq.jrpc.api";
option java_outer_classname = "UserService";

package user;

service User {
    rpc SayHello (UserRequest) returns (UserResponse) {}
}

message UserRequest {
    string name = 1;
    int32 age = 2;
    string addr = 3;
}

message UserResponse {
    string name = 1;
    int32 age = 2;
    string addr = 3;
    OtherMsg otherMsg = 4;
    map<string, string> otherMap = 5;

    // 嵌套对象
    message OtherMsg {
        string ext1 = 1;
        string ext2 = 2;
    }
}

2.2.3 使用插件机制生产proto相关文件

在 jrpc-api pom.xml 中添加如下:

xml 复制代码
<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.6.2</version>
        </extension>
    </extensions>
    <plugins>
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <!--&lt;!&ndash; ${os.detected.classifier} 变量由${os.detected.name} 和 ${os.detected.arch} 组成-->
                <protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.28.1:exe:${os.detected.classifier}</pluginArtifact>
                <!--protoSourceRoot 默认src/main/proto-->
                <protoSourceRoot>src/main/proto</protoSourceRoot>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

执行命令生产proto相关文件

将生成的文件拷贝到工程中,如下:

2.2 jrpc-server

jrpc-server 为 springboot 项目。

2.2.1 引入 jrpc-api 依赖

xml 复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>jrpc-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2.2.2 编写impl

java 复制代码
@Service
public class UserServiceImpl extends UserGrpc.UserImplBase {

    @Override
    public void sayHello(UserRequest request, StreamObserver<UserResponse> responseObserver) {
        Map<String, String> otherMap = new HashMap<>();
        otherMap.put("test", "testmap");
        UserResponse response = UserResponse.newBuilder()
                .setName("server:" + request.getName())
                .setAddr("server:" + request.getAddr())
                .setAge(request.getAge())
                .setOtherMsg(UserResponse.OtherMsg.newBuilder()
                        .setExt1("ext1")
                        .setExt2("ext2")
                        .build())
                .putAllOtherMap(otherMap)
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

2.2.3 编写Config

java 复制代码
@Configuration
public class GrpcServerConfiguration {

    @Value("${grpc.server-port}")
    private int port;

    @Bean
    public Server server() throws Exception {
        System.out.println("Starting gRPC on port {}." + port);
        // 构建服务端
        ServerBuilder<?> serverBuilder = ServerBuilder.forPort(port);
        // 添加需要暴露的接口
        this.addService(serverBuilder);
        // start
        Server server = serverBuilder.build().start();
        System.out.println("gRPC server started, listening on {}." + port);

        // 添加服务端关闭的逻辑
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutting down gRPC server.");
            if (server != null) {
                // 关闭服务端
                server.shutdown();
            }
            System.out.println("gRPC server shut down successfully.");
        }));

        if (server != null) {
            // 服务端启动后直到应用关闭都处于阻塞状态,方便接收请求
            server.awaitTermination();
        }
        return server;
    }

    @Autowired
    private UserServiceImpl userService;

    /**
     * 添加需要暴露的接口
     * @param serverBuilder
     */
    private void addService(ServerBuilder<?> serverBuilder) {
        serverBuilder.addService(userService);
    }
}

2.2.4 yaml

yaml 复制代码
server:
  port: 8081
spring:
  application:
    name: spring-boot-jrpc-server
grpc:
  server-port: 18081

2.3 jrpc-client

2.3.1 引入 jrpc-api 依赖

xml 复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>jrpc-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2.3.2 编写config

java 复制代码
@Configuration
public class GrpcClientConfiguration {

    @Value("${server-host}")
    private String host;

    /**
     * gRPC Server的端口
     */
    @Value("${server-port}")
    private int port;

    @Bean
    public ManagedChannel managedChannel() {
        // 开启gRPC客户端
        ManagedChannel managedChannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
        System.out.println("gRPC client started, server address: " + host + " , " + port);

        // 添加客户端关闭的逻辑
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                // 调用shutdown方法后等待1秒关闭channel
                managedChannel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
                System.out.println("gRPC client shut down successfully.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }));
        return managedChannel;
    }

    @Autowired
    private ManagedChannel managedChannel;

    @Bean
    public UserGrpc.UserBlockingStub userBlockingStub(ManagedChannel channel) {
        // 通过channel获取到服务端的stub
        return UserGrpc.newBlockingStub(managedChannel);
    }
}

2.3.3 yaml

yaml 复制代码
server:
  port: 8080
spring:
  application:
    name: spring-boot-jrpc-client

# 本地测试
server-host: 127.0.0.1
server-port: 18081

2.3.4 测试验证

java 复制代码
@RestController("/user")
public class UserController {

    @Autowired
    private UserGrpc.UserBlockingStub userBlockingStub;

    @GetMapping("/sayHello")
    public String sayHello(String name, String addr, int age) {
        UserRequest request = UserRequest.newBuilder()
                .setName(name)
                .setAddr(addr)
                .setAge(age)
                .build();
        UserResponse response;
        try {
            response = userBlockingStub.sayHello(request);
        } catch (StatusRuntimeException e) {
            e.printStackTrace();
            return e.getMessage();
        }
        return response.toString();
    }
}

浏览器访问:http://localhost:8080/user/sayHello?name=test&addr=addr&age=99 返回:

yaml 复制代码
name: "server:test" age: 99 addr: "server:addr" otherMsg { ext1: "ext1" ext2: "ext2" } otherMap { key: "test" value: "testmap" }

三、gRPC高级应用

可以阅读 Alibaba Nacos 2.x 源码,通过 gRPC 进行client、server网络通信,并且会有连接保持、重试机制。

相关推荐
码码哈哈0.015 分钟前
Vue 3 + Vite 集成 Spring Boot 完整部署指南 - 前后端一体化打包方案
前端·vue.js·spring boot
Easonmax2 小时前
用 Rust 打造可复现的 ASCII 艺术渲染器:从像素到字符的完整工程实践
开发语言·后端·rust
百锦再2 小时前
选择Rust的理由:从内存管理到抛弃抽象
android·java·开发语言·后端·python·rust·go
小羊失眠啦.2 小时前
深入解析Rust的所有权系统:告别空指针和数据竞争
开发语言·后端·rust
百***84452 小时前
SpringBoot(整合MyBatis + MyBatis-Plus + MyBatisX插件使用)
spring boot·tomcat·mybatis
q***71852 小时前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
大象席地抽烟3 小时前
使用 Ollama 本地模型与 Spring AI Alibaba
后端
程序员小假3 小时前
SQL 语句左连接右连接内连接如何使用,区别是什么?
java·后端
小坏讲微服务3 小时前
Spring Cloud Alibaba Gateway 集成 Redis 限流的完整配置
数据库·redis·分布式·后端·spring cloud·架构·gateway
方圆想当图灵3 小时前
Nacos 源码深度畅游:Nacos 配置同步详解(下)
分布式·后端·github