一、概述
上一章节我们通过一个简单的例子入门和了解了gRPC的一元rpc,知道如何配置一个服务和请求、相应消息,并通编译工具生成java代码。以及讲解了如何创启动一个服务器,并添加我们的服务类。使用client stub 跟服务端完成服务的调用和返回。
一元rpc跟我们平时的rest调用类似,客户端每次发送一次请求,等待服务端的返回,这样一来一回请求方式处理大量数据交互消息会很低。gRPC还提供了更强大的流功能满足更复杂的业务需求和场景,比如:
客户端或者服务端需要发送大量的数据
- 客户端持续订阅服务端源源不断生产的消息
- 客户端源源不断发送消息给服务端, 比如生产设备的数据实时上报给服务端
- 客户端和服务端频繁的数据交互比如聊天场景
- 因此stream rpc 适用于,大规模的数据传递,和实时场景。
在本章节中,我们将学习 gRPC 流。流允许在服务器和客户端之间多路复用消息,创建非常高效和灵活的进程间通信。
2. gRPC Streaming 基础
gRPC 使用HTTP/2网络协议进行服务间通信。 HTTP/2 的一个 关键优势是它支持流。每个流可以多路复用共享单个连接的多个双向消息。
在 gRPC 中,我们可以使用三种函数调用类型进行流式处理:
- 服务器流式 RPC:客户端向服务器发送一个请求并取回它按顺序读取的几条消息。
- 客户端流式 RPC:客户端向服务器发送一系列消息。客户端等待服务器处理消息并读取返回的响应。
- 双向流式 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.Server。StockService扩展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);
}
// ...
}
StockQuoteProviderBlockingStub和StockQuoteProviderStub支持进行同步和异步的客户端方法请求。
接下来我们看看三个流式 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使用非阻塞stub将Stock的列表发送到服务器。
使用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 中使用流式传输。流式传输是一项强大的功能,它允许客户端和服务器通过在单个连接上发送多条消息进行通信。此外,消息的接收顺序与发送顺序相同,但任何一方都可以按照他们想要的任何顺序读取或写入消息。