Nacos源码服务端集群系列第2篇gRPC 流入门

一、概述

上一章节我们通过一个简单的例子入门和了解了gRPC的一元rpc,知道如何配置一个服务和请求、相应消息,并通编译工具生成java代码。以及讲解了如何创启动一个服务器,并添加我们的服务类。使用client stub 跟服务端完成服务的调用和返回。

一元rpc跟我们平时的rest调用类似,客户端每次发送一次请求,等待服务端的返回,这样一来一回请求方式处理大量数据交互消息会很低。gRPC还提供了更强大的流功能满足更复杂的业务需求和场景,比如:

客户端或者服务端需要发送大量的数据

  • 客户端持续订阅服务端源源不断生产的消息
  • 客户端源源不断发送消息给服务端, 比如生产设备的数据实时上报给服务端
  • 客户端和服务端频繁的数据交互比如聊天场景
  • 因此stream rpc 适用于,大规模的数据传递,和实时场景。

在本章节中,我们将学习 gRPC 流。流允许在服务器和客户端之间多路复用消息,创建非常高效和灵活的进程间通信

2. gRPC Streaming 基础

gRPC 使用HTTP/2网络协议进行服务间通信 HTTP/2 的一个 关键优势是它支持流。每个流可以多路复用共享单个连接的多个双向消息。

在 gRPC 中,我们可以使用三种函数调用类型进行流式处理:

  1. 服务器流式 RPC:客户端向服务器发送一个请求并取回它按顺序读取的几条消息。
  2. 客户端流式 RPC:客户端向服务器发送一系列消息。客户端等待服务器处理消息并读取返回的响应。
  3. 双向流式 RPC:客户端和服务器可以来回发送多条消息。消息的接收顺序与发送顺序相同。但是,服务器或客户端可以按照他们选择的顺序响应接收到的消息。

为了演示如何使用这些功能,我们将编写一个简单的客户端-服务器应用程序示例来交换股票报价信息。

3.服务定义

我们使用stock_quote.proto来定义服务接口和有效负载消息的结构:

service StockQuoteProvider {
  
  rpc serverSideStreamingGetListStockQuotes(Stock) returns (stream StockQuote) {}

  rpc clientSideStreamingGetStatisticsOfStocks(stream Stock) returns (StockQuote) {}
  
  rpc bidirectionalStreamingGetListsStockQuotes(stream Stock) returns (stream StockQuote) {}
}
message Stock {
   string ticker_symbol = 1;
   string company_name = 2;
   string description = 3;
}
message StockQuote {
   double price = 1;
   int32 offer_number = 2;
   string description = 3;
}

StockQuoteProvider服务具有三种支持消息流的方法类型。在下面的内容中,我们将介绍它们的实现。

我们从服务的方法签名中看到,客户端通过发送Stock消息来查询服务器。服务器使用StockQuote消息发回响应。

我们使用pom.xml文件中定义的protobuf-maven-plugin从stock-quote.proto IDL 文件中生成 Java 代码

该插件在target/generated-sources/protobuf/java/grpc-java目录中生成客户端stub和服务器端代码。

我们将利用生成的代码来实现我们的 server 和 client

4. 服务器实现

StockServer构造函数使用 gRPC服务器来侦听和分派接收到的请求:

public class StockServer {
    private int port;
    private io.grpc.Server server;

    public StockServer(int port) throws IOException {
        this.port = port;
        server = ServerBuilder.forPort(port)
          .addService(new StockService())
          .build();
    }
    //...
}

我们将StockService添加到io.grpc.ServerStockService扩展StockQuoteProviderImplBase,它是protobuf插件从我们的 proto 文件生成的。因此,StockQuoteProviderImplBase具有三个流服务方法的stub

StockService需要重写这些stub方法来执行我们的服务的实际实现

接下来,我们将看看这三个流式方法是如何完成的。

4.1。服务器端流式传输

客户发送一个报价请求并返回多个响应,每个响应为商品提供不同的价格:

