gRPC

发布时间 2023-06-28 15:51:12作者: zerozayu

一、protobuf

gRPC传输用这个挺好的,跨语言调用

  • 足够简单
  • 序列化后体积小
  • 解析速度比XML块
  • 多语言支持
  • 兼容性好

proto3

这里就先不提proto2了
文件后缀名是 .proto

1.开始部分

syntax = "proto3"; // compile is proto3

// 生成的文件是处在哪个目录哪个包中,.代表在当前目录生成,service代表文件生成的包名是service
option java_package = ".;service";

2.Message

可以理解为需要传输的数据格式的定义,类似于Java中的类class。使用 protobuf 编译器将 proto 编译成 java 代码后,每个 message 都会生成一个名字与之对应的类。

字段规则:

  • optional:可选字段,默认为可选字段
  • repeate:可重复字段 => 数组

消息号:
每个字段必须有一个唯一的标识号,不能重复,范围[2,2&29-1]

message SearchRequest {
	string query = 1;// 每个字段必须有一个唯一的标识号,不能重复,范围:[2,2^29-1]
	int32 page_number = 2;
	int32 result_per_page = 3;
}

1.Type

double, float, int32, int64, uint32, uint64, sint32, sint64,(s* is more effcient in negative number)
fixed32, fixed64, sfixed32, sfixed64, bool, string, bytes.

repeated //by index
Timestamp
Duration
Struct
ListValue

默认值

string ""
bytes ""
bool false
numeric 0
enums 0
message fields are language-dependent.

image.png

2.enum

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAEGS = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
/////////////////////////////////////
message MyMessage1 {
  enum EnumAllowingAlias {
    option Allow_alias = true; // same variable in different enums, need this options
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  }
}

3.using other message

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

4.Importing Definitions

JAVA is not suitable

5.Nested Types

as deeply as U like

// 1 *************************
// staight Nested another message
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snipeets = 3;
}

// 2 ***************************
// nest partial message
message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

6.any

7.one of

have many fields but use ao most one field once.
a oneof cannot be repeated.

message SampleMessage{
  oneof test_oneof {
    string name = 4;
    SubMessage submessage = 9;
  }
}

SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name filed;
CHECK(!message.has_name());

8.Maps

map<string, Project> projects = 3;

9.Define Services

service SearchService{
  rpc Search(SearchRequest) return (SearchResponse);
  rpc Search1(SearchRequest) return (stream SearchResponse); // 服务端流式 gRPC 响应
  rpc Search2(stream SearchRequest) return (SearchResponse); // 客户端流式 gRPc 通信
}

10.Generating Classes

protoc --proto_path=IMPORT_PATH --cpp_out --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
# *_out point the DST_DIR of the output
# proto_path points the import_PATH?
# path/to/file.proto is the file be compiling.

protoc -I=$SRC_DIR --cpp_out=$DST_DIR xxx.proto

  • $SRC_DIR: proto所在的源目录
  • --cpp_out:生成 cpp 代码
  • $DST_DIR:生成代码的目标目录
  • xxx.proto:要针对哪个 proto 文件生成接口,例如 tutorial.person.proto

编译完成后,将生成 2 个文件,tutorial.pb.htutorial.pb.c,pb 是 protobuf 的缩写。
此外,protocol buffer 编译器为.proto文件中定义的消息的每个字段生成一套存取器方法:
对于 message Person 中的 required int32 id = 2,编译器将生成下列存取器方法:
bool has_id() const: 用于判断字段id是否存在。如果字段被设置,返回true。
**int32 id() const: **返回字段id的当前值,如果字段没有被设置,返回缺省值。
void set_id(int32 value) : 设置字段id的值。调用此方法后,has_id() 将返回true以及id() 将返回value。
void clear_id():清除字段的值。调用此方法后,has_id()将返回false 以及id()将返回缺省值。
当然,对于其他类型的字段,编译器也会生成不同的存取方法,这里就不一一列举了。

二、rpc 通信模式

image.png

stub-存根,客户端使用
ImplBase-骨架类,服务端使用

1.一元 RPC 通信模式

image.png
使用newBlockingStub方法,同步,

2.服务端流式 RPC 通信模式

image.png
使用newBlockingStub,支持服务器端推流

3.客户端流式 RPC 通信模式

