gRPC從入門到放棄之概述與實(shí)戰(zhàn)
?微服務(wù)如火如荼的當(dāng)下,各種服務(wù)框架層出不窮。Dubbo、SpringCloud在國(guó)內(nèi)Java后端微服務(wù)領(lǐng)域目前占據(jù)大部分份額。
但是隨著云原生愈發(fā)普及,具備跨語(yǔ)言、高性能特性的RPC通信框架橫空出世,其中g(shù)RPC與Thrift是其中的佼佼者。
?
本文我們將視角集中在gRPC這RPC框架。
?gRPC 是Google開(kāi)源的高性能、通用的RPC框架??蛻舳伺c服務(wù)端約定接口調(diào)用, 可以在各種環(huán)境中運(yùn)行,具有跨語(yǔ)言特性, 適合構(gòu)建分布式、微服務(wù)應(yīng)用。
?
個(gè)人認(rèn)為,gRPC最為殺手锏的特性就是“跨語(yǔ)言”,其次才是高性能。
它的跨語(yǔ)言特性體現(xiàn)在,通過(guò)定義IDL(接口定義語(yǔ)言),隔離了不同編程語(yǔ)言之間的差異,對(duì)IDL進(jìn)行編譯后,生成對(duì)應(yīng)編程語(yǔ)言的nativeCode,讓開(kāi)發(fā)者能夠集中注意在實(shí)現(xiàn)業(yè)務(wù)需求上,而不需要花費(fèi)額外的精力在語(yǔ)言層面上。
?官網(wǎng)的一張圖能夠很好地體現(xiàn)這個(gè)特點(diǎn)
?

gRPC特性介紹
?gRPC具備以下特性
?
性能優(yōu)異:
它采用Proto Buffer作序列化傳輸媒介, 對(duì)比JSON與XML有數(shù)倍提升。
采用HTTP2協(xié)議, 對(duì)頭部信息(header)壓縮, 對(duì)連接進(jìn)行復(fù)用,能夠減少TCP連接次數(shù)。
針對(duì)Java語(yǔ)言,gRPC底層采用Netty作為NIO處理框架, 性能強(qiáng)勁。
多語(yǔ)言支持,多客戶端接入, 支持C++/GO/Ruby等語(yǔ)言。
支持負(fù)載均衡、跟蹤、健康檢查和認(rèn)證。
gRPC的線程模型是怎樣的?
?筆者主力語(yǔ)言為Java,因此我們講解也集中在Java的實(shí)現(xiàn)上。
?
gRPC的Java實(shí)現(xiàn),服務(wù)端底層采用了Netty作為核心處理框架,因此其線程模型核心也是遵循了 Netty 的線程分工原則。
協(xié)議層消息的接收和編解碼由 Netty 的 I/O(NioEventLoop) 線程負(fù)責(zé), 應(yīng)用層的處理由應(yīng)用線程負(fù)責(zé),防止由于應(yīng)用處理耗時(shí)而阻塞 Netty 的 I/O 線程。
?Netty線程模型是基于NIO的Reactor模式。
?

?Netty是基于NIO構(gòu)建的通信框架。
?
在 Java NIO 中最重要的概念就是多路復(fù)用器 Selector,它是 Java NIO 編程的基礎(chǔ)。Selector提供了選擇已經(jīng)就緒的任務(wù)的能力。
?簡(jiǎn)單來(lái)講,Selector 會(huì)不斷地輪詢注冊(cè)在其上的 Channel,如果某個(gè) Channel 上面有新的 TCP 連接接入、讀和寫事件,這個(gè) Channel 就處于就緒狀態(tài),會(huì)被 Selector 輪詢出來(lái),然后通過(guò)SelectionKey 可以獲取就緒 Channel 的集合,進(jìn)行后續(xù)的 I/O 操作。
?
一般來(lái)說(shuō),一個(gè) I/O 線程會(huì)聚合一個(gè) Selector,一個(gè) Selector 可以同時(shí)注冊(cè) N 個(gè) Channel, 這樣單個(gè)
I/O 線程就可以同時(shí)并發(fā)處理多個(gè)客戶端連接。
又由于 I/O 操作是非阻塞的,因此也不會(huì)受限于網(wǎng)絡(luò)速度和對(duì)方端點(diǎn)的處理時(shí)延,可靠性和效率都得到了很大提升。
gRPC客戶端如何請(qǐng)求服務(wù)端?
?作為RPC框架,至少有客戶端和服務(wù)端兩個(gè)角色,對(duì)于gRPC而言,客戶端請(qǐng)求服務(wù)端的調(diào)用過(guò)程如圖所示。
?

