gRPC从入门到放弃之概述与实战
❝微服务如火如荼的当下,各种服务框架层出不穷。Dubbo、SpringCloud在国内Java后端微服务领域目前占据大部分份额。
但是随着云原生愈发普及,具备跨语言、高性能特性的RPC通信框架横空出世,其中gRPC与Thrift是其中的佼佼者。
❞
本文我们将视角集中在gRPC这RPC框架。
❝gRPC 是Google开源的高性能、通用的RPC框架。客户端与服务端约定接口调用, 可以在各种环境中运行,具有跨语言特性, 适合构建分布式、微服务应用。
❞
个人认为,gRPC最为杀手锏的特性就是“跨语言”,其次才是高性能。
它的跨语言特性体现在,通过定义IDL(接口定义语言),隔离了不同编程语言之间的差异,对IDL进行编译后,生成对应编程语言的nativeCode,让开发者能够集中注意在实现业务需求上,而不需要花费额外的精力在语言层面上。
❝官网的一张图能够很好地体现这个特点
❞
gRPC特性介绍
❝gRPC具备以下特性
❞
性能优异:
它采用Proto Buffer作序列化传输媒介, 对比JSON与XML有数倍提升。
采用HTTP2协议, 对头部信息(header)压缩, 对连接进行复用,能够减少TCP连接次数。
针对Java语言,gRPC底层采用Netty作为NIO处理框架, 性能强劲。
多语言支持,多客户端接入, 支持C++/GO/Ruby等语言。
支持负载均衡、跟踪、健康检查和认证。
gRPC的线程模型是怎样的?
❝笔者主力语言为Java,因此我们讲解也集中在Java的实现上。
❞
gRPC的Java实现,服务端底层采用了Netty作为核心处理框架,因此其线程模型核心也是遵循了 Netty 的线程分工原则。
协议层消息的接收和编解码由 Netty 的 I/O(NioEventLoop) 线程负责, 应用层的处理由应用线程负责,防止由于应用处理耗时而阻塞 Netty 的 I/O 线程。
❝Netty线程模型是基于NIO的Reactor模式。
❞
❝Netty是基于NIO构建的通信框架。
❞
在 Java NIO 中最重要的概念就是多路复用器 Selector,它是 Java NIO 编程的基础。Selector提供了选择已经就绪的任务的能力。
❝简单来讲,Selector 会不断地轮询注册在其上的 Channel,如果某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
❞
一般来说,一个 I/O 线程会聚合一个 Selector,一个 Selector 可以同时注册 N 个 Channel, 这样单个
I/O 线程就可以同时并发处理多个客户端连接。
又由于 I/O 操作是非阻塞的,因此也不会受限于网络速度和对方端点的处理时延,可靠性和效率都得到了很大提升。
gRPC客户端如何请求服务端?
❝作为RPC框架,至少有客户端和服务端两个角色,对于gRPC而言,客户端请求服务端的调用过程如图所示。
❞
具体过程:
【Stub生成】客户端生成Stub ,通过Stub发起 RPC远程服务调用 ; 【负载均衡】客户端获取服务端的地址信息(列表),使用默认的 LoadBalancer 策略,选择一个具体的 gRPC 服务端进行调用; 【建立链接】如果客户端与服务端之间没有可用的连接,则创建 NettyClientTransport 和 NettyClientHandler,建立 HTTP/2 连接; 【客户端请求序列化】对请求使用 PB(Protobuf)序列化,并通过 HTTP/2 Stream 发送给 gRPC 服务端; 【服务端反序列化】服务端接收到响应之后,使用 PB(Protobuf)做反序列化。 【请求响应】回调 GrpcFuture 的 set(Response) 方法,唤醒阻塞的客户端调用线程,获取 RPC 响应数据。
gRPC性能到底有多强?
❝没有对比就没有发言权。
❞
在不同的操作系统,不同请求数量下,对gRPC与Rest请求进行对比的结论如下:
❝官网也给出了权威性的比对,具体比对gRPC+ProtoBuf与Http+JSON方式请求的差异。
❞
官方性能比对结果
「实测结果显示GRpc的通讯方案, 性能有32%的提升, 资源占用降低30%左右。」
gRPC-Java 服务调用实战
❝按照惯例,我们提供一个简单的订单案例展示gRPC在实际开发中如何使用。
该案例在实际中的意义为:提供一个报价服务,客户端发送下单请求到服务端进行报价,服务端对用户报价单进行汇总计算,并提供查询接口供客户端查询。
主要提供批量下单及查询用户订单能力。
❞
流程图大致如下:
工程结构如下:
|==> grpc-demo 父级工程, 管理依赖相关
|==>grpc-demo-sdk 通用jar依赖,生成protobuf对象与gRPC Service,供提供方与调用方使用
|==>grpc-server-demo 服务端,提供下单及订单查询服务
|==>grpc-client-demo 客户端,负责调用gRPC服务
grpc-demo父工程
❝父工程相对比较简单,管理了子工程及依赖版本。
❞
<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是较为关键的公共依赖,主要基于proto对服务进行定义,生成java代码并打包供服务提供方与消费方使用。
❞
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>
❝重点关注一下plugin,我们使用protobuf-maven-plugin作为protobuf的编译工具,有了该插件,我们在执行mvn clean compile命令时便可以实现将proto编译为java代码的目的。
同样,执行mvn clean package命令可以实现将proto编译为java代码并打包为jar包的目的。
可以说是极为方便了。
❞
编写proto文件,定义服务接口
❝编写OrderService.proto,定义服务接口,主要定义了查询用户订单,批量下单接口,及对应的各种实体和枚举。
❞
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.snowalker.grpc.sdk";
option java_outer_classname = "OrderServiceProto";
// 订单服务IDL定义
service OrderService {
// 查询用户订单列表
rpc queryUserOrders (QueryUserOrderRequest) returns (QueryUserOrderResponse) {
}
// 下单
rpc placeOrder(PlaceOrderRequest) returns (PlaceOrderRequestResponse) {
}
}
// 查询订单请求
message QueryUserOrderRequest {
int32 userId = 1;
}
// 查询订单响应
message QueryUserOrderResponse {
int32 userId = 1;
string totalPrice = 2;
repeated UserOrder userOrder = 3;
}
// 批量下单请求
message PlaceOrderRequest {
int32 userId = 1;
repeated PlaceUserOrderParam placeUserOrderParam = 2;
}
// 批量下单响应
message PlaceOrderRequestResponse {
int32 userId = 1;
ResultCode resultCode = 2;
}
// 订单查询详情
message UserOrder {
int64 orderId = 1;
string orderPrice = 2;
string orderAmount = 3;
int32 productId = 4;
}
// 下单请求详情
message PlaceUserOrderParam {
string orderPrice = 1; // 单价
string orderAmount = 2; // 数量
int32 productId = 3; // 商品id
}
// 结果枚举:成功/失败
enum ResultCode {
SUCCESS = 0;
FAILURE = 1;
UNKNOWN = 2;
}
❝如下为protobuf与java、c++对应关系,
更多protobuf的使用,请参考官网文档:https://developers.google.com/protocol-buffers/docs/javatutorial
❞
「protobuf」「属性」 | 「C++」「属性」 | 「java」「属性」 | 「备注」 |
double | double | double | 固定8个字节 |
float | float | float | 固定4个字节 |
int32 | int32 | int32 | 使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint32 |
int64 | int64 | int64 | 使用变长编码,对于负数编码效率较低,如果经常使用负数,建议使用sint64 |
uint32 | uint32 | int | 使用变长编码 |
uint64 | uint64 | long | 使用变长编码 |
sint32 | int32 | int | 采用zigzag压缩,对负数编码效率比int32高 |
sint64 | int64 | long | 采用zigzag压缩,对负数编码效率比int64高 |
fixed32 | uint32 | int | 总是4字节,如果数据>2^28,编码效率高于unit32 |
fixed64 | uint64 | long | 总是8字节,如果数据>2^56,编码效率高于unit32 |
sfixed32 | int32 | int | 总是4字节 |
sfixed64 | int64 | long | 总是8字节 |
bool | bool | boolean | |
string | string | String | 一个字符串必须是utf-8编码或者7-bit的ascii编码的文本 |
bytes | string | ByteString | 可能包含任意顺序的字节数据 |
编译打包grpc-demo-sdk工程
❝编写完proto文件后,对grpc-demo-sdk工程执行打包编译
❞
mvn clean install -DskipTests
编写服务端grpc-server-demo
❝接着编写服务端
❞
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-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实现核心业务逻辑
❝首先编写OrderServiceImpl,实现核心的下单与查订单业务逻辑。
❞
/**
* @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());
// 计算总价
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());
// 响应
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);
// 响应
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;
}
}
这里的代码是完整的代码,读者可以自行复制并直接使用,简单解释下代码:
placeOrder为下单服务,核心逻辑就是解析用户下单请求PlaceOrderRequest,将用户订单增量添加到内存订单簿USER_MEMORY_ORDER_BOOK中。 核心的数据结构为:*Map >*,在实战中,通用会持久化订单到redis、MySQL、RocksDB等存储设施中; queryUserOrders为查询订单服务,核心逻辑为解析用户查询订单请求QueryUserOrderRequest,取出用户id(userId),并在订单簿中匹配当前用户的订单列表。
服务端启动类OrderServerBoot
❝有了服务端业务代码之后,重点关注一下服务端启动类的编写。
❞
/**
* @author snowalker
* @version 1.0
* @date 2022/3/12 23:46
* @desc 服务端启动类
*/
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);
// 优雅停机
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");
}));
}
/**
* 服务关闭
*/
private void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* 由于 grpc 库使用守护线程,因此在主线程上等待终止。
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
@SneakyThrows
public static void main(String[] args) {
OrderServerBoot boot = new OrderServerBoot();
// 启动服务
boot.startServer();
// 主线程等待终止
boot.blockUntilShutdown();
}
}
解释下代码:
核心逻辑为main方法,首先定义OrderServerBoot,通过startServer()启动服务,并通过blockUntilShutdown()让主线程等待终止。 「startServer()」 方法核心逻辑,启动一个服务端进程并绑定到对应的端口,这里使用10880,并添加优雅停机钩子; 「stop()」 逻辑为服务关闭逻辑; 「blockUntilShutdown()」 :由于grpc使用守护线程,因此需要在主线程上等待终止。
编写客户端grpc-client-demo
❝有了服务端,我们接着看下客户端工程的编写。
❞
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>
与服务端相同,除了lombok外,其余的依赖由grpc-demo-sdk间接引入。
编写客户端服务调用代理OrderClientAgent
❝客户端调用远程服务,需要借助proto生成的stub桩,作为客户端而言,常常会对该stub进行包装,这里我们通过一个OrderClientAgent作为stub的包装类。
❞
public class OrderClientAgent {
private static final Logger logger = Logger.getLogger(OrderClientAgent.class.getName());
private final ManagedChannel channel;
// 客户端请求服务端的桩
private final OrderServiceGrpc.OrderServiceBlockingStub orderServiceBlockingStub;
public OrderClientAgent(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port)
//使用非安全机制传输,默认情况下,通道是安全的(通过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;
}
}
简单解释下代码:
通过构造方法传入主机名,服务端端口,构造客户端与服务端间的链接通过ManagedChannel 通过OrderServiceGrpc.newBlockingStub(channel)生成客户端访问的stub实例,这里使用的是阻塞型Stub,即同步等待服务端返回所有结果; placeOrder方法通过stub访问服务端的下单服务; queryOrders方法通过stub访问服务端的查询订单服务。
编写客户端启动类
/**
* @author snowalker
* @version 1.0
* @date 2022/3/12 23:56
* @desc 客户端启动类
*/
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());
}
}
重点关注main方法:
声明服务端端口,这里注意务必与服务端暴露服务端口保持一致; 通过构造方法创建客户端访问服务端的agent实例,即上面提到的OrderClientAgent; 通过实例化的OrderClientAgent执行下单、查询订单操作 调用完成后,关闭OrderClientAgent,关闭客户端与服务端之间的链接。 「实际生产中,客户端往往会与服务端保持链接开启,而不会频繁创建、关闭服务。」
测试
❝sdk、客户端、服务端均编写完毕,我们启动服务进行测试。
❞
首先编译打包sdk
在grpc-demo-sdk根目录下执行:
mvn clean install -DskipTests
启动服务端
运行OrderServerBoot的main方法,日志打印如下:
三月 13, 2022 10:50:29 上午 OrderServerBoot startServer
信息: OrderServerBoot started, listening on:10880
启动客户端
运行OrderClientBoot的main方法,启动客户端并发起服务调用
❝首先进行下单:
❞
三月 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
下单成功,接着发起查询订单操作:
三月 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
}
可以看到,下单成功,且通过查询订单调用,将用户10086下的两个订单获取到了。
观察服务端日志
❝服务端日志打印如下
❞
三月 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
}
服务端完成下单之后,对用户订单总价值进行计算
❝totalPrice = 12.5*15 + 10 *2 = 207.5
❞
小结
本文我们对gRPC进行了如下介绍:
gRPC特性介绍 gRPC-java线程模型 gRPC客户端请求服务端方式 gRPC与REST性能比对
并通过一个完整的demo展示了基于gRPC实现的报价服务,全景展示了gRPC在实战中如何进行使用。
到此我们对gRPC应当有了大致的了解和认知,后续我们将继续从入门到放弃的学习之路。
预告:接下来将对gRPC的底层机制进行讲解,并会为我们的报价服务添加服务发现能力,整合Nacos提供服务注册与发现,降低客户端与服务端之间的耦合,敬请期待。