1. Overview
gRPC is a platform to do inter-process Remote Procedure Calls (RPC). It follows a client-server model, is highly performant, and supports the most important computer languages. Check out our article Introduction to gRPC for a good review.
In this tutorial, we'll focus on gRPC streams. Streaming allows multiplex messages between servers and clients, creating very efficient and flexible inter-process communications.
2. Basics of gRPC Streaming
gRPC uses the HTTP/2 network protocol to do inter-service communications. One key advantage of HTTP/2 is that it supports streams. Each stream can multiplex multiple bidirectional messages sharing a single connection.
In gRPC, we can have streaming with three functional call types:
- Server streaming RPC: The client sends a single request to the server and gets back several messages that it reads sequentially.
- Client streaming RPC: The client sends a sequence of messages to the server. The client waits for the server to process the messages and reads the returned response.
- Bidirectional streaming RPC: The client and server can send multiple messages back and forth. The messages are received in the same order that they were sent. However, the server or client can respond to the received messages in the order that they choose.
To demonstrate how to use these procedural calls, we'll write a simple client-server application example that exchanges information on stock securities.
3. Service Definition
We use stock_quote.proto to define the service interface and the structure of the payload messages:
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;
}
The StockQuoteProvider service has three method types that support message streaming. In the next section, we'll cover their implementations.
We see from the service's method signatures that the client queries the server by sending Stock messages. The server sends the response back using StockQuote messages.
We use the protobuf-maven-plugin defined in the pom.xml file to generate the Java code from the stock-quote.proto IDL file.
The plugin generates client-side stubs and server-side code in the target/generated-sources/protobuf/java and /grpc-java directories.
We're going to leverage the generated code to implement our server and client.
4. Server Implementation
The StockServer constructor uses the gRPC Server to listen to and dispatch incoming requests:
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();
}
//...
}
We add StockService to the io.grpc.Server. StockService extends StockQuoteProviderImplBase, which the protobuf plugin generated from our proto file. Therefore, StockQuoteProviderImplBase has stubs for the three streaming service methods.
StockService needs to override these stub methods to do the actual implementation of our service.
Next, we're going to see how this is done for the three streaming cases.
4.1. Server-Side Streaming
The client sends a single request for a quote and gets back several responses, each with different prices offered for the commodity:
@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();
}
The method creates a StockQuote, fetches the prices, and marks the offer number. For each offer, it sends a message to the client invoking responseObserver::onNext. It uses reponseObserver::onCompleted to signal that it's done with the RPC.
4.2. Client-Side Streaming
The client sends multiple stocks and the server returns back a single 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() ...
};
}
The method gets a StreamObserver<StockQuote> as a parameter to respond to the client. It returns a StreamObserver<Stock>, where it processes the client request messages.
The returned StreamObserver<Stock> overrides onNext() to get notified each time the client sends a request.
The method StreamObserver<Stock>.onCompleted() is called when the client has finished sending all the messages. With all the Stock messages that we have received, we find the average of the fetched stock prices, create a StockQuote, and invoke responseObserver::onNext to deliver the result to the client.
Finally, we override StreamObserver<Stock>.onError() to handle abnormal terminations.
4.3. Bidirectional Streaming
The client sends several stocks and the server returns a set of prices for each request:
@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() ...
};
}
We have the same method signature as in the previous example. What changes is the implementation: We don't wait for the client to send all the messages before we respond.
In this case, we invoke responseObserver::onNext immediately after receiving each incoming message, and in the same order that it was received.
It's important to notice that we could have easily changed the order of the responses if needed.
5. Client Implementation
The constructor of StockClient takes a gRPC channel and instantiates the stub classes generated by the gRPC Maven plugin:
public class StockClient {
private StockQuoteProviderBlockingStub blockingStub;
private StockQuoteProviderStub nonBlockingStub;
public StockClient(Channel channel) {
blockingStub = StockQuoteProviderGrpc.newBlockingStub(channel);
nonBlockingStub = StockQuoteProviderGrpc.newStub(channel);
}
// ...
}
StockQuoteProviderBlockingStub and StockQuoteProviderStub support making synchronous and asynchronous client method requests.
We're going to see the client implementation for the three streaming RPCs next.
5.1. Client RPC with Server-Side Streaming
The client makes a single call to the server requesting a stock price and gets back a list of quotes:
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());
}
}
We use blockingStub::serverSideStreamingGetListStocks to make a synchronous request. We get back a list of StockQuotes with the Iterator.
5.2. Client RPC with Client-Side Streaming
The client sends a stream of Stocks to the server and gets back a StockQuote with some statistics:
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();
}
As we did with the server example, we use StreamObservers to send and receive messages.
The requestObserver uses the non-blocking stub to send the list of Stocks to the server.
With responseObserver, we get back the StockQuote with some statistics.
5.3. Client RPC with Bidirectional Streaming
The client sends a stream of Stocks and gets back a list of prices for each 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();
}
The implementation is quite similar to the client-side streaming case. We send the Stocks with the requestObserver — the only difference is that now we get multiple responses with the responseObserver. The responses are decoupled from the requests — they can arrive in any order.
6. Running the Server and Client
After using Maven to compile the code, we just need to open two command windows.
To run the server:
mvn exec:java -Dexec.mainClass=com.baeldung.grpc.streaming.StockServer
To run the client:
mvn exec:java -Dexec.mainClass=com.baeldung.grpc.streaming.StockClient
7. Conclusion
In this article, we've seen how to use streaming in gRPC. Streaming is a powerful feature that allows clients and servers to communicate by sending multiple messages over a single connection. Furthermore, the messages are received in the same order as they were sent, but either side can read or write the messages in any order they wish.
The source code of the examples can be found over on GitHub.