SpringBoot整合Grpc实现跨语言RPC通讯
什么是gRPC
gRPC是谷歌開源的基于go語(yǔ)言的一個(gè)現(xiàn)代的開源高性能RPC框架,可以在任何環(huán)境中運(yùn)行。它可以有效地連接數(shù)據(jù)中心內(nèi)和跨數(shù)據(jù)中心的服務(wù),并提供可插拔的支持,以實(shí)現(xiàn)負(fù)載平衡,跟蹤,健康檢查和身份驗(yàn)證。它還適用于分布式計(jì)算的最后一英里,用于將設(shè)備,移動(dòng)應(yīng)用程序和瀏覽器連接到后端服務(wù)。
簡(jiǎn)單的服務(wù)定義:使用Protocol Buffers定義您的服務(wù),這是一個(gè)功能強(qiáng)大的二進(jìn)制序列化工具集和語(yǔ)言.
跨語(yǔ)言和平臺(tái)工作:自動(dòng)為各種語(yǔ)言和平臺(tái)的服務(wù)生成慣用的客戶端和服務(wù)器存根,當(dāng)然單純的java語(yǔ)言之間也是可以的。
一般主要是Java和Go,PHP,Python之間通訊。
快速啟動(dòng)并擴(kuò)展:使用單行安裝運(yùn)行時(shí)和開發(fā)環(huán)境,并使用框架每秒擴(kuò)展到數(shù)百萬(wàn)個(gè)RPC
雙向流媒體和集成的身份驗(yàn)證:基于http/2的傳輸?shù)碾p向流和完全集成的可插拔身份驗(yàn)證
官網(wǎng)地址:https://www.grpc.io/
這是一個(gè)可以運(yùn)行的例子,本文基于此增加了一些代碼:https://codenotfound.com/grpc-java-example.html
這個(gè)例子使用的jar是grpc-spring-boot-starter@io.github.lognet
?
這個(gè)例子也可以參考:https://github.com/yidongnan/grpc-spring-boot-starter
不過這個(gè)例子使用的是另一個(gè)jar是grpc-spring-boot-starter@net.devh
?
說明:Thrift也可以實(shí)現(xiàn)跨語(yǔ)言的通訊,有人對(duì)此做了對(duì)比參考:開源RPC(gRPC/Thrift)框架性能評(píng)測(cè)
?
服務(wù)定義
與許多RPC系統(tǒng)一樣,gRPC基于定義服務(wù)的思想,指定可以使用其參數(shù)和返回類型遠(yuǎn)程調(diào)用的方法。默認(rèn)情況下,gRPC使用Protocol Buffers作為接口定義語(yǔ)言(IDL)來描述服務(wù)接口和有效負(fù)載消息的結(jié)構(gòu)。如果需要,可以使用其他替代方案。
Protocol Buffers?是一種輕便高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式,可以用于結(jié)構(gòu)化數(shù)據(jù)串行化,或者說序列化。
它很適合做數(shù)據(jù)存儲(chǔ)或?RPC?數(shù)據(jù)交換格式。可用于通訊協(xié)議、數(shù)據(jù)存儲(chǔ)等領(lǐng)域的語(yǔ)言無關(guān)、平臺(tái)無關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)格式。
數(shù)據(jù)序列化和反序列化
序列化: 將數(shù)據(jù)結(jié)構(gòu)或?qū)ο筠D(zhuǎn)換成二進(jìn)制串的過程。
反序列化:將在序列化過程中所生成的二進(jìn)制串轉(zhuǎn)換成數(shù)據(jù)結(jié)構(gòu)或者對(duì)象的過程。
這里有人翻譯了官方文檔:Protocol Buffers官方文檔(開發(fā)指南)
也可以看IBM的文檔:Google Protocol Buffer 的使用和原理
https://github.com/protocolbuffers/protobuf
https://developers.google.com/protocol-buffers/docs/javatutorial
參考官網(wǎng)指南:
如您所見,語(yǔ)法類似于C ++或Java。讓我們?yōu)g覽文件的每個(gè)部分,看看它的作用。
該.proto文件以包聲明開頭,這有助于防止不同項(xiàng)目之間的命名沖突。在Java中,包名稱用作Java包,除非您已經(jīng)明確指定了a?java_package,就像我們?cè)谶@里一樣。即使您提供了a?java_package,您仍應(yīng)定義一個(gè)法線package,以避免在Protocol Buffers名稱空間和非Java語(yǔ)言中發(fā)生名稱沖突。
在包聲明之后,您可以看到兩個(gè)特定于Java的選項(xiàng):?java_package和java_outer_classname。?java_package指定生成的類應(yīng)該以什么Java包名稱存在。如果沒有明確指定它,它只是匹配package聲明給出的包名,但這些名稱通常不是合適的Java包名(因?yàn)樗鼈兺ǔ2灰杂蛎_頭)。該java_outer_classname選項(xiàng)定義應(yīng)包含此文件中所有類的類名。如果你沒有java_outer_classname明確地給出,它將通過將文件名轉(zhuǎn)換為camel case來生成。例如,默認(rèn)情況下,“my_proto.proto”將使用“MyProto”作為外部類名。
接下來,您有消息定義。消息只是包含一組類型字段的聚合。許多標(biāo)準(zhǔn)的簡(jiǎn)單數(shù)據(jù)類型都可以作為字段類型,
包括bool,int32,float,double,和string。您還可以使用其他消息類型作為字段類型在消息中添加更多結(jié)構(gòu) - 在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定義嵌套在其他消息中的消息類型 -?? 如您所見,PhoneNumber類型在內(nèi)部定義Person。enum如果您希望其中一個(gè)字段具有預(yù)定義的值列表之一,您也可以定義類型 - 在此您要指定電話號(hào)碼可以是其中之一MOBILE,HOME或者WORK。
每個(gè)元素上的“= 1”,“= 2”標(biāo)記標(biāo)識(shí)該字段在二進(jìn)制編碼中使用的唯一“標(biāo)記”。標(biāo)簽號(hào)1-15需要少于一個(gè)字節(jié)來編碼而不是更高的數(shù)字,因此作為優(yōu)化,您可以決定將這些標(biāo)簽用于常用或重復(fù)的元素,將標(biāo)簽16和更高版本留給不太常用的可選元素。重復(fù)字段中的每個(gè)元素都需要重新編碼標(biāo)記號(hào),因此重復(fù)字段特別適合此優(yōu)化。
必須使用以下修飾符之一注釋每個(gè)字段:
required:必須提供該字段的值,否則該消息將被視為“未初始化”。嘗試構(gòu)建一個(gè)未初始化的消息將拋出一個(gè)RuntimeException。解析未初始化的消息將拋出一個(gè)IOException。除此之外,必填字段的行為與可選字段完全相同。optional:該字段可能已設(shè)置,也可能未設(shè)置。如果未設(shè)置可選字段值,則使用默認(rèn)值。對(duì)于簡(jiǎn)單類型,您可以指定自己的默認(rèn)值,就像我們type在示例中為電話號(hào)碼所做的那樣。否則,使用系統(tǒng)默認(rèn)值:數(shù)字類型為0,字符串為空字符串,bools為false。對(duì)于嵌入式消息,默認(rèn)值始終是消息的“默認(rèn)實(shí)例”或“原型”,其中沒有設(shè)置其字段。調(diào)用訪問器以獲取尚未顯式設(shè)置的可選(或必需)字段的值始終返回該字段的默認(rèn)值。repeated:該字段可以重復(fù)任意次數(shù)(包括零)。重復(fù)值的順序?qū)⒈A粼趨f(xié)議緩沖區(qū)中。將重復(fù)字段視為動(dòng)態(tài)大小的數(shù)組。
永遠(yuǎn)是必需的?你應(yīng)該非常小心地將字段標(biāo)記為required。如果您希望在某個(gè)時(shí)刻停止寫入或發(fā)送必填字段,則將字段更改為可選字段會(huì)有問題 - 舊讀者會(huì)認(rèn)為沒有此字段的郵件不完整,可能會(huì)無意中拒絕或丟棄它們。您應(yīng)該考慮為緩沖區(qū)編寫特定于應(yīng)用程序的自定義驗(yàn)證例程。谷歌的一些工程師得出的結(jié)論是,使用required弊大于利;?他們更喜歡只使用optional和repeated。但是,這種觀點(diǎn)并不普遍。
您.proto可以在Protocol Buffer Language Guide中找到編寫文件的完整指南- 包括所有可能的字段類型。不要去尋找類繼承類似的工具,但協(xié)議緩沖區(qū)不會(huì)這樣做。
?
如果你還不是很理解,就只要參考下面這個(gè)例子就行了。
我們將使用以下工具/框架:
- gRPC 1.16
- Spring Boot 2.1
- Maven 3.5
我們的項(xiàng)目具有以下目錄結(jié)構(gòu):
?
使用Protocol Buffers定義服務(wù)
先看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是我定義的,其實(shí)就是定義入?yún)⒊鰠?#xff0c;1和2那是表示是第幾個(gè)參數(shù)而已,數(shù)據(jù)類型有string和int32。
?
Maven設(shè)置
我們使用Maven構(gòu)建并運(yùn)行我們的示例。
下面顯示的是POM文件中Maven項(xiàng)目的XML表示。它包含編譯和運(yùn)行示例所需的依賴項(xiàng)。
為了配置和公開Hello World gRPC服務(wù)端點(diǎn),我們將使用Spring Boot項(xiàng)目。
為了便于管理不同的Spring依賴項(xiàng),使用了Spring Boot Starters。這些是一組方便的依賴項(xiàng)描述符,您可以在應(yīng)用程序中包含這些描述符。
我們包含spring-boot-starter-web依賴項(xiàng),該依賴項(xiàng)自動(dòng)設(shè)置將托管我們的gRPC服務(wù)端點(diǎn)的嵌入式Apache Tomcat。
在spring-boot-starter-test包括用于包括測(cè)試啟動(dòng)的應(yīng)用程序的依賴關(guān)系的JUnit,Hamcrest和的Mockito。
用于gRPC框架的Spring啟動(dòng)啟動(dòng)程序自動(dòng)配置并運(yùn)行嵌入式gRPC服務(wù)器,@GRpcService啟用Beans作為Spring Boot應(yīng)用程序的一部分。啟動(dòng)器支持Spring Boot版本1.5.X和2.XX我們通過包含grpc-spring-boot-starter依賴項(xiàng)來啟用它。
協(xié)議緩沖區(qū)支持許多編程語(yǔ)言中生成的代碼。本教程重點(diǎn)介紹Java。
有多種方法可以生成基于protobuf的代碼,在本例中,我們將使用grobc-java?GitHub頁(yè)面上記錄的protobuf-maven-plugin。
我們還包括os-maven-plugin擴(kuò)展,它可以生成各種有用的平臺(tái)相關(guān)項(xiàng)目屬性。由于協(xié)議緩沖區(qū)編譯器是本機(jī)代碼,因此需要此信息。換句話說,protobuf-maven-plugin需要為正在運(yùn)行的平臺(tái)獲取正確的編譯器。
最后,插件部分包括spring-boot-maven-plugin。這允許我們構(gòu)建一個(gè)可運(yùn)行的超級(jí)jar。這是執(zhí)行和傳輸代碼的便捷方式。此外,該插件允許我們通過Maven命令啟動(dòng)示例。
項(xiàng)目的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>
注意:必須使用這個(gè)編譯compile工具生成特定語(yǔ)言(比如我們這里是java)的執(zhí)行代碼。
手動(dòng)執(zhí)行以下Maven命令,應(yīng)在target / generated-sources / protobuf /下生成不同的消息和服務(wù)類。
mvn compile
也可以使用IDE編譯。
編譯執(zhí)行:
會(huì)生成編譯后的文件:
注意上面的文件是自動(dòng)編譯生成的,不是你自己寫的!
?
Spring Boot安裝程序
創(chuàng)建一個(gè)SpringGrpcApplication包含一個(gè)main()方法,該方法使用Spring Boot的SpringApplication.run()方法來引導(dǎo)應(yīng)用程序。
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);}
}
?
創(chuàng)建服務(wù)器
定義Service,我增加了一個(gè)加法運(yùn)算方法,需要注意的是輸入,輸出參數(shù)都寫在方法定義里。
服務(wù)實(shí)現(xiàn)在HelloWorldServiceImplPOJO中定義,該P(yáng)OJO實(shí)現(xiàn)HelloWorldServiceImplBase從HelloWorld.proto文件生成的類。
我們覆蓋該sayHello()方法并Greeting根據(jù)Person請(qǐng)求中傳遞的名字和姓氏生成響應(yīng)。
請(qǐng)注意,響應(yīng)是一個(gè)
StreamObserver對(duì)象。換句話說,該服務(wù)默認(rèn)是異步的。在接收響應(yīng)時(shí)是否要阻止是客戶的決定,我們將在下面進(jìn)一步了解。
我們使用響應(yīng)觀察者的onNext()方法返回Greeting,然后調(diào)用響應(yīng)觀察者的onCompleted()方法告訴gRPC我們已經(jīng)完成了寫響應(yīng)。
該HelloWorldServiceImplPOJO標(biāo)注有@GRpcService其自動(dòng)配置到端口露出指定GRPC服務(wù)默認(rèn)端口6565。
request:入?yún)?/p>
responseObserver:出參
格式按照標(biāo)準(zhǔn)
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();}
}
服務(wù)端使用了@GRpcService注解.
?
也可以使用這個(gè)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
?
創(chuàng)建客戶端
下面是客戶端代碼,這里為了簡(jiǎn)單服務(wù)端和客戶端寫一個(gè)項(xiàng)目,實(shí)際開發(fā)肯定是分開,不然也就沒必要用Grpc這么麻煩了。?
客戶端代碼在HelloWorldClient類中指定。
@Component如果啟用了自動(dòng)組件掃描,我們將使用Spring?注釋客戶端,這將導(dǎo)致Spring自動(dòng)創(chuàng)建并將下面的bean導(dǎo)入容器(將@SpringBootApplication注釋添加到主SpringWsApplication類等同于使用@ComponentScan)。
要調(diào)用gRPC服務(wù)方法,我們首先需要?jiǎng)?chuàng)建一個(gè)stub。
有兩種類型的stub可用:
- 一個(gè)阻塞/同步stub,將等待服務(wù)器響應(yīng)
- 一個(gè)非阻塞/異步stub使非阻塞調(diào)用到服務(wù)器,其中,所述響應(yīng)是異步返回。
在此示例中,我們將實(shí)現(xiàn)阻塞stub。
為了傳輸消息,gRPC使用http/2和其間的一些抽象層。這種復(fù)雜性隱藏在MessageChannel處理連接的背后。
一般建議是為每個(gè)應(yīng)用程序使用一個(gè)通道并在服務(wù)stub之間共享它。
我們使用一個(gè)init()帶注釋的方法@PostConstruct,以便MessageChannel在bean初始化之后構(gòu)建一個(gè)新的權(quán)限。然后使用該通道創(chuàng)建helloWorldServiceBlockingStub。
gRPC默認(rèn)使用安全連接機(jī)制,如TLS。因?yàn)檫@是一個(gè)簡(jiǎn)單的開發(fā)測(cè)試將使用
usePlaintext(),以避免必須配置不同的安全工件,如密鑰/信任存儲(chǔ)。
該sayHello()方法使用Builder模式創(chuàng)建Person對(duì)象,我們?cè)谄渖显O(shè)置'firstname'和'lastname'輸入?yún)?shù)。
該helloWorldServiceBlockingStub則用來發(fā)送走向世界您好GRPC服務(wù)的請(qǐng)求。結(jié)果是一個(gè)Greeting對(duì)象,我們從中返回包含消息。
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();}
}
注意如果使用自定義端口需要修改這個(gè),默認(rèn)是6565,保持和你修改的配置文件一致,或者你不配置用默認(rèn)的就行:
?
gRPC測(cè)試用例:
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個(gè)斷言:
一個(gè)是原始的就是字符串輸出,第二個(gè)是我增加的做個(gè)簡(jiǎn)單的加法運(yùn)算1+2=3就對(duì)了。
?
運(yùn)行測(cè)試用例:
?
故意修改為和4比較結(jié)果,報(bào)錯(cuò)就對(duì)了。
總結(jié):建議先跑完整的例子,不要陷入grpc太深。
定義好.proto,再生成對(duì)應(yīng)編譯文件,再寫實(shí)現(xiàn)類,定義服務(wù)端,使用客戶端調(diào)用。?
總結(jié)
以上是生活随笔為你收集整理的SpringBoot整合Grpc实现跨语言RPC通讯的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《梦仙》第三十四句是什么
- 下一篇: Java泛型使用需要小心