具體過(guò)程:
【Stub生成】客戶端生成Stub ,通過(guò)Stub發(fā)起 RPC遠(yuǎn)程服務(wù)調(diào)用 ; 【負(fù)載均衡】客戶端獲取服務(wù)端的地址信息(列表),使用默認(rèn)的 LoadBalancer 策略,選擇一個(gè)具體的 gRPC 服務(wù)端進(jìn)行調(diào)用; 【建立鏈接】如果客戶端與服務(wù)端之間沒(méi)有可用的連接,則創(chuàng)建 NettyClientTransport 和 NettyClientHandler,建立 HTTP/2 連接; 【客戶端請(qǐng)求序列化】對(duì)請(qǐng)求使用 PB(Protobuf)序列化,并通過(guò) HTTP/2 Stream 發(fā)送給 gRPC 服務(wù)端; 【服務(wù)端反序列化】服務(wù)端接收到響應(yīng)之后,使用 PB(Protobuf)做反序列化。 【請(qǐng)求響應(yīng)】回調(diào) GrpcFuture 的 set(Response) 方法,喚醒阻塞的客戶端調(diào)用線程,獲取 RPC 響應(yīng)數(shù)據(jù)。
gRPC性能到底有多強(qiáng)?
?沒(méi)有對(duì)比就沒(méi)有發(fā)言權(quán)。
?
在不同的操作系統(tǒng),不同請(qǐng)求數(shù)量下,對(duì)gRPC與Rest請(qǐng)求進(jìn)行對(duì)比的結(jié)論如下:

?官網(wǎng)也給出了權(quán)威性的比對(duì),具體比對(duì)gRPC+ProtoBuf與Http+JSON方式請(qǐng)求的差異。
?
官方性能比對(duì)結(jié)果

「實(shí)測(cè)結(jié)果顯示GRpc的通訊方案, 性能有32%的提升, 資源占用降低30%左右?!?/strong>
gRPC-Java 服務(wù)調(diào)用實(shí)戰(zhàn)
?按照慣例,我們提供一個(gè)簡(jiǎn)單的訂單案例展示gRPC在實(shí)際開(kāi)發(fā)中如何使用。
該案例在實(shí)際中的意義為:提供一個(gè)報(bào)價(jià)服務(wù),客戶端發(fā)送下單請(qǐng)求到服務(wù)端進(jìn)行報(bào)價(jià),服務(wù)端對(duì)用戶報(bào)價(jià)單進(jìn)行匯總計(jì)算,并提供查詢接口供客戶端查詢。
主要提供批量下單及查詢用戶訂單能力。
?
流程圖大致如下:

