java version of gRPC practice 5: two-way flow

Links to the full series of "java version gRPC actual combat"

  1. Generate code with proto
  2. Service publishing and invocation
  3. Server stream
  4. Client stream
  5. Bidirectional flow
  6. The client dynamically obtains the server address
  7. Registration discovery based on eureka

Overview of this article

  • This article is the fifth in the series of gRPC practice in java. The goal is to master the service of two-way flow type, that is, the request parameter is in the form of flow, and the content of response is also in the form of flow;
  • Let's take a look at the introduction of two-way streaming RPC in the official data: both sides use read-write streams to send a message sequence. The two streams operate independently, so the client and server can read and write in any desired order: for example, the server can wait to receive all client messages before writing a response, or can read and write messages alternately, or other combinations of reading and writing. The message sequence in each flow is reserved;
  • After mastering the development of client-side flow and server-side flow, the two-way flow type is well understood, which is the combination of the previous two types. The request and response can be processed in the way of flow;
  • In today's actual combat, let's design the function of an online mall: batch inventory reduction, that is, the client submits multiple goods and quantities, and the server returns the success and failure of inventory reduction for each commodity;
  • Let's start coding as soon as possible. The details are as follows:
  1. Define the gRPC interface of bidirectional flow type in the proto file, and then generate java code through proto
  2. Developing server applications
  3. Developing client applications
  4. verification

Source download

  • The complete source code in this actual combat can be downloaded from GitHub. The address and link information are shown in the table below( https://github.com/zq2599/blog_demos):

name

link

remarks

Project Home

https://github.com/zq2599/blog_demos

The project is on the GitHub home page

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 project source code warehouse address, ssh protocol

  • There are multiple folders in the git project. The source code of the gRPC practical combat series for java is in the gRPC tutorials folder, as shown in the red box below:
  • There are multiple directories in the grpc tutorials folder. The server code corresponding to this article is in the double stream server side directory, and the client code is in the double stream client side directory, as shown in the following figure:

Define gRPC interface of bidirectional flow type in proto file

  • The first thing to do is to define the gRPC interface, open mall.proto, and add new methods and related data structures in it. What needs to be paid attention to is that the input parameter ProductOrder and return value DeductReply of the batchproduce method are added with stream decoration (ProductOrder is defined in the previous chapter), which means that the method is a two-way flow type:
// gRPC service, which is the inventory service of an online mall
service StockService {
    // Two way flow: batch deduction of inventory
    rpc BatchDeduct (stream ProductOrder) returns (stream DeductReply) {}
}

// Data structure of the return result of inventory deduction
message DeductReply {
    // Return code
    int32 code = 1;
    // Description information
    string message = 2;
}
  • Double click the task in the red box below to generate java code:
  • Generate the file in the red box below, that is, the server definition and return value data structure:
  • Next, develop the server;

Developing server applications

  • Create a new module named double stream server side under the parent project grpc tutorials, and its build.gradle content is as follows:
// Using the springboot plug-in
plugins {
    id 'org.springframework.boot'
}

dependencies {
    implementation 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter'
    // As a gRPC service provider, you need to use this library
    implementation 'net.devh:grpc-server-spring-boot-starter'
    // Projects that rely on automatic source code generation
    implementation project(':grpc-lib')
    // The annotation processor will not be passed. Modules that use lombok generated code need to declare the annotation processor themselves
    annotationProcessor 'org.projectlombok:lombok'
}
  • Configuration file application.yml:
spring:
  application:
    name: double-stream-server-side
# For the configuration related to gRPC, only the service port number needs to be configured here
grpc:
  server:
    port: 9901
  • The code of the startup class DoubleStreamServerSideApplication.java will not be pasted. It is just an ordinary springboot startup class;
  • The key point is GrpcServerService.java, which provides grpc service. What we need to do is to return an anonymous class to the upper framework. When the onnext and onCompleted methods are called is determined by the upper framework. In addition, the member variable totalCount is prepared so that the total can be recorded. Since the request parameter is flow, the onnext of the anonymous class will be called many times, And because the return value is the flow, onNext calls the responseObserver.onNext method to respond to every request in the stream, so that the client will receive the response data of the server side (that is, the onNext method of the client will be called many 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) {
        // Returns an anonymous class for use by the upper framework
        return new StreamObserver<ProductOrder>() {

            private int totalCount = 0;

            @Override
            public void onNext(ProductOrder value) {
                log.info("Processing item[{}],Quantity is[{}]",
                        value.getProductId(),
                        value.getNumber());

                // Increase total
                totalCount += value.getNumber();

                int code;
                String message;

                // It is assumed that the odd number has the problem of insufficient inventory
                if (0 == value.getNumber() % 2) {
                    code = 10000;
                    message = String.format("commodity[%d]Deduct inventory[%d]success", value.getProductId(), value.getNumber());
                } else {
                    code = 10001;
                    message = String.format("commodity[%d]Deduct inventory[%d]fail", value.getProductId(), value.getNumber());
                }

                responseObserver.onNext(DeductReply.newBuilder()
                        .setCode(code)
                        .setMessage(message)
                        .build());
            }

            @Override
            public void onError(Throwable t) {
                log.error("Batch deduction inventory exception", t);
            }

            @Override
            public void onCompleted() {
                log.info("Batch deduction inventory completed, total[{}]Pieces of goods", totalCount);
                responseObserver.onCompleted();
            }
        };
    }
}