@Override
public void serverSideStreamingGetListStockQuotes(Stock request, StreamObserver<StockQuote> responseObserver) {
    for (int i = 1; i <= 5; i++) {
        StockQuote stockQuote = StockQuote.newBuilder()
          .setPrice(fetchStockPriceBid(request))
          .setOfferNumber(i)
          .setDescription("Price for stock:" + request.getTickerSymbol())
          .build();
        responseObserver.onNext(stockQuote);
    }
    responseObserver.onCompleted();
}

该方法循环创建StockQuote,获取价格,并标记报价编号。每一次循环,对象构建完毕后,调用responseObserver::onNext向客户端发送该对象消息。方法的最后使用reponseObserver::onCompleted 表示完成 RPC调用 。

4.2. 客户端流式传输

客户端发送多只股票,服务器返回一个StockQuote

@Override
public StreamObserver<Stock> clientSideStreamingGetStatisticsOfStocks(StreamObserver<StockQuote> responseObserver) {
    return new StreamObserver<Stock>() {
        int count;
        double price = 0.0;
        StringBuffer sb = new StringBuffer();

        @Override
        public void onNext(Stock stock) {
            count++;
            price = +fetchStockPriceBid(stock);
            sb.append(":")
                .append(stock.getTickerSymbol());
        }

        @Override
        public void onCompleted() {
            responseObserver.onNext(StockQuote.newBuilder()
                .setPrice(price / count)
                .setDescription("Statistics-" + sb.toString())
                .build());
            responseObserver.onCompleted();
        }

        // handle onError() ...
    };
}

该方法获取一个StreamObserver<StockQuote>作为客户端请求参数。它返回一个StreamObserver<Stock>对象。重写了里面的onNext,onCompleted, onError 3个方法

onNext()以在客户端每次发送请求时执行该方法累计股票的价格,并累计股票的数量,打印消息。

StreamObserver<Stock>.onCompleted()方法在客户端发送完所有消息后被调用。使用我们收到的所有Stock消息,我们找到收到的股票价格的平均值,创建StockQuote并调用responseObserver::onNext将结果传递给客户端。

最后,我们重写StreamObserver<Stock>.onError()来处理异常情况。

4.3. 双向流

客户端发送几只股票,服务器为每个请求返回一组价格

@Override
public StreamObserver<Stock> bidirectionalStreamingGetListsStockQuotes(StreamObserver<StockQuote> responseObserver) {
    return new StreamObserver<Stock>() {
        @Override
        public void onNext(Stock request) {
            for (int i = 1; i <= 5; i++) {
                StockQuote stockQuote = StockQuote.newBuilder()
                  .setPrice(fetchStockPriceBid(request))
                  .setOfferNumber(i)
                  .setDescription("Price for stock:" + request.getTickerSymbol())
                  .build();
                responseObserver.onNext(stockQuote);
            }
        }

        @Override
        public void onCompleted() {
            responseObserver.onCompleted();
        }

        //handle OnError() ...
    };
}

我们的方法签名与前面的示例相同。不同的地方是:在我们响应客户点请求之前,我们不等待客户端发送所有消息。客户点每次发送一个请求,我们就返回一组价格列表给客户点。

在这种情况下,我们在接收到每条传入消息后立即调用responseObserver::onNext,并且与接收消息的顺序相同。

需要注意的是,如果需要,我们可以轻松更改响应的顺序。

5. 客户端实现

StockClient的构造函数采用 gRPC 通道并实例化由 gRPC Maven 插件生成的stub类:

public class StockClient {
    private StockQuoteProviderBlockingStub blockingStub;
    private StockQuoteProviderStub nonBlockingStub;

    public StockClient(Channel channel) {
        blockingStub = StockQuoteProviderGrpc.newBlockingStub(channel);
        nonBlockingStub = StockQuoteProviderGrpc.newStub(channel);
    }
    // ...
}

StockQuoteProviderBlockingStubStockQuoteProviderStub支持进行同步和异步的客户端方法请求

接下来我们看看三个流式 RPC 的客户端实现。

5.1。带有服务器端流的客户端 RPC

客户端向服务器发出一次请求,请求股票价格并获取报价列表:

