java version of gRPC practice 4: client stream

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 fourth in the "java version gRPC practice" series. The previous article has mastered the server-side stream, which is suitable for obtaining a large amount of data from the server. Today's goal is to master the client-side stream type services, including the development of both service providers and users;
  • Let's take a look at the official information on client streaming RPC: the client writes a message sequence and sends it to the server, which also uses streaming. Once the client completes writing the message, it waits for the server to complete reading and return its response;
  • This paper consists of the following parts:
  1. Summarize several important knowledge points in advance and focus on them later in the development process;
  2. Define the gRPC interface of the client stream type in the proto file, and then generate java code through proto;
  3. Develop server applications;
  4. Develop client applications;
  5. verification;

Advance summary

In order to highlight the key points, several key knowledge points are given in advance:

  1. The characteristic of client stream is that the requester submits data to the responder in the form of stream;
  2. In an RPC request, the requester can submit data in a continuous stream until the onCompleted method of StreamObserver is called;
  3. Normally, when we call a method, the data used inside the method is transmitted through the input parameter, but it is different here. The data to be transmitted from the client to the server has nothing to do with the input parameter of the gRPC method, but is related to the return object of the method (the onNext method executing the return object can transmit the data to the server);
  4. After the client uploads the data in thread A, the server's response is executed in another thread B. therefore, if thread A gets the server's response, the asynchronous response method of thread B must be executed. There are many waiting methods. I use CountDownLatch;
  5. On the server side, the code to be written by the developer is different from the previous web development. Instead of processing and returning the data, it returns a StreamObserver instance to the upper framework, which is responsible for the processing logic. The developer can focus on developing the implementation of StreamObserver, such as rewriting the onNext method, and the client uploads each data through the stream, The onNext method will be executed once by the outer framework;
  6. If you use IDEA, remember to check the box in the red box below, otherwise you may encounter lombok related problems when running the application:
  • These mentioned above will be fully reflected in the next development process;

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 client stream server side directory, and the client code is in the client stream client side directory, as shown in the following figure:

Define the gRPC interface of the client stream type in the 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 stream modifier is added in front of the input parameter ProductOrder of the AddToCart method, which means that the method is a client stream type:
// gRPC service, which is a shopping cart service in an online mall
service CartService {
    // Client streaming: add multiple items to shopping cart
    rpc AddToCart (stream ProductOrder) returns (AddCartReply) {}
}

// Product information when submitting shopping cart
message ProductOrder {
    // Item ID
    int32 productId = 1;
    // Quantity of goods
    int32 number = 2;
}

// Data structure for submitting shopping cart return results
message AddCartReply {
    // 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:
  • Next, develop the server;

Developing server applications

  • Create a new module named client stream server side under the parent project grpc tutorials. The content of build.gradle 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: client-stream-server-side
# For the configuration related to gRPC, only the service port number needs to be configured here
grpc:
  server:
    port: 9900
  • The code of the startup class ClientStreamServerSideApplication.java will not be pasted. It is just an ordinary springboot startup class;
  • The key point is GrpcServerService.java, which provides grpc service. Please read the code in combination with the fifth point of the previous summary. 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 to record the total number:
package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.AddCartReply;
import com.bolingcavalry.grpctutorials.lib.CartServiceGrpc;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
@Slf4j
public class GrpcServerService extends CartServiceGrpc.CartServiceImplBase {

    @Override
    public StreamObserver<ProductOrder> addToCart(StreamObserver<AddCartReply> responseObserver) {
        // Returns an anonymous class for use by the upper framework
        return new StreamObserver<ProductOrder>() {

            // Record the total amount of products processed
            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();
            }

            @Override
            public void onError(Throwable t) {
                log.error("Add shopping cart exception", t);
            }

            @Override
            public void onCompleted() {
                log.info("Add shopping cart complete, total[{}]Pieces of goods", totalCount);
                responseObserver.onNext(AddCartReply.newBuilder()
                                                    .setCode(10000)
                                                    .setMessage(String.format("Add shopping cart complete, total[%d]Pieces of goods", totalCount))
                                                    .build());
                responseObserver.onCompleted();
            }
        };
    }
}

Developing client applications

  • Create a new module named client stream server side under the parent project grpc tutorials. The content of build.gradle 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: client-stream-client-side

grpc:
  client:
    # The name of gRPC configuration. GrpcClient annotation will be used
    client-stream-server-side:
      # gRPC server address
      address: 'static://127.0.0.1:9900'
      enableKeepAlive: true
      keepAliveWithoutCalls: true
      negotiationType: plaintext
  • The code of the startup class ClientStreamClientSideApplication.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 client stream type. Points 2, 3 and 4 mentioned in the previous summary will be involved. Detailed comments have been added in the code:
package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.AddCartReply;
import com.bolingcavalry.grpctutorials.lib.CartServiceGrpc;
import com.bolingcavalry.grpctutorials.lib.ProductOrder;
import io.grpc.stub.StreamObserver;
import lombok.Getter;
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("client-stream-server-side")
    private CartServiceGrpc.CartServiceStub cartServiceStub;

    public String addToCart(int count) {
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        // onNext and onCompleted of responseObserver will be executed in another thread,
        // ExtendResponseObserver inherits from StreamObserver
        ExtendResponseObserver<AddCartReply> responseObserver = new ExtendResponseObserver<AddCartReply>() {

            String extraStr;

            @Override
            public String getExtra() {
                return extraStr;
            }

            private int code;

            private String message;

            @Override
            public void onNext(AddCartReply value) {
                log.info("on next");
                code = value.getCode();
                message = value.getMessage();
            }

            @Override
            public void onError(Throwable t) {
                log.error("gRPC request error", t);
                extraStr = "gRPC error, " + t.getMessage();
                countDownLatch.countDown();
            }

            @Override
            public void onCompleted() {
                log.info("on complete");
                extraStr = String.format("Return code[%d],Return information:%s" , code, message);
                countDownLatch.countDown();
            }
        };
        
        // Remote call. At this time, the data has not been given to the server
        StreamObserver<ProductOrder> requestObserver = cartServiceStub.addToCart(responseObserver);
        
        for(int i=0; i<count; i++) {
            // Send a data to the server
            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 com.bolingcavalry.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;
import java.util.List;

@RestController
public class GrpcClientController {

    @Autowired
    private GrpcClientService grpcClientService;

    @RequestMapping("/")
    public String printMessage(@RequestParam(defaultValue = "1") int count) {
        return grpcClientService.addToCart(count);
    }
}
  • After coding, start verification;

verification

  • Start the client streamserversideapplication on the server:
  • Start client streamclientsideapplication:
  • Browser input http://localhost:8082/?count=100 , the response is as follows. It can be seen that the remote call to gRPC service succeeded:
  • 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 gRPC service of client stream type and its client development have been completed. This asynchronous operation is still different from the synchronous web interface we usually develop. I hope this paper can bring you some references. In the next article, we will practice the last type: two-way streaming;

Posted by gabrielp on Tue, 07 Dec 2021 00:10:16 -0800