Developing client applications

  • Create a new module named double stream server side under the parent project grpc tutorials, 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')
}
  • Configure the file application.yml and set your own web port number and server address:
server:
  port: 8082
spring:
  application:
    name: double-stream-client-side

grpc:
  client:
    # The name of gRPC configuration. GrpcClient annotation will be used
    double-stream-server-side:
      # gRPC server address
      address: 'static://127.0.0.1:9901'
      enableKeepAlive: true
      keepAliveWithoutCalls: true
      negotiationType: plaintext
  • The code of the startup class DoubleStreamClientSideApplication.java will not be pasted. It is just an ordinary springboot startup class;
  • Under normal circumstances, we use StreamObserver to process the server response. Because it is an asynchronous response, we need additional methods to fetch business data from StreamObserver, so we set a new interface and inherit it from StreamObserver. The new getExtra method can return String objects. We will see the detailed usage later:
package com.bolingcavalry.grpctutorials;

import io.grpc.stub.StreamObserver;

public interface ExtendResponseObserver<T> extends StreamObserver<T> {
    String getExtra();
}
  • Here's the play. Let's see how to remotely call the gRPC interface of bidirectional flow type. Detailed comments have been added in 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;

    /**
     * Batch inventory reduction
     * @param count
     * @return
     */
    public String batchDeduct(int count) {

        CountDownLatch countDownLatch = new CountDownLatch(1);

        // onNext and onCompleted of responseObserver will be executed in another thread,
        // ExtendResponseObserver inherits from StreamObserver
        ExtendResponseObserver<DeductReply> responseObserver = new ExtendResponseObserver<DeductReply>() {

            // Save all the responses from the server with stringBuilder
            private StringBuilder stringBuilder = new StringBuilder();

            @Override
            public String getExtra() {
                return stringBuilder.toString();
            }

            /**
             * During the streaming request of the client, each request will receive a response from the server,
             * For each response, the onNext method here will be executed once, and the input parameter is the response content
             * @param value
             */
            @Override
            public void onNext(DeductReply value) {
                log.info("batch deduct on next");
                // Put it into the member variable of the anonymous class
                stringBuilder.append(String.format("Return code[%d],Return information:%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();
            }

            /**
             * After the server confirms that the response is complete, the onCompleted method here will be called
             */
            @Override
            public void onCompleted() {
                log.info("batch deduct on complete");
                // After the countDown method is executed, the thread previously executing the countDownLatch.await method will no longer wait,
                // Will continue
                countDownLatch.countDown();
            }
        };

        // Remote call. At this time, the data has not been given to the server
        StreamObserver<ProductOrder> requestObserver = stockServiceStub.batchDeduct(responseObserver);

        for(int i=0; i<count; i++) {
            // Each time you execute onNext, you will send a data to the server,
            // The onNext method of the server will be executed once
            requestObserver.onNext(build(101 + i, 1 + i));
        }

        // The client tells the server that the data has been sent
        requestObserver.onCompleted();

        try {
            // Start waiting. If the server processing is completed, the onCompleted method of responseObserver will be executed in another thread,
            // The countDown method of countDownLatch will be executed there. Once countDown is executed, the following await will be executed,
            // The timeout for await is set to 2 seconds
            countDownLatch.await(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("countDownLatch await error", e);
        }

        log.info("service finish");
        // The content returned by the server is placed in requestObserver and can be obtained from getExtra method
        return responseObserver.getExtra();
    }

    /**
     * Create ProductOrder object
     * @param productId
     * @param num
     * @return
     */
    private static ProductOrder build(int productId, int num) {
        return ProductOrder.newBuilder().setProductId(productId).setNumber(num).build();
    }
}
  • Finally, make a web interface to verify the remote call through the web request:
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);
    }
}
  • After coding, start verification;

verification

  • Start the server-side DoubleStreamServerSideApplication:
  • Start client DoubleStreamClientSideApplication:
  • Here to change: Browser input http://localhost:8083/?count=10 , the response is as follows. It can be seen that the remote call to gRPC service is successful, and each return of streaming response is received by the client:
  • The following is the server log. It can be seen that each data of the client is processed one by one:
  • The following is the client log. It can be seen that due to the function of CountDownLatch, the thread initiating the gRPC request has been waiting for responseObserver.onCompleted, and will not continue to execute until another thread is executed:
  • So far, the development of the four types of GRC services and their clients has been completed. We can handle the general business scenarios easily. In the next articles, we will continue to learn more about GRC operations in complex scenarios;

Posted on Mon, 06 Dec 2021 23:38:39 -0500 by stride-r