public void serverSideStreamingListOfStockPrices() {
    Stock request = Stock.newBuilder()
      .setTickerSymbol("AU")
      .setCompanyName("Austich")
      .setDescription("server streaming example")
      .build();
    Iterator<StockQuote> stockQuotes;
    try {
        logInfo("REQUEST - ticker symbol {0}", request.getTickerSymbol());
        stockQuotes = blockingStub.serverSideStreamingGetListStockQuotes(request);
        for (int i = 1; stockQuotes.hasNext(); i++) {
            StockQuote stockQuote = stockQuotes.next();
            logInfo("RESPONSE - Price #" + i + ": {0}", stockQuote.getPrice());
        }
    } catch (StatusRuntimeException e) {
        logInfo("RPC failed: {0}", e.getStatus());
    }
}

我们使用blockingStub::serverSideStreamingGetListStock来发出同步请求。然后使用迭代器返回一个StockQuotes列表。

5.2. 带有客户端流的客户端 RPC

客户端向服务器发送一个Stock流,并返回一个带有一些统计信息的StockQuote :

public void clientSideStreamingGetStatisticsOfStocks() throws InterruptedException {
    StreamObserver<StockQuote> responseObserver = new StreamObserver<StockQuote>() {
        @Override
        public void onNext(StockQuote summary) {
            logInfo("RESPONSE, got stock statistics - Average Price: {0}, description: {1}", summary.getPrice(), summary.getDescription());
        }

        @Override
        public void onCompleted() {
            logInfo("Finished clientSideStreamingGetStatisticsOfStocks");
        }

        // Override OnError ...
    };
    
    StreamObserver<Stock> requestObserver = nonBlockingStub.clientSideStreamingGetStatisticsOfStocks(responseObserver);
    try {
        for (Stock stock : stocks) {
            logInfo("REQUEST: {0}, {1}", stock.getTickerSymbol(), stock.getCompanyName());
            requestObserver.onNext(stock);
        }
    } catch (RuntimeException e) {
        requestObserver.onError(e);
        throw e;
    }
    requestObserver.onCompleted();
}

正如我们对服务端示例所做的那样,我们使用StreamObservers来发送和接收消息。

requestObserver使用非阻塞stubStock的列表发送到服务器。

使用responseObserver,我们可以返回带有统计数据的StockQuote 。

5.3. 具有双向流的客户端 RPC

客户端发送一个Stock流并返回每个Stock的价格列表。

public void bidirectionalStreamingGetListsStockQuotes() throws InterruptedException{
    StreamObserver<StockQuote> responseObserver = new StreamObserver<StockQuote>() {
        @Override
        public void onNext(StockQuote stockQuote) {
            logInfo("RESPONSE price#{0} : {1}, description:{2}", stockQuote.getOfferNumber(), stockQuote.getPrice(), stockQuote.getDescription());
        }

        @Override
        public void onCompleted() {
            logInfo("Finished bidirectionalStreamingGetListsStockQuotes");
        }

        //Override onError() ...
    };
    
    StreamObserver<Stock> requestObserver = nonBlockingStub.bidirectionalStreamingGetListsStockQuotes(responseObserver);
    try {
        for (Stock stock : stocks) {
            logInfo("REQUEST: {0}, {1}", stock.getTickerSymbol(), stock.getCompanyName());
            requestObserver.onNext(stock);
            Thread.sleep(200);
        }
    } catch (RuntimeException e) {
        requestObserver.onError(e);
        throw e;
    }
    requestObserver.onCompleted();
}

该实现与客户端流式传输例子非常相似。我们使用requestObserver发送Stock s — 唯一的区别是现在我们使用 responseObserver 获得多个响应。响应与请求是分离的——它们可以按任何顺序到达。

6. 运行服务器和客户端

7. 结论

在本文中,我们了解了如何在 gRPC 中使用流式传输。流式传输是一项强大的功能,它允许客户端和服务器通过在单个连接上发送多条消息进行通信。此外,消息的接收顺序与发送顺序相同,但任何一方都可以按照他们想要的任何顺序读取或写入消息

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值