计算机系统应用教程网站

网站首页 > 技术文章 正文

SpringBoot整合Grpc实现跨语言RPC通讯

btikc 2024-10-10 04:54:10 技术文章 14 ℃ 0 评论

什么是gRPC

gRPC谷歌开源基于go语言的一个现代的开源高性能RPC框架,可以在任何环境中运行。它可以有效地连接数据中心内和跨数据中心的服务,并提供可插拔的支持,以实现负载平衡,跟踪,健康检查和身份验证。它还适用于分布式计算的最后一英里,用于将设备,移动应用程序和浏览器连接到后端服务。

简单的服务定义:使用Protocol Buffers定义您的服务,这是一个功能强大的二进制序列化工具集和语言.

跨语言和平台工作:自动为各种语言和平台的服务生成惯用的客户端和服务器存根,当然单纯的java语言之间也是可以的。

一般主要是Java和Go,PHP,Python之间通讯。

快速启动并扩展:使用单行安装运行时和开发环境,并使用框架每秒扩展到数百万个RPC

双向流媒体和集成的身份验证:基于http/2的传输的双向流和完全集成的可插拔身份验证

官网地址:https://www.grpc.io/

这是一个可以运行的例子,本文基于此增加了一些代码:https://codenotfound.com/grpc-java-example.html

这个例子使用的jar是grpc-spring-boot-starter@io.github.lognet

这个例子也可以参考:https://github.com/yidongnan/grpc-spring-boot-starter

不过这个例子使用的是另一个jar是grpc-spring-boot-starter@net.devh

说明:Thrift也可以实现跨语言的通讯,有人对此做了对比参考:开源RPC(gRPC/Thrift)框架性能评测

服务定义

与许多RPC系统一样,gRPC基于定义服务的思想,指定可以使用其参数和返回类型远程调用的方法。默认情况下,gRPC使用Protocol Buffers作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。如果需要,可以使用其他替代方案。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。

它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

数据序列化和反序列化

序列化:将数据结构或对象转换成二进制串的过程。

反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。

这里有人翻译了官方文档:Protocol Buffers官方文档(开发指南)

也可以看IBM的文档:Google Protocol Buffer 的使用和原理

https://github.com/protocolbuffers/protobuf

https://developers.google.com/protocol-buffers/docs/javatutorial

参考官网指南:

如您所见,语法类似于C ++或Java。让我们浏览文件的每个部分,看看它的作用。

该.proto文件以包声明开头,这有助于防止不同项目之间的命名冲突。在Java中,包名称用作Java包,除非您已经明确指定了ajava_package,就像我们在这里一样。即使您提供了ajava_package,您仍应定义一个法线package,以避免在Protocol Buffers名称空间和非Java语言中发生名称冲突。

在包声明之后,您可以看到两个特定于Java的选项: java_package和java_outer_classname。 java_package指定生成的类应该以什么Java包名称存在。如果没有明确指定它,它只是匹配package声明给出的包名,但这些名称通常不是合适的Java包名(因为它们通常不以域名开头)。该java_outer_classname选项定义应包含此文件中所有类的类名。如果你没有java_outer_classname明确地给出,它将通过将文件名转换为camel case来生成。例如,默认情况下,“my_proto.proto”将使用“MyProto”作为外部类名。

接下来,您有消息定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可以作为字段类型,

包括bool,int32,float,double,和string。您还可以使用其他消息类型作为字段类型在消息中添加更多结构 - 在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义嵌套在其他消息中的消息类型 - 如您所见,PhoneNumber类型在内部定义Person。enum如果您希望其中一个字段具有预定义的值列表之一,您也可以定义类型 - 在此您要指定电话号码可以是其中之一MOBILE,HOME或者WORK。

每个元素上的“= 1”,“= 2”标记标识该字段在二进制编码中使用的唯一“标记”。标签号1-15需要少于一个字节来编码而不是更高的数字,因此作为优化,您可以决定将这些标签用于常用或重复的元素,将标签16和更高版本留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标记号,因此重复字段特别适合此优化。