image.png
使用 newStub,异步通信

4.双向流式 RPC 通信模式

跟 3 差不多,使用 newStub,异步通信

三、gRPC 集成到 springboot 与 Eureka

1.集成 springboot

使用下面的 starter ,结合注解与配置文件使用
pom 文件(c 端与 s 端只是使用的具体 starter 不同,s 为grpc-server-spring-boot-starter,c 为grpc-client-spring-boot-starter):

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>spring-boot-demo</artifactId>
        <groupId>com.zhangyu</groupId>
        <version>0.0.1</version>
    </parent>
    <artifactId>grpc-server-spriongboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>grpc-server-spriongboot</name>
    <description>grpc-server-spriongboot</description>
    <properties>
        <java.version>11</java.version>
        <protoc.version>3.21.5</protoc.version>
        <protobuf.version>1.51.0</protobuf.version>
        <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
    </properties>
    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- grpc-server -->
        <dependency>
            <groupId>net.devh</groupId>
            <artifactId>grpc-server-spring-boot-starter</artifactId>
            <version>2.14.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <extensions>
            <!-- 为了识别不同的操作系统,这样插件可以根据不同的平台加载不同protoc编译器文件 -->
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>${protobuf-maven-plugin.version}</version>
                <configuration>
                    <pluginId>grpc-java</pluginId>
                    <!-- proto文件放置的目录 -->
                    <!-- <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot> -->
                    <!-- 生成文件的目录, 可以让其在源码目录生成 -->
                    <!-- <outputDirectory>${project.basedir}/src/main/java</outputDirectory>-->
                    <!-- 生成文件前是否把目标目录清空,这个最好设置为false,以免误删项目文件 -->
                    <!-- <clearOutputDirectory>false</clearOutputDirectory>-->

                    <!-- 寻找本机 protoc 执行命令,需配置环境变量 -->
                    <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
                    </protocArtifact>
                    <!-- 生成 grpc 的通信类 -->
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${protobuf.version}:exe:${os.detected.classifier}
                    </pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <!-- 生成消息代码 -->
                            <goal>compile</goal>
                            <!-- 生成 grpc 的通信文件 -->
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

application.yml:

  • s 端
server:
  port: 8080
grpc:
  server:
    port: 9090
  • c 端
server:
  port: 8081
grpc:
  client:
    # 自定义名称,结合@GrpcClient("grpc-server")使用,得到存根bean
    grpc-server:
      # static 是使用静态地址
      address: 'static://127.0.0.1:9090'
      # 文本传输
      negotiation-type: plaintext

业务代码层:

  • s 端
@GrpcService // 需要加这个注解
public class NewService extends NewsServiceGrpc.NewsServiceImplBase {
    @Override
    public void list(NewsProto.NewsRequest request, StreamObserver<NewsProto.NewsResponse> responseObserver) {
        String date = request.getDate();
        NewsProto.NewsResponse response = NewsProto.NewsResponse.newBuilder().setTitle("新闻-" + date).build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}
  • c 端
@RestController
@RequestMapping("/grpc")
public class NewsController {
    
    // 指明使用的配置文件的自定义名称,来获取存根类 bean
    @GrpcClient("grpc-server")
    private NewsServiceGrpc.NewsServiceBlockingStub blockingStub;

    @GetMapping("/news/list")
    public String listNews(@RequestParam(value = "date") String date) {

        NewsProto.NewsResponse response = blockingStub.list(NewsProto.NewsRequest.newBuilder().setDate(date).build());

        return response.getTitle();
    }
}

2.接入 Eureka

微服务体系(不过这好像是 cloud 一代目的东西?不是很清楚)
image.png
ps:二代目是这样:
image.png

1.Eureka 服务端

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Eureka 记得声明 colud 的版本-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
server:
	# 服务开在 8761 端口
  port: 8761

eureka:
  server:
    # 是否开启自我保护,默认true
    enable-self-preservation: false
  instance:
    appname: eureka-server
    hostname: localhost
  client:
    service-url:
      # 默认服务注册中心地址
      defaultZone:
        http://localhost:8761/eureka/
    register-with-eureka: false
    fetch-registry: false

具体配置查看Eureka 在 application.yml 里面的配置详解

@SpringBootApplication
// 开启 Eureka 服务器端
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

2.Eureka 客户端 - grpc 服务端

<!-- web -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- grpc-server -->
<!-- client 的话把server 换成 client 就行 -->
<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-server-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

<!-- Eureka -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

//---------
<build>
  <extensions>
    <!-- 为了识别不同的操作系统,这样插件可以根据不同的平台加载不同protoc编译器文件 -->
    <extension>
      <groupId>kr.motd.maven</groupId>
      <artifactId>os-maven-plugin</artifactId>
      <version>1.5.0.Final</version>
    </extension>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
    </plugin>
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>${protobuf-maven-plugin.version}</version>
      <configuration>
        <pluginId>grpc-java</pluginId>
        <!-- proto文件放置的目录 -->
        <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot>
        <!-- 生成文件的目录, 可以让其在源码目录生成 -->
        <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
        <!-- 生成文件前是否把目标目录清空,这个最好设置为false,以免误删项目文件 -->
        <clearOutputDirectory>false</clearOutputDirectory>

