vlambda博客
学习文章列表

gRPC从入门到放弃之概述与实战

微服务如火如荼的当下,各种服务框架层出不穷。Dubbo、SpringCloud在国内Java后端微服务领域目前占据大部分份额。

但是随着云原生愈发普及,具备跨语言、高性能特性的RPC通信框架横空出世,其中gRPC与Thrift是其中的佼佼者。

本文我们将视角集中在gRPC这RPC框架。

gRPC 是Google开源的高性能、通用的RPC框架。客户端与服务端约定接口调用, 可以在各种环境中运行,具有跨语言特性, 适合构建分布式、微服务应用。

个人认为,gRPC最为杀手锏的特性就是“跨语言”,其次才是高性能。

它的跨语言特性体现在,通过定义IDL(接口定义语言),隔离了不同编程语言之间的差异,对IDL进行编译后,生成对应编程语言的nativeCode,让开发者能够集中注意在实现业务需求上,而不需要花费额外的精力在语言层面上。

官网的一张图能够很好地体现这个特点

gRPC多语言

gRPC特性介绍

gRPC具备以下特性

  • 性能优异:

    1. 它采用Proto Buffer作序列化传输媒介, 对比JSON与XML有数倍提升。

    2. 采用HTTP2协议, 对头部信息(header)压缩, 对连接进行复用,能够减少TCP连接次数。

    3. 针对Java语言,gRPC底层采用Netty作为NIO处理框架, 性能强劲。

  • 多语言支持,多客户端接入, 支持C++/GO/Ruby等语言。

  • 支持负载均衡、跟踪、健康检查和认证。

gRPC的线程模型是怎样的?

笔者主力语言为Java,因此我们讲解也集中在Java的实现上。

gRPC的Java实现,服务端底层采用了Netty作为核心处理框架,因此其线程模型核心也是遵循了 Netty 的线程分工原则。

协议层消息的接收和编解码由 Netty 的 I/O(NioEventLoop) 线程负责, 应用层的处理由应用线程负责,防止由于应用处理耗时而阻塞 Netty 的 I/O 线程。

Netty线程模型是基于NIO的Reactor模式。

gRPC从入门到放弃之概述与实战

gRPC-Java线程模型

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而言,客户端请求服务端的调用过程如图所示。

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从入门到放弃之概述与实战

官网也给出了权威性的比对,具体比对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父工程

父工程相对比较简单,管理了子工程及依赖版本。

<?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.snowalker</groupId>
    <artifactId>grpc-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>grpc-server-demo</module>
        <module>grpc-client-demo</module>
        <module>grpc-demo-sdk</module>
    </modules>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <grpc-version>1.44.1</grpc-version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-netty-shaded</artifactId>
                <version>${grpc-version}</version>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-protobuf</artifactId>
                <version>${grpc-version}</version>
            </dependency>
            <dependency>
                <groupId>io.grpc</groupId>
                <artifactId>grpc-stub</artifactId>
                <version>${grpc-version}</version>
            </dependency>
            <dependency>
                <artifactId>lombok</artifactId>
                <groupId>org.projectlombok</groupId>
                <version>1.18.22</version>
                <scope>provided</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

grpc-demo-sdk

grpc-demo-sdk是较为关键的公共依赖,主要基于proto对服务进行定义,生成java代码并打包供服务提供方与消费方使用。

pom.xml

sdk的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">

    <parent>
        <artifactId>grpc-demo</artifactId>
        <groupId>com.snowalker</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <name>grpc-demo-sdk</name>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>grpc-demo-sdk</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
        </dependency>
    </dependencies>

    <build>

        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <excludes>
                    <exclude>**</exclude>
                </excludes>
            </resource>
            <resource>
                <directory>src/main/proto</directory>
                <targetPath>proto</targetPath>
                <filtering>false</filtering>
            </resource>
        </resources>

        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.2</version>
            </extension>
        </extensions>

        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.19.1:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.43.1:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </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内容如下

<?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">

    <parent>
        <artifactId>grpc-demo</artifactId>
        <groupId>com.snowalker</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>grpc-server-demo</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <artifactId>grpc-demo-sdk</artifactId>
            <groupId>com.snowalker</groupId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <artifactId>lombok</artifactId>
            <groupId>org.projectlombok</groupId>
            <scope>provided</scope>
        </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<Integer, LinkedList<UserOrder>> USER_MEMORY_ORDER_BOOK = Maps.newConcurrentMap();

 /**
  * <pre>
  * 查询用户订单列表
  * </pre>
  *
  * @param request
  * @param responseObserver
  */

 @Override
 public void queryUserOrders(QueryUserOrderRequest request, StreamObserver<QueryUserOrderResponse> responseObserver) {
  int userId = request.getUserId();
  // 查询订单
  List<UserOrder> 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<UserOrder> orders) {
  Optional<BigDecimal> count = orders.stream()
    .map(order -> new BigDecimal(order.getOrderAmount()).multiply(new BigDecimal(order.getOrderPrice())))
    .reduce(BigDecimal::add);
  return count.orElseGet(() -> BigDecimal.ZERO).toPlainString();
 }

 /**
  * <pre>
  * 下单
  * </pre>
  *
  * @param request
  * @param responseObserver
  */

 @Override
 public void placeOrder(PlaceOrderRequest request, StreamObserver<PlaceOrderRequestResponse> 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<UserOrder> 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<UserOrder> 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;
 }
}

这里的代码是完整的代码,读者可以自行复制并直接使用,简单解释下代码:

  1. placeOrder为下单服务,核心逻辑就是解析用户下单请求PlaceOrderRequest,将用户订单增量添加到内存订单簿USER_MEMORY_ORDER_BOOK中。
    1. 核心的数据结构为:*Map<Integer, LinkedList >*,在实战中,通用会持久化订单到redis、MySQL、RocksDB等存储设施中;
  2. 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如下

<?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">

    <parent>
        <artifactId>grpc-demo</artifactId>
        <groupId>com.snowalker</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>grpc-client-demo</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <artifactId>grpc-demo-sdk</artifactId>
            <groupId>com.snowalker</groupId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <artifactId>lombok</artifactId>
            <groupId>org.projectlombok</groupId>
            <scope>provided</scope>
        </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提供服务注册与发现,降低客户端与服务端之间的耦合,敬请期待。