工程結(jié)構(gòu)如下:
|==>?grpc-demo??????父級(jí)工程,?管理依賴相關(guān)
?????|==>grpc-demo-sdk?????通用jar依賴,生成protobuf對(duì)象與gRPC?Service,供提供方與調(diào)用方使用
?????|==>grpc-server-demo??服務(wù)端,提供下單及訂單查詢服務(wù)
?????|==>grpc-client-demo??客戶端,負(fù)責(zé)調(diào)用gRPC服務(wù)
grpc-demo父工程
?父工程相對(duì)比較簡(jiǎn)單,管理了子工程及依賴版本。
?
<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.0modelVersion>
????<groupId>com.snowalkergroupId>
????<artifactId>grpc-demoartifactId>
????<version>1.0-SNAPSHOTversion>
????<modules>
????????<module>grpc-server-demomodule>
????????<module>grpc-client-demomodule>
????????<module>grpc-demo-sdkmodule>
????modules>
????<packaging>pompackaging>
????<properties>
????????<maven.compiler.source>8maven.compiler.source>
????????<maven.compiler.target>8maven.compiler.target>
????????<grpc-version>1.44.1grpc-version>
????properties>
????<dependencyManagement>
????????<dependencies>
????????????<dependency>
????????????????<groupId>io.grpcgroupId>
????????????????<artifactId>grpc-netty-shadedartifactId>
????????????????<version>${grpc-version}version>
????????????dependency>
????????????<dependency>
????????????????<groupId>io.grpcgroupId>
????????????????<artifactId>grpc-protobufartifactId>
????????????????<version>${grpc-version}version>
????????????dependency>
????????????<dependency>
????????????????<groupId>io.grpcgroupId>
????????????????<artifactId>grpc-stubartifactId>
????????????????<version>${grpc-version}version>
????????????dependency>
????????????<dependency>
????????????????<artifactId>lombokartifactId>
????????????????<groupId>org.projectlombokgroupId>
????????????????<version>1.18.22version>
????????????????<scope>providedscope>
????????????dependency>
????????dependencies>
????dependencyManagement>
project>
grpc-demo-sdk
?grpc-demo-sdk是較為關(guān)鍵的公共依賴,主要基于proto對(duì)服務(wù)進(jìn)行定義,生成java代碼并打包供服務(wù)提供方與消費(fèi)方使用。
?
pom.xml
?sdk的pom文件如下:
?
<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">
????<parent>
????????<artifactId>grpc-demoartifactId>
????????<groupId>com.snowalkergroupId>
????????<version>1.0-SNAPSHOTversion>
????parent>
????<name>grpc-demo-sdkname>
????<modelVersion>4.0.0modelVersion>
????<artifactId>grpc-demo-sdkartifactId>
????<packaging>jarpackaging>
????<properties>
????????<maven.compiler.source>8maven.compiler.source>
????????<maven.compiler.target>8maven.compiler.target>
????????<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
????properties>
????<dependencies>
????????<dependency>
????????????<groupId>io.grpcgroupId>
????????????<artifactId>grpc-netty-shadedartifactId>
????????dependency>
????????<dependency>
????????????<groupId>io.grpcgroupId>
????????????<artifactId>grpc-protobufartifactId>
????????dependency>
????????<dependency>
????????????<groupId>io.grpcgroupId>
????????????<artifactId>grpc-stubartifactId>
????????dependency>
????dependencies>
????<build>
????????<resources>
????????????<resource>
????????????????<directory>src/main/resourcesdirectory>
????????????????<excludes>
????????????????????<exclude>**exclude>
????????????????excludes>
????????????resource>
????????????<resource>
????????????????<directory>src/main/protodirectory>
????????????????<targetPath>prototargetPath>
????????????????<filtering>falsefiltering>
????????????resource>
????????resources>
????????<extensions>
????????????<extension>
????????????????<groupId>kr.motd.mavengroupId>
????????????????<artifactId>os-maven-pluginartifactId>
????????????????<version>1.6.2version>
????????????extension>
????????extensions>
????????<plugins>
????????????<plugin>
????????????????<groupId>org.xolstice.maven.pluginsgroupId>
????????????????<artifactId>protobuf-maven-pluginartifactId>
????????????????<version>0.6.1version>
????????????????<configuration>
????????????????????<protocArtifact>com.google.protobuf:protoc:3.19.1:exe:${os.detected.classifier}protocArtifact>
????????????????????<pluginId>grpc-javapluginId>
????????????????????<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.43.1:exe:${os.detected.classifier}pluginArtifact>
????????????????configuration>
????????????????<executions>
????????????????????<execution>
????????????????????????<goals>
????????????????????????????<goal>compilegoal>
????????????????????????????<goal>compile-customgoal>
????????????????????????goals>
????????????????????execution>
????????????????executions>
????????????plugin>
????????plugins>
????build>
project>
?重點(diǎn)關(guān)注一下plugin,我們使用protobuf-maven-plugin作為protobuf的編譯工具,有了該插件,我們?cè)趫?zhí)行mvn clean compile命令時(shí)便可以實(shí)現(xiàn)將proto編譯為java代碼的目的。
同樣,執(zhí)行mvn clean package命令可以實(shí)現(xiàn)將proto編譯為java代碼并打包為jar包的目的。
可以說(shuō)是極為方便了。
?
編寫proto文件,定義服務(wù)接口
?編寫OrderService.proto,定義服務(wù)接口,主要定義了查詢用戶訂單,批量下單接口,及對(duì)應(yīng)的各種實(shí)體和枚舉。
?
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.snowalker.grpc.sdk";
option java_outer_classname = "OrderServiceProto";
// 訂單服務(wù)IDL定義
service OrderService {
// 查詢用戶訂單列表
rpc queryUserOrders (QueryUserOrderRequest) returns (QueryUserOrderResponse) {
}
// 下單
rpc placeOrder(PlaceOrderRequest) returns (PlaceOrderRequestResponse) {
}
}
// 查詢訂單請(qǐng)求
message QueryUserOrderRequest {
int32 userId = 1;
}
// 查詢訂單響應(yīng)
message QueryUserOrderResponse {
int32 userId = 1;
string totalPrice = 2;
repeated UserOrder userOrder = 3;
}
// 批量下單請(qǐng)求
message PlaceOrderRequest {
int32 userId = 1;
repeated PlaceUserOrderParam placeUserOrderParam = 2;
}
// 批量下單響應(yīng)
message PlaceOrderRequestResponse {
int32 userId = 1;
ResultCode resultCode = 2;
}
// 訂單查詢?cè)斍?br>message UserOrder {
int64 orderId = 1;
string orderPrice = 2;
string orderAmount = 3;
int32 productId = 4;
}
// 下單請(qǐng)求詳情
message PlaceUserOrderParam {
string orderPrice = 1; // 單價(jià)
string orderAmount = 2; // 數(shù)量
int32 productId = 3; // 商品id
}
// 結(jié)果枚舉:成功/失敗
enum ResultCode {
SUCCESS = 0;
FAILURE = 1;
UNKNOWN = 2;
}
?如下為protobuf與java、c++對(duì)應(yīng)關(guān)系,
更多protobuf的使用,請(qǐng)參考官網(wǎng)文檔:https://developers.google.com/protocol-buffers/docs/javatutorial
?
「protobuf」「屬性」 | 「C++」「屬性」 | 「java」「屬性」 | 「?jìng)渥ⅰ?/strong> |
double | double | double | 固定8個(gè)字節(jié) |
float | float | float | 固定4個(gè)字節(jié) |
int32 | int32 | int32 | 使用變長(zhǎng)編碼,對(duì)于負(fù)數(shù)編碼效率較低,如果經(jīng)常使用負(fù)數(shù),建議使用sint32 |
int64 | int64 | int64 | 使用變長(zhǎng)編碼,對(duì)于負(fù)數(shù)編碼效率較低,如果經(jīng)常使用負(fù)數(shù),建議使用sint64 |
uint32 | uint32 | int | 使用變長(zhǎng)編碼 |
uint64 | uint64 | long | 使用變長(zhǎng)編碼 |
sint32 | int32 | int | 采用zigzag壓縮,對(duì)負(fù)數(shù)編碼效率比int32高 |
sint64 | int64 | long | 采用zigzag壓縮,對(duì)負(fù)數(shù)編碼效率比int64高 |
fixed32 | uint32 | int | 總是4字節(jié),如果數(shù)據(jù)>2^28,編碼效率高于unit32 |
fixed64 | uint64 | long | 總是8字節(jié),如果數(shù)據(jù)>2^56,編碼效率高于unit32 |
sfixed32 | int32 | int | 總是4字節(jié) |
sfixed64 | int64 | long | 總是8字節(jié) |
bool | bool | boolean | |
string | string | String | 一個(gè)字符串必須是utf-8編碼或者7-bit的ascii編碼的文本 |
bytes | string | ByteString | 可能包含任意順序的字節(jié)數(shù)據(jù) |
編譯打包grpc-demo-sdk工程
?編寫完proto文件后,對(duì)grpc-demo-sdk工程執(zhí)行打包編譯
?
mvn?clean?install?-DskipTests
編寫服務(wù)端grpc-server-demo
?接著編寫服務(wù)端
?
pom.xml
?服務(wù)端pom內(nèi)容如下
?
<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">
????<parent>
????????<artifactId>grpc-demoartifactId>
????????<groupId>com.snowalkergroupId>
????????<version>1.0-SNAPSHOTversion>
????parent>
????<modelVersion>4.0.0modelVersion>
????<artifactId>grpc-server-demoartifactId>
????<properties>
????????<maven.compiler.source>8maven.compiler.source>
????????<maven.compiler.target>8maven.compiler.target>
????????<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
????properties>
????<dependencies>
????????<dependency>
????????????<artifactId>grpc-demo-sdkartifactId>
????????????<groupId>com.snowalkergroupId>
????????????<version>1.0-SNAPSHOTversion>
????????dependency>
????????<dependency>
????????????<artifactId>lombokartifactId>
????????????<groupId>org.projectlombokgroupId>
????????????<scope>providedscope>
????????dependency>
????dependencies>
project>
除了lombok外,其余的依賴由grpc-demo-sdk間接引入。
編寫OrderServiceImpl實(shí)現(xiàn)核心業(yè)務(wù)邏輯
?首先編寫OrderServiceImpl,實(shí)現(xiàn)核心的下單與查訂單業(yè)務(wù)邏輯。
?
/**
?*?@author?snowalker
?*?@version?1.0
?*?@date?2022/3/12?23:47
?*?@className
?*?@desc
?*/
public?class?OrderServiceImpl?extends?OrderServiceGrpc.OrderServiceImplBase?{
?private?static?final?Logger?logger?=?Logger.getLogger(OrderServiceImpl.class.getName());
?private?static?final?Map>?USER_MEMORY_ORDER_BOOK?=?Maps.newConcurrentMap();
?/**
??*?
??*?查詢用戶訂單列表
??*?
??*
??*?@param?request
??*?@param?responseObserver
??*/
?@Override
?public?void?queryUserOrders(QueryUserOrderRequest?request,?StreamObserver?responseObserver) ?{
??int?userId?=?request.getUserId();
??//?查詢訂單
??List?orders?=?USER_MEMORY_ORDER_BOOK.getOrDefault(userId,?Lists.newLinkedList());
??//?計(jì)算總價(jià)
??String?totalPrice?=?calculateTotalPrice(orders);
??//?組裝response
??QueryUserOrderResponse?queryUserOrderResponse?=?QueryUserOrderResponse.newBuilder()
????.setUserId(userId)
????.addAllUserOrder(orders)
????.setTotalPrice(totalPrice)
????.build();
??logger.info("[Server]?queryUserOrders,?request:"?+?request.toString()?+?"\n"?+?"response:"?+?queryUserOrderResponse.toString());
??//?響應(yīng)
??responseObserver.onNext(queryUserOrderResponse);
??responseObserver.onCompleted();
?}
?private?String?calculateTotalPrice(List?orders) ?{
??Optional?count?=?orders.stream()
????.map(order?->?new?BigDecimal(order.getOrderAmount()).multiply(new?BigDecimal(order.getOrderPrice())))
????.reduce(BigDecimal::add);
??return?count.orElseGet(()?->?BigDecimal.ZERO).toPlainString();
?}
?/**
??*?
??*?下單
??*?
??*
??*?@param?request
??*?@param?responseObserver
??*/
?@Override
?public?void?placeOrder(PlaceOrderRequest?request,?StreamObserver?responseObserver) ?{
??ThreadLocalRandom?orderIdGenerator?=?ThreadLocalRandom.current();
??PlaceOrderRequestResponse.Builder?placeOrderRequestResponse?=?PlaceOrderRequestResponse.newBuilder();
??int?userId?=?request.getUserId();
??if?(request.getPlaceUserOrderParamCount()?<=?0)?{
???placeOrderRequestResponse.setUserId(userId).setResultCode(ResultCode.FAILURE).build();
???responseObserver.onNext(placeOrderRequestResponse.build());
???responseObserver.onCompleted();
??}
??//?獲取用戶訂單列表
??LinkedList?userOrderList?=?USER_MEMORY_ORDER_BOOK.getOrDefault(userId,?Lists.newLinkedList());
??if?(userOrderList.size()?==?0)?{
???USER_MEMORY_ORDER_BOOK.put(userId,?Lists.newLinkedList());
??}
??int?orderId?=?getOrderId(orderIdGenerator);
??//?本次訂單
??List?userOrders?=?request.getPlaceUserOrderParamList().stream().map(
????param?->?UserOrder.newBuilder()
??????.setOrderId(orderId)
??????.setOrderAmount(param.getOrderAmount())
??????.setOrderPrice(param.getOrderPrice())
??????.setProductId(param.getProductId())
??????.build()).collect(Collectors.toList());
??//?追加訂單列表
??userOrderList.addAll(userOrders);
??USER_MEMORY_ORDER_BOOK.put(userId,?userOrderList);
??//?響應(yīng)
??responseObserver.onNext(placeOrderRequestResponse.setUserId(userId).setResultCode(ResultCode.SUCCESS).build());
??responseObserver.onCompleted();
?}
?private?int?getOrderId(ThreadLocalRandom?orderIdGenerator)?{
??int?orderId?=?orderIdGenerator.nextInt();
??if?(orderId?0)?{
???orderId?*=?-1;
??}
??return?orderId;
?}
}
這里的代碼是完整的代碼,讀者可以自行復(fù)制并直接使用,簡(jiǎn)單解釋下代碼:
placeOrder為下單服務(wù),核心邏輯就是解析用戶下單請(qǐng)求PlaceOrderRequest,將用戶訂單增量添加到內(nèi)存訂單簿USER_MEMORY_ORDER_BOOK中。 核心的數(shù)據(jù)結(jié)構(gòu)為:*Map >*,在實(shí)戰(zhàn)中,通用會(huì)持久化訂單到redis、MySQL、RocksDB等存儲(chǔ)設(shè)施中; queryUserOrders為查詢訂單服務(wù),核心邏輯為解析用戶查詢訂單請(qǐng)求QueryUserOrderRequest,取出用戶id(userId),并在訂單簿中匹配當(dāng)前用戶的訂單列表。
服務(wù)端啟動(dòng)類OrderServerBoot
?有了服務(wù)端業(yè)務(wù)代碼之后,重點(diǎn)關(guān)注一下服務(wù)端啟動(dòng)類的編寫。
?
/**
?*?@author?snowalker
?*?@version?1.0
?*?@date?2022/3/12?23:46
?*?@desc?服務(wù)端啟動(dòng)類
?*/
public?class?OrderServerBoot?{
?private?static?final?Logger?logger?=?Logger.getLogger(OrderServerBoot.class.getName());
?private?Server?server;
?@SneakyThrows
?private?void?startServer()?{
??int?serverPort?=?10880;
??server?=?ServerBuilder.forPort(serverPort)
????.addService(new?OrderServiceImpl())
????.build();
??server.start();
??logger.info("OrderServerBoot?started,?listening?on:"?+?serverPort);
??//?優(yōu)雅停機(jī)
??addGracefulShowdownHook();
?}
?private?void?addGracefulShowdownHook()?{
??Runtime.getRuntime().addShutdownHook(new?Thread(()?->?{
???//?Use?stderr?here?since?the?logger?may?have?been?reset?by?its?JVM?shutdown?hook.
???System.err.println("***?shutting?down?gRPC?server?since?JVM?is?shutting?down");
???OrderServerBoot.this.stop();
???System.err.println("***?server?shut?down");
??}));
?}
?/**
??*?服務(wù)關(guān)閉
??*/
?private?void?stop()?{
??if?(server?!=?null)?{
???server.shutdown();
??}
?}
?/**
??*?由于 grpc 庫(kù)使用守護(hù)線程,因此在主線程上等待終止。
??*/
?private?void?blockUntilShutdown()?throws?InterruptedException?{
??if?(server?!=?null)?{
???server.awaitTermination();
??}
?}
?@SneakyThrows
?public?static?void?main(String[]?args)?{
??OrderServerBoot?boot?=?new?OrderServerBoot();
??//?啟動(dòng)服務(wù)
??boot.startServer();
??//?主線程等待終止
??boot.blockUntilShutdown();
?}
}
解釋下代碼:
核心邏輯為main方法,首先定義OrderServerBoot,通過(guò)startServer()啟動(dòng)服務(wù),并通過(guò)blockUntilShutdown()讓主線程等待終止。 「startServer()」 方法核心邏輯,啟動(dòng)一個(gè)服務(wù)端進(jìn)程并綁定到對(duì)應(yīng)的端口,這里使用10880,并添加優(yōu)雅停機(jī)鉤子; 「stop()」 邏輯為服務(wù)關(guān)閉邏輯; 「blockUntilShutdown()」 :由于grpc使用守護(hù)線程,因此需要在主線程上等待終止。
編寫客戶端grpc-client-demo
?有了服務(wù)端,我們接著看下客戶端工程的編寫。
?
pom.xml
?客戶端pom如下
?
<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">
????<parent>
????????<artifactId>grpc-demoartifactId>
????????<groupId>com.snowalkergroupId>
????????<version>1.0-SNAPSHOTversion>
????parent>
????<modelVersion>4.0.0modelVersion>
????<artifactId>grpc-client-demoartifactId>
????<properties>
????????<maven.compiler.source>8maven.compiler.source>
????????<maven.compiler.target>8maven.compiler.target>
????????<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
????properties>
????<dependencies>
????????<dependency>
????????????<artifactId>grpc-demo-sdkartifactId>
????????????<groupId>com.snowalkergroupId>
????????????<version>1.0-SNAPSHOTversion>
????????dependency>
????????<dependency>
????????????<artifactId>lombokartifactId>
????????????<groupId>org.projectlombokgroupId>
????????????<scope>providedscope>
????????dependency>
????dependencies>
project>
與服務(wù)端相同,除了lombok外,其余的依賴由grpc-demo-sdk間接引入。
編寫客戶端服務(wù)調(diào)用代理OrderClientAgent
?客戶端調(diào)用遠(yuǎn)程服務(wù),需要借助proto生成的stub樁,作為客戶端而言,常常會(huì)對(duì)該stub進(jìn)行包裝,這里我們通過(guò)一個(gè)OrderClientAgent作為stub的包裝類。
?
public?class?OrderClientAgent?{
?private?static?final?Logger?logger?=?Logger.getLogger(OrderClientAgent.class.getName());
?private?final?ManagedChannel?channel;
?//?客戶端請(qǐng)求服務(wù)端的樁
?private?final?OrderServiceGrpc.OrderServiceBlockingStub?orderServiceBlockingStub;
?public?OrderClientAgent(String?host,?int?port)?{
??this(ManagedChannelBuilder.forAddress(host,?port)
????//使用非安全機(jī)制傳輸,默認(rèn)情況下,通道是安全的(通過(guò)SSLTLS)
????.usePlaintext()
????.build());
?}
?OrderClientAgent(ManagedChannel?channel)?{
??this.channel?=?channel;
??orderServiceBlockingStub?=?OrderServiceGrpc.newBlockingStub(channel);
?}
?public?void?shutdown()?throws?InterruptedException?{
??channel.shutdown().awaitTermination(5,?TimeUnit.SECONDS);
?}
?/**
??*?下單
??*?@param?request
??*/
?public?PlaceOrderRequestResponse?placeOrder(PlaceOrderRequest?request)?{
??logger.info("client?placeOrder?start.?request:"?+?request.toString());
??PlaceOrderRequestResponse?placeOrderRequestResponse;
??try?{
???placeOrderRequestResponse?=?orderServiceBlockingStub.placeOrder(request);
??}?catch?(Exception?e)?{
???e.printStackTrace();
???return?null;
??}
??return?placeOrderRequestResponse;
?}
?/**
??*?訂單查詢
??*?@param?request
??*?@return
??*/
?public?QueryUserOrderResponse?queryOrders(QueryUserOrderRequest?request)?{
??logger.info("client?queryOrders?start.?request:"?+?request.toString());
??QueryUserOrderResponse?queryUserOrderResponse;
??try?{
???queryUserOrderResponse?=?orderServiceBlockingStub.queryUserOrders(request);
??}?catch?(Exception?e)?{
???e.printStackTrace();
???return?null;
??}
??return?queryUserOrderResponse;
?}
}
簡(jiǎn)單解釋下代碼:
通過(guò)構(gòu)造方法傳入主機(jī)名,服務(wù)端端口,構(gòu)造客戶端與服務(wù)端間的鏈接通過(guò)ManagedChannel 通過(guò)OrderServiceGrpc.newBlockingStub(channel)生成客戶端訪問(wèn)的stub實(shí)例,這里使用的是阻塞型Stub,即同步等待服務(wù)端返回所有結(jié)果; placeOrder方法通過(guò)stub訪問(wèn)服務(wù)端的下單服務(wù); queryOrders方法通過(guò)stub訪問(wèn)服務(wù)端的查詢訂單服務(wù)。
編寫客戶端啟動(dòng)類
/**
?*?@author?snowalker
?*?@version?1.0
?*?@date?2022/3/12?23:56
?*?@desc?客戶端啟動(dòng)類
?*/
public?class?OrderClientBoot?{
???private?static?final?Logger?logger?=?Logger.getLogger(OrderClientBoot.class.getName());
???@SneakyThrows
???public?static?void?main(String[]?args)?{
??????int?port?=?10880;
??????OrderClientAgent?orderClientAgent?=?new?OrderClientAgent("127.0.0.1",?port);
??????try?{
?????????int?userId?=?10086;
?????????//?下單
?????????doPlaceOrder(orderClientAgent,?userId);
?????????//?查訂單
?????????doQueryOrder(orderClientAgent,?userId);
??????}?finally?{
?????????orderClientAgent.shutdown();
??????}
???}
???private?static?void?doQueryOrder(OrderClientAgent?orderClientAgent,?int?userId)?{
??????QueryUserOrderRequest?queryUserOrderRequest?=?QueryUserOrderRequest.newBuilder()
????????????.setUserId(userId)
????????????.buildPartial();
??????QueryUserOrderResponse?queryUserOrderResponse?=?orderClientAgent.queryOrders(queryUserOrderRequest);
??????logger.info("client?queryOrders?end.?response:"?+?queryUserOrderResponse.toString());
???}
???private?static?void?doPlaceOrder(OrderClientAgent?orderClientAgent,?int?userId)?{
??????PlaceUserOrderParam?orderParam0?=?PlaceUserOrderParam.newBuilder()
????????????.setProductId(1)
????????????.setOrderAmount("15.00")
????????????.setOrderPrice("12.50")
????????????.build();
??????PlaceUserOrderParam?orderParam1?=?PlaceUserOrderParam.newBuilder()
????????????.setProductId(2)
????????????.setOrderAmount("2.00")
????????????.setOrderPrice("10.00")
????????????.build();
??????PlaceOrderRequest?placeOrderRequest?=?PlaceOrderRequest.newBuilder()
????????????.setUserId(userId)
????????????.addAllPlaceUserOrderParam(Lists.newArrayList(orderParam0,?orderParam1))
????????????.buildPartial();
??????PlaceOrderRequestResponse?placeOrderRequestResponse?=?orderClientAgent.placeOrder(placeOrderRequest);
??????logger.info("client?placeOrder?end.?response:"?+?placeOrderRequestResponse.toString()?+?",resultCode:"?+?placeOrderRequestResponse.getResultCode());
???}
}
重點(diǎn)關(guān)注main方法:
聲明服務(wù)端端口,這里注意務(wù)必與服務(wù)端暴露服務(wù)端口保持一致; 通過(guò)構(gòu)造方法創(chuàng)建客戶端訪問(wèn)服務(wù)端的agent實(shí)例,即上面提到的OrderClientAgent; 通過(guò)實(shí)例化的OrderClientAgent執(zhí)行下單、查詢訂單操作 調(diào)用完成后,關(guān)閉OrderClientAgent,關(guān)閉客戶端與服務(wù)端之間的鏈接。 「實(shí)際生產(chǎn)中,客戶端往往會(huì)與服務(wù)端保持鏈接開(kāi)啟,而不會(huì)頻繁創(chuàng)建、關(guān)閉服務(wù)。」
測(cè)試
?sdk、客戶端、服務(wù)端均編寫完畢,我們啟動(dòng)服務(wù)進(jìn)行測(cè)試。
?
首先編譯打包sdk
在grpc-demo-sdk根目錄下執(zhí)行:
mvn?clean?install?-DskipTests
啟動(dòng)服務(wù)端
運(yùn)行OrderServerBoot的main方法,日志打印如下:
三月?13,?2022?10:50:29?上午?OrderServerBoot?startServer
信息:?OrderServerBoot?started,?listening?on:10880
啟動(dòng)客戶端
運(yùn)行OrderClientBoot的main方法,啟動(dòng)客戶端并發(fā)起服務(wù)調(diào)用
?首先進(jìn)行下單:
?
三月?13,?2022?10:54:57?上午?agent.OrderClientAgent?placeOrder
信息:?client?placeOrder?start.?request:userId:?10086
placeUserOrderParam?{
??orderPrice:?"12.50"
??orderAmount:?"15.00"
??productId:?1
}
placeUserOrderParam?{
??orderPrice:?"10.00"
??orderAmount:?"2.00"
??productId:?2
}
三月?13,?2022?10:54:58?上午?OrderClientBoot?doPlaceOrder
信息:?client?placeOrder?end.?response:userId:?10086
,resultCode:SUCCESS
下單成功,接著發(fā)起查詢訂單操作:
三月?13,?2022?12:20:55?下午?agent.OrderClientAgent?queryOrders
信息:?client?queryOrders?start.?request:userId:?10086
三月?13,?2022?12:20:55?下午?OrderClientBoot?doQueryOrder
信息:?client?queryOrders?end.?response:userId:?10086
totalPrice:?"207.5000"
userOrder?{
??orderId:?510807688
??orderPrice:?"12.50"
??orderAmount:?"15.00"
??productId:?1
}
userOrder?{
??orderId:?510807688
??orderPrice:?"10.00"
??orderAmount:?"2.00"
??productId:?2
}
可以看到,下單成功,且通過(guò)查詢訂單調(diào)用,將用戶10086下的兩個(gè)訂單獲取到了。
觀察服務(wù)端日志
?服務(wù)端日志打印如下
?
三月?13,?2022?12:20:55?下午?service.OrderServiceImpl?queryUserOrders
信息:?[Server]?queryUserOrders,?request:userId:?10086
response:userId:?10086
totalPrice:?"207.5000"
userOrder?{
??orderId:?510807688
??orderPrice:?"12.50"
??orderAmount:?"15.00"
??productId:?1
}
userOrder?{
??orderId:?510807688
??orderPrice:?"10.00"
??orderAmount:?"2.00"
??productId:?2
}
服務(wù)端完成下單之后,對(duì)用戶訂單總價(jià)值進(jìn)行計(jì)算
?totalPrice = 12.5*15 + 10 *2 = 207.5
?
小結(jié)
本文我們對(duì)gRPC進(jìn)行了如下介紹:
gRPC特性介紹 gRPC-java線程模型 gRPC客戶端請(qǐng)求服務(wù)端方式 gRPC與REST性能比對(duì)
并通過(guò)一個(gè)完整的demo展示了基于gRPC實(shí)現(xiàn)的報(bào)價(jià)服務(wù),全景展示了gRPC在實(shí)戰(zhàn)中如何進(jìn)行使用。
到此我們對(duì)gRPC應(yīng)當(dāng)有了大致的了解和認(rèn)知,后續(xù)我們將繼續(xù)從入門到放棄的學(xué)習(xí)之路。
預(yù)告:接下來(lái)將對(duì)gRPC的底層機(jī)制進(jìn)行講解,并會(huì)為我們的報(bào)價(jià)服務(wù)添加服務(wù)發(fā)現(xiàn)能力,整合Nacos提供服務(wù)注冊(cè)與發(fā)現(xiàn),降低客戶端與服務(wù)端之間的耦合,敬請(qǐng)期待。