必须使用以下修饰符之一注释每个字段:

  • required:必须提供该字段的值,否则该消息将被视为“未初始化”。尝试构建一个未初始化的消息将抛出一个RuntimeException。解析未初始化的消息将抛出一个IOException。除此之外,必填字段的行为与可选字段完全相同。
  • optional:该字段可能已设置,也可能未设置。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像我们type在示例中为电话号码所做的那样。否则,使用系统默认值:数字类型为0,字符串为空字符串,bools为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其中没有设置其字段。调用访问器以获取尚未显式设置的可选(或必需)字段的值始终返回该字段的默认值。
  • repeated:该字段可以重复任意次数(包括零)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。

永远是必需的 你应该非常小心地将字段标记为required。如果您希望在某个时刻停止写入或发送必填字段,则将字段更改为可选字段会有问题 - 旧读者会认为没有此字段的邮件不完整,可能会无意中拒绝或丢弃它们。您应该考虑为缓冲区编写特定于应用程序的自定义验证例程。谷歌的一些工程师得出的结论是,使用required弊大于利; 他们更喜欢只使用optional和repeated。但是,这种观点并不普遍。

您.proto可以在Protocol Buffer Language Guide中找到编写文件的完整指南- 包括所有可能的字段类型。不要去寻找类继承类似的工具,但协议缓冲区不会这样做。

如果你还不是很理解,就只要参考下面这个例子就行了。

我们将使用以下工具/框架:

  • gRPC 1.16
  • Spring Boot 2.1
  • Maven 3.5

我们的项目具有以下目录结构:

使用Protocol Buffers定义服务

先看proto文件

syntax = "proto3";

option java_multiple_files = true;
package com.codenotfound.grpc.helloworld;

message Person {
  string first_name = 1;
  string last_name = 2;
}

message Greeting {
  string message = 1;
}

message A1 {
  int32 a = 1;
  int32 b = 2;
}

message A2 {
  int32 message = 1;
}

service HelloWorldService {
  rpc sayHello (Person) returns (Greeting);
  rpc addOperation (A1) returns (A2);
}

注意:A1,A2是我定义的,其实就是定义入参出参,1和2那是表示是第几个参数而已,数据类型有string和int32。

Maven设置

我们使用Maven构建并运行我们的示例。

下面显示的是POM文件中Maven项目的XML表示。它包含编译和运行示例所需的依赖项。

为了配置和公开Hello World gRPC服务端点,我们将使用Spring Boot项目。

为了便于管理不同的Spring依赖项,使用了Spring Boot Starters。这些是一组方便的依赖项描述符,您可以在应用程序中包含这些描述符。

我们包含spring-boot-starter-web依赖项,该依赖项自动设置将托管我们的gRPC服务端点的嵌入式Apache Tomcat。

在spring-boot-starter-test包括用于包括测试启动的应用程序的依赖关系的JUnit,Hamcrest和的Mockito。

用于gRPC框架的Spring启动启动程序自动配置并运行嵌入式gRPC服务器,@GRpcService启用Beans作为Spring Boot应用程序的一部分。启动器支持Spring Boot版本1.5.X和2.XX我们通过包含grpc-spring-boot-starter依赖项来启用它。

协议缓冲区支持许多编程语言中生成的代码。本教程重点介绍Java。

有多种方法可以生成基于protobuf的代码,在本例中,我们将使用grobc-java GitHub页面上记录的protobuf-maven-plugin。

我们还包括os-maven-plugin扩展,它可以生成各种有用的平台相关项目属性。由于协议缓冲区编译器是本机代码,因此需要此信息。换句话说,protobuf-maven-plugin需要为正在运行的平台获取正确的编译器。

最后,插件部分包括spring-boot-maven-plugin。这允许我们构建一个可运行的超级jar。这是执行和传输代码的便捷方式。此外,该插件允许我们通过Maven命令启动示例。

