Welcome to my GitHub
https://github.com/zq2599/blog_demos
Content: Classification and summary of all original articles and supporting source code, involving Java, Docker, Kubernetes, DevOPS, etc.;
Overview of this article
- This article is the fifth in the "java version of gRPC combat" series, the goal is to master the two-way flow type of service, that is, the request parameter is in the form of a stream, and the content of the response is also in the form of a stream;
- Let's take a look at the official information on the introduction of two-way streaming RPC: both parties use read and write streams to send a sequence of messages. The two streams operate independently, so the client and server can read and write in any order they like: for example, the server can wait to receive all client messages before writing the response, or can alternately read and write messages, or other The combination of reading and writing. The sequence of messages in each stream is reserved;
- After mastering the development of the two types of client-side flow and server-side flow, the two-way flow type is well understood, which is a combination of the previous two types, and the request and response can be processed according to the flow method;
- Today’s actual combat, let’s design an online store’s function: batch deduction of inventory, that is, the client submits multiple products and quantities, and the server returns the success and failure of each product’s inventory deduction;
- Let's enter the coding link as soon as possible, the specific content is as follows:
- Define the two-way flow type gRPC interface in the proto file, and then generate java code through proto
- Develop server-side applications
- Develop client applications
- verify
Source download
- The complete source code of this actual combat can be downloaded on GitHub. The address and link information are shown in the following table ( https://github.com/zq2599/blog_demos):
name | Link | Remark |
---|---|---|
Project homepage | https://github.com/zq2599/blog_demos | The project's homepage on GitHub |
git warehouse address (https) | https://github.com/zq2599/blog_demos.git | The warehouse address of the source code of the project, https protocol |
git warehouse address (ssh) | git@github.com:zq2599/blog_demos.git | The warehouse address of the source code of the project, ssh protocol |
- There are multiple folders in this git project. The source code of the "java version of gRPC combat" series is under the <font color="blue">grpc-tutorials</font> folder, as shown in the red box below:
- There are multiple directories under the <font color="blue">grpc-tutorials</font> folder. The server code for this article is in <font color="blue">double-stream-server-side</font > In the directory, the client code is in the <font color="blue">double-stream-client-side</font> directory, as shown below:
Define the gRPC interface of the bidirectional stream type in the proto file
- The first thing to do is to define the gRPC interface, open <font color="blue">mall.proto</font>, add methods and related data structures in it, and focus on the input parameter ProductOrder and return of the BatchDeduct method The value of DeductReply is decorated with <font color="red">stream</font> (ProductOrder is defined in the previous chapter), indicating that the method is a two-way stream type:
// gRPC服务,这是个在线商城的库存服务
service StockService {
// 双向流式:批量扣减库存
rpc BatchDeduct (stream ProductOrder) returns (stream DeductReply) {}
}
// 扣减库存返回结果的数据结构
message DeductReply {
// 返回码
int32 code = 1;
// 描述信息
string message = 2;
}
- Double-click the task in the red box in the figure below to generate java code:
- Generate the file in the red box in the figure below, that is, the server definition and the return value data structure:
- Next, develop the server;
Develop server-side applications
- Create a new module named <font color="blue">double-stream-server-side</font> under the parent project grpc-turtorials, and its build.gradle content is as follows:
// 使用springboot插件
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter'
// 作为gRPC服务提供方,需要用到此库
implementation 'net.devh:grpc-server-spring-boot-starter'
// 依赖自动生成源码的工程
implementation project(':grpc-lib')
// annotationProcessor不会传递,使用了lombok生成代码的模块,需要自己声明annotationProcessor
annotationProcessor 'org.projectlombok:lombok'
}
- Configuration file application.yml:
spring:
application:
name: double-stream-server-side
# gRPC有关的配置,这里只需要配置服务端口号
grpc:
server:
port: 9901
- The code for the startup class DoubleStreamServerSideApplication.java will not be posted, just the normal springboot startup class;
- The key point is GrpcServerService.java, which provides grpc service. What we have to do is to return an anonymous class to the upper framework. As for when the onNext and onCompleted methods are called, the upper framework determines when the onNext and onCompleted methods are called. In addition, the member variable totalCount is also prepared. The total number can be recorded. Because the request parameter is a stream, the onNext of the anonymous class will be called multiple times, and because the return value is a stream, the <font color="blue">responseObserver.onNext</font> method is called in onNext To respond to each request in the stream, so that the client continues to receive the response data from the server (that is, the client's onNext method will be called multiple times):
package grpctutorials;
import com.bolingcavalry.grpctutorials.lib.DeductReply;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import com.bolingcavalry.grpctutorials.lib.StockServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
@Slf4j
public class GrpcServerService extends StockServiceGrpc.StockServiceImplBase {
@Override
public StreamObserver<ProductOrder> batchDeduct(StreamObserver<DeductReply> responseObserver) {
// 返回匿名类,给上层框架使用
return new StreamObserver<ProductOrder>() {
private int totalCount = 0;
@Override
public void onNext(ProductOrder value) {
log.info("正在处理商品[{}],数量为[{}]",
value.getProductId(),
value.getNumber());
// 增加总量
totalCount += value.getNumber();
int code;
String message;
// 假设单数的都有库存不足的问题
if (0 == value.getNumber() % 2) {
code = 10000;
message = String.format("商品[%d]扣减库存数[%d]成功", value.getProductId(), value.getNumber());
} else {
code = 10001;
message = String.format("商品[%d]扣减库存数[%d]失败", value.getProductId(), value.getNumber());
}
responseObserver.onNext(DeductReply.newBuilder()
.setCode(code)
.setMessage(message)
.build());
}
@Override
public void onError(Throwable t) {
log.error("批量减扣库存异常", t);
}
@Override
public void onCompleted() {
log.info("批量减扣库存完成,共计[{}]件商品", totalCount);
responseObserver.onCompleted();
}
};
}
}
Develop client applications
- Create a new module named <font color="blue">double-stream-server-side</font> under the parent project grpc-turtorials, and its build.gradle content is as follows:
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'net.devh:grpc-client-spring-boot-starter'
implementation project(':grpc-lib')
}
- Configuration file application.yml, set your own web port number and server address:
server:
port: 8082
spring:
application:
name: double-stream-client-side
grpc:
client:
# gRPC配置的名字,GrpcClient注解会用到
double-stream-server-side:
# gRPC服务端地址
address: 'static://127.0.0.1:9901'
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
- The code for the startup class DoubleStreamClientSideApplication.java will not be posted, just the normal springboot startup class;
- Under normal circumstances, we use StreamObserver to process the server response. Because it is an asynchronous response, we need an additional method to retrieve business data from the StreamObserver, so we set a new interface, inherited from StreamObserver, and add <font color="blue"> The getExtra</font> method can return a String object. The detailed usage will be seen later:
package com.bolingcavalry.grpctutorials;
import io.grpc.stub.StreamObserver;
public interface ExtendResponseObserver<T> extends StreamObserver<T> {
String getExtra();
}
- Here comes the highlight, let’s take a look at how to remotely call the gRPC interface of the bidirectional stream type. Detailed comments have been added to the code:
package grpctutorials;
import com.bolingcavalry.grpctutorials.lib.DeductReply;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import com.bolingcavalry.grpctutorials.lib.StockServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class GrpcClientService {
@GrpcClient("double-stream-server-side")
private StockServiceGrpc.StockServiceStub stockServiceStub;
/**
* 批量减库存
* @param count
* @return
*/
public String batchDeduct(int count) {
CountDownLatch countDownLatch = new CountDownLatch(1);
// responseObserver的onNext和onCompleted会在另一个线程中被执行,
// ExtendResponseObserver继承自StreamObserver
ExtendResponseObserver<DeductReply> responseObserver = new ExtendResponseObserver<DeductReply>() {
// 用stringBuilder保存所有来自服务端的响应
private StringBuilder stringBuilder = new StringBuilder();
@Override
public String getExtra() {
return stringBuilder.toString();
}
/**
* 客户端的流式请求期间,每一笔请求都会收到服务端的一个响应,
* 对应每个响应,这里的onNext方法都会被执行一次,入参是响应内容
* @param value
*/
@Override
public void onNext(DeductReply value) {
log.info("batch deduct on next");
// 放入匿名类的成员变量中
stringBuilder.append(String.format("返回码[%d],返回信息:%s<br>" , value.getCode(), value.getMessage()));
}
@Override
public void onError(Throwable t) {
log.error("batch deduct gRPC request error", t);
stringBuilder.append("batch deduct gRPC error, " + t.getMessage());
countDownLatch.countDown();
}
/**
* 服务端确认响应完成后,这里的onCompleted方法会被调用
*/
@Override
public void onCompleted() {
log.info("batch deduct on complete");
// 执行了countDown方法后,前面执行countDownLatch.await方法的线程就不再wait了,
// 会继续往下执行
countDownLatch.countDown();
}
};
// 远程调用,此时数据还没有给到服务端
StreamObserver<ProductOrder> requestObserver = stockServiceStub.batchDeduct(responseObserver);
for(int i=0; i<count; i++) {
// 每次执行onNext都会发送一笔数据到服务端,
// 服务端的onNext方法都会被执行一次
requestObserver.onNext(build(101 + i, 1 + i));
}
// 客户端告诉服务端:数据已经发完了
requestObserver.onCompleted();
try {
// 开始等待,如果服务端处理完成,那么responseObserver的onCompleted方法会在另一个线程被执行,
// 那里会执行countDownLatch的countDown方法,一但countDown被执行,下面的await就执行完毕了,
// await的超时时间设置为2秒
countDownLatch.await(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("countDownLatch await error", e);
}
log.info("service finish");
// 服务端返回的内容被放置在requestObserver中,从getExtra方法可以取得
return responseObserver.getExtra();
}
/**
* 创建ProductOrder对象
* @param productId
* @param num
* @return
*/
private static ProductOrder build(int productId, int num) {
return ProductOrder.newBuilder().setProductId(productId).setNumber(num).build();
}
}
- Finally, a web interface can be used to verify remote calls through web requests:
package grpctutorials;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GrpcClientController {
@Autowired
private GrpcClientService grpcClientService;
@RequestMapping("/")
public String printMessage(@RequestParam(defaultValue = "1") int count) {
return grpcClientService.batchDeduct(count);
}
}
- Encoding is complete, start verification;
verify
- Start the server DoubleStreamServerSideApplication:
- Start the client DoubleStreamClientSideApplication:
- Here to change: browser input <font color="blue"> http://localhost:8083/?count=10 </font>, the response is as follows, it can be seen that the remote call to the gRPC service is successful, and every stream response All pen returns are received by the client:
- The following is the server log, it can be seen that each data of the client has been processed one by one:
- The following is the client log. It can be seen that due to the role of CountDownLatch, the thread that initiated the gRPC request has been waiting for responseObserver.onCompleted to be executed in another thread before continuing:
- At this point, the development of the four types of gRPC services and their clients has been completed, and we can handle general business scenarios with ease. In the next article, we will continue to study in depth to understand gRPC operations in complex scenarios;
You are not alone, Xinchen and original are with you all the way
- Java series
- Spring series
- Docker series
- kubernetes series
- Database + Middleware Series
- DevOps series
Welcome to pay attention to the public account: programmer Xin Chen
Search "Programmer Xin Chen" on WeChat, I am Xin Chen, and I look forward to traveling the Java world with you...
https://github.com/zq2599/blog_demos
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。