SpringBoot整合Grpc實現跨語言RPC通訊

什麼是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是[email protected]

這個例子也可以參考:
https://github.com/yidongnan/grpc-spring-boot-starter

不過這個例子使用的是另一個jar是[email protected]

說明:Thrift也可以實現跨語言的通訊,有人對此做了對比參考:開源RPC(gRPC/Thrift)框架性能評測

服務定義

與許多RPC系統一樣,gRPC基於定義服務的思想,指定可以使用其參數和返回類型遠程調用的方法。默認情況下,gRPC使用Protocol Buffers作爲接口定義語言(IDL)來描述服務接口和有效負載消息的結構。如果需要,可以使用其他替代方案。

Protocol Buffers 是一種輕便高效的結構化數據存儲格式,可以用於結構化數據串行化,或者說序列化。

它很適合做數據存儲或 RPC 數據交換格式。可用於通訊協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。

數據序列化和反序列化

序列化:將數據結構或對象轉換成二進制串的過程。

反序列化:將在序列化過程中所生成的二進制串轉換成數據結構或者對象的過程。

SpringBoot整合Grpc實現跨語言RPC通訊

 

這裏有人翻譯了官方文檔:Protocol Buffers官方文檔(開發指南)

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

https://github.com/protocolbuffers/protobuf

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

參考官網指南:

SpringBoot整合Grpc實現跨語言RPC通訊

 

如您所見,語法類似於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

我們的項目具有以下目錄結構:

SpringBoot整合Grpc實現跨語言RPC通訊

 

使用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編譯。

編譯執行:

SpringBoot整合Grpc實現跨語言RPC通訊

 

會生成編譯後的文件:

SpringBoot整合Grpc實現跨語言RPC通訊

 

注意上面的文件是自動編譯生成的,不是你自己寫的!

SpringBoot整合Grpc實現跨語言RPC通訊

 

 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

源碼在這裏:

SpringBoot整合Grpc實現跨語言RPC通訊

 

你可以自定義端口:

grpc:
    port: 9090

SpringBoot整合Grpc實現跨語言RPC通訊

 

 創建客戶端

下面是客戶端代碼,這裏爲了簡單服務端和客戶端寫一個項目,實際開發肯定是分開,不然也就沒必要用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,保持和你修改的配置文件一致,或者你不配置用默認的就行:

SpringBoot整合Grpc實現跨語言RPC通訊

 

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就對了。

運行測試用例:

SpringBoot整合Grpc實現跨語言RPC通訊

 

SpringBoot整合Grpc實現跨語言RPC通訊

 

故意修改爲和4比較結果,報錯就對了。

SpringBoot整合Grpc實現跨語言RPC通訊

 

總結

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

定義好.proto,再生成對應編譯文件,再寫實現類,定義服務端,使用客戶端調用。

推薦閱讀:

阿里P9架構師120分鐘帶你掌握線程池,不在爲線程而煩惱​

不懂算法怎麼去字節等大廠面試?左程雲大神聯合馬士兵大佬120分鐘帶你掌握算法底層

馬士兵教育:Spring源碼實戰全集,資深架構師帶你搞懂Spring源碼底層從入門到入墳

四十歲的Java程序員活該被淘汰?馬士兵給所有Java程序員的忠告,告知當代應屆生進大廠的祕訣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章