项目的pom文件:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.codenotfound</groupId>
  <artifactId>grpc-java-hello-world</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>grpc-java-hello-world</name>
  <description>gRPC Java Example</description>
  <url>https://codenotfound.com/grpc-java-example.html</url>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <grpc-spring-boot-starter.version>3.0.0</grpc-spring-boot-starter.version>
    <os-maven-plugin.version>1.6.1</os-maven-plugin.version>
    <protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.github.lognet</groupId>
      <artifactId>grpc-spring-boot-starter</artifactId>
      <version>${grpc-spring-boot-starter.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <extensions>
      <extension>
        <groupId>kr.motd.maven</groupId>
        <artifactId>os-maven-plugin</artifactId>
        <version>${os-maven-plugin.version}</version>
      </extension>
    </extensions>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.xolstice.maven.plugins</groupId>
        <artifactId>protobuf-maven-plugin</artifactId>
        <version>${protobuf-maven-plugin.version}</version>
        <configuration>
          <protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
          <pluginId>grpc-java</pluginId>
          <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.16.1:exe:${os.detected.classifier}</pluginArtifact>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>compile-custom</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

注意:必须使用这个编译compile工具生成特定语言(比如我们这里是java)的执行代码。

手动执行以下Maven命令,应在target / generated-sources / protobuf /下生成不同的消息和服务类。

mvn compile

也可以使用IDE编译。

编译执行:

会生成编译后的文件:

注意上面的文件是自动编译生成的,不是你自己写的!

Spring Boot安装程序

创建一个SpringGrpcApplication包含一个main()方法,该方法使用Spring Boot的SpringApplication.run()方法来引导应用程序。

package com.codenotfound;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringGrpcApplication {

  public static void main(String[] args) {
    SpringApplication.run(SpringGrpcApplication.class, args);
  }
}

创建服务器

定义Service,我增加了一个加法运算方法,需要注意的是输入,输出参数都写在方法定义里。

服务实现在HelloWorldServiceImplPOJO中定义,该POJO实现HelloWorldServiceImplBase从HelloWorld.proto文件生成的类。

我们覆盖该sayHello()方法并Greeting根据Person请求中传递的名字和姓氏生成响应。

请注意,响应是一个StreamObserver对象。换句话说,该服务默认是异步的。在接收响应时是否要阻止是客户的决定,我们将在下面进一步了解。

我们使用响应观察者的onNext()方法返回Greeting,然后调用响应观察者的onCompleted()方法告诉gRPC我们已经完成了写响应。

该HelloWorldServiceImplPOJO标注有@GRpcService其自动配置到端口露出指定GRPC服务默认端口6565。

request:入参

responseObserver:出参

格式按照标准

package com.codenotfound.grpc;

import com.codenotfound.grpc.helloworld.A1;
import com.codenotfound.grpc.helloworld.A2;
import org.lognet.springboot.grpc.GRpcService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.codenotfound.grpc.helloworld.Greeting;
import com.codenotfound.grpc.helloworld.HelloWorldServiceGrpc;
import com.codenotfound.grpc.helloworld.Person;
import io.grpc.stub.StreamObserver;

@GRpcService
public class HelloWorldServiceImpl
        extends HelloWorldServiceGrpc.HelloWorldServiceImplBase {

    private static final Logger LOGGER
            = LoggerFactory.getLogger(HelloWorldServiceImpl.class);

    @Override
    public void sayHello(Person request,
            StreamObserver<Greeting> responseObserver) {
        LOGGER.info("server received {}", request);

        String message = "Hello " + request.getFirstName() + " "
                + request.getLastName() + "!";
        Greeting greeting
                = Greeting.newBuilder().setMessage(message).build();
        LOGGER.info("server responded {}", greeting);
        System.out.println("message>>>" + message);
        responseObserver.onNext(greeting);
        responseObserver.onCompleted();
    }

    @Override
    public void addOperation(A1 request,
            StreamObserver<A2> responseObserver) {
        LOGGER.info("server received {}", request);

        int message = request.getA() + request.getB();
        A2 a2 = A2.newBuilder().setMessage(message).build();
        LOGGER.info("server responded {}", a2);
        System.out.println("message>>>" + message);
        responseObserver.onNext(a2);
        responseObserver.onCompleted();
    }
}

服务端使用了@GRpcService注解.

也可以使用这个jar注解@GrpcService就可以,代码和前面一种一致的

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

https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/README-zh.md

源码在这里:

你可以自定义端口:

grpc:
    port: 9090

创建客户端

下面是客户端代码,这里为了简单服务端和客户端写一个项目,实际开发肯定是分开,不然也就没必要用Grpc这么麻烦了。

客户端代码在HelloWorldClient类中指定。

@Component如果启用了自动组件扫描,我们将使用Spring 注释客户端,这将导致Spring自动创建并将下面的bean导入容器(将@SpringBootApplication注释添加到主SpringWsApplication类等同于使用@ComponentScan)。

要调用gRPC服务方法,我们首先需要创建一个stub。

有两种类型的stub可用:

  • 一个阻塞/同步stub,将等待服务器响应
  • 一个非阻塞/异步stub使非阻塞调用到服务器,其中,所述响应是异步返回。

在此示例中,我们将实现阻塞stub。

为了传输消息,gRPC使用http/2和其间的一些抽象层。这种复杂性隐藏在MessageChannel处理连接的背后。

一般建议是为每个应用程序使用一个通道并在服务stub之间共享它。

我们使用一个init()带注释的方法@PostConstruct,以便MessageChannel在bean初始化之后构建一个新的权限。然后使用该通道创建helloWorldServiceBlockingStub。

gRPC默认使用安全连接机制,如TLS。因为这是一个简单的开发测试将使用usePlaintext(),以避免必须配置不同的安全工件,如密钥/信任存储。

该sayHello()方法使用Builder模式创建Person对象,我们在其上设置'firstname'和'lastname'输入参数。

该helloWorldServiceBlockingStub则用来发送走向世界您好GRPC服务的请求。结果是一个Greeting对象,我们从中返回包含消息。

package com.codenotfound.grpc;

import com.codenotfound.grpc.helloworld.A1;
import com.codenotfound.grpc.helloworld.A2;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.codenotfound.grpc.helloworld.Greeting;
import com.codenotfound.grpc.helloworld.HelloWorldServiceGrpc;
import com.codenotfound.grpc.helloworld.Person;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

@Component
public class HelloWorldClient {

    private static final Logger LOGGER
            = LoggerFactory.getLogger(HelloWorldClient.class);

    private HelloWorldServiceGrpc.HelloWorldServiceBlockingStub helloWorldServiceBlockingStub;

    @PostConstruct
    private void init() {
        ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext().build();
        helloWorldServiceBlockingStub = HelloWorldServiceGrpc.newBlockingStub(managedChannel);
    }

    public String sayHello(String firstName, String lastName) {
        Person person = Person.newBuilder().setFirstName(firstName).setLastName(lastName).build();
        LOGGER.info("client sending {}", person);

        Greeting greeting = helloWorldServiceBlockingStub.sayHello(person);
        LOGGER.info("client received {}", greeting);

        return greeting.getMessage();
    }

    public int addOperation(int a, int b) {
        A1 a1 = A1.newBuilder().setA(a).setB(b).build();
        A2 a2 = helloWorldServiceBlockingStub.addOperation(a1);
        return a2.getMessage();
    }
}

注意如果使用自定义端口需要修改这个,默认是6565,保持和你修改的配置文件一致,或者你不配置用默认的就行:

gRPC测试用例

package com.codenotfound;

import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import com.codenotfound.grpc.HelloWorldClient;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringGrpcApplicationTests {

  @Autowired
  private HelloWorldClient helloWorldClient;

  @Test
  public void testSayHello() {
    assertThat(helloWorldClient.sayHello("Grpc", "Java")).isEqualTo("Hello Grpc Java!");
    assertThat(helloWorldClient.addOperation(1, 2)).isEqualTo(3);
  }
}

2个断言:

一个是原始的就是字符串输出,第二个是我增加的做个简单的加法运算1+2=3就对了。

运行测试用例:

故意修改为和4比较结果,报错就对了。

总结

建议先跑完整的例子,不要陷入grpc太深。

定义好.proto,再生成对应编译文件,再写实现类,定义服务端,使用客户端调用。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表