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是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包,除非您已經明確指定了a?java_package,就像我們在這里一樣。即使您提供了a?java_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 HelloWorldServiceImplextends HelloWorldServiceGrpc.HelloWorldServiceImplBase {private static final Logger LOGGER= LoggerFactory.getLogger(HelloWorldServiceImpl.class);@Overridepublic 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();}@Overridepublic 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;@PostConstructprivate 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 {@Autowiredprivate HelloWorldClient helloWorldClient;@Testpublic 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,再生成對應編譯文件,再寫實現類,定義服務端,使用客戶端調用。?
總結
以上是生活随笔為你收集整理的SpringBoot整合Grpc实现跨语言RPC通讯的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《梦仙》第三十四句是什么
- 下一篇: Java泛型使用需要小心