        <!-- 寻找本机 protoc 执行命令,需配置环境变量 -->
        <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
        </protocArtifact>
        <!-- 生成 grpc 的通信类 -->
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:${protobuf.version}:exe:${os.detected.classifier}
        </pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <!-- 生成消息代码 -->
            <goal>compile</goal>
            <!-- 生成 grpc 的通信文件 -->
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

server:
  port: 8080

spring:
  application:
    # 此名字会向 Eureka 注册
    name: spb-server-eureka
eureka:
  client:
    service-url:
      # 具体哪个地址提供了 Eureka 服务
      defaultZone:
        http://localhost:8761/eureka/
grpc:
  server:
    # 采用一个未占用的随机端口号来提供 gRPC 服务
    port: 0
syntax = "proto3";

option java_multiple_files = false;
option java_package = "com.zhangyu.news.proto";
option java_outer_classname = "NewProto";

package news;

service NewService {
  rpc listNews(NewsRequest) returns(NewsResponse){};
}

message NewsRequest {
  string name = 1;
}

message NewsResponse {
  string result = 1;
}
@SpringBootApplication
// 开启 Eureka 客户端类
@EnableEurekaClient
public class SpbServerEurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpbServerEurekaApplication.class, args);
    }
}

// 开启 grpc 注解
@GrpcService
public class NewsService extends NewServiceGrpc.NewServiceImplBase {

    @Override
    public void listNews(NewProto.NewsRequest request, StreamObserver<NewProto.NewsResponse> responseObserver) {
        String name = request.getName();
        responseObserver.onNext(NewProto.NewsResponse.newBuilder().setResult("News name is " + name).build());
        responseObserver.onCompleted();
    }
}

3.Eureka 客户端 - grpc 客户端

<dependency>
  <groupId>net.devh</groupId>
  <artifactId>grpc-client-spring-boot-starter</artifactId>
  <version>2.14.0.RELEASE</version>
</dependency>

把服务端的 gprc 服务端依赖改成对应的客户端依赖就行

server:
  port: 8081

spring:
  application:
    # 会把这个名字注册到 Eureka 注册中心
    name: spb-client-eureka
  eureka:
    client:
    service-url:
      # 具体哪个地址提供了 Eureka 服务
      defaultZone:
        http://localhost:8761/eureka/
grpc:
  client:
    # 自定义名称,且需服务端的 application 名(即注册到 eureka 的服务名)相同,不需要再显式的写明地址端口号
    spb-server-eureka:
      # 声明传输格式
      negotiation-type: plaintext

@SpringBootApplication
// 开启 Eureka 客户端
@EnableEurekaClient
public class SpbClientEurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpbClientEurekaApplication.class, args);
    }
}
@RestController
@RequestMapping("/grpc")
public class NewsController {
    // 需要与配置文件里面自定义名称相关联,来获取相应的存根类
    @GrpcClient("spb-server-eureka")
    private NewServiceGrpc.NewServiceBlockingStub newServiceBlockingStub;

    @GetMapping("/list")
    public void list() {
        NewProto.NewsResponse newsResponse = newServiceBlockingStub.listNews(NewProto.NewsRequest.newBuilder().setName("震惊!现在居然还有人在用 Eureka!").build());

        System.out.println(newsResponse.getResult());
    }
}