java版gRPC實戰之三:服務端流

程式設計師欣宸 發表於 2021-09-15
Java

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

《java版gRPC實戰》全系列連結

  1. 用proto生成程式碼
  2. 服務釋出和呼叫
  3. 服務端流
  4. 客戶端流
  5. 雙向流
  6. 客戶端動態獲取服務端地址
  7. 基於eureka的註冊發現

關於gRPC定義的四種型別

本文是《java版gRPC實戰》系列的第三篇,前文我們們實戰體驗了簡單的RPC請求和響應,那種簡單的請求響應方式其實只是gRPC定義的四種型別之一,這裡給出《gRPC 官方文件中文版》對這四種gRPC型別的描述:

  1. 簡單 RPC:客戶端使用存根(stub)傳送請求到伺服器並等待響應返回,就像平常的函式呼叫一樣;
  2. 伺服器端流式 RPC:客戶端傳送請求到伺服器,拿到一個流去讀取返回的訊息序列。 客戶端讀取返回的流,直到裡面沒有任何訊息;(即本篇內容)
  3. 客戶端流式 RPC:客戶端寫入一個訊息序列並將其傳送到伺服器,同樣也是使用流。一旦 客戶端完成寫入訊息,它等待伺服器完成讀取返回它的響應;
  4. 雙向流式 RPC:是雙方使用讀寫流去傳送一個訊息序列。兩個流獨立操作,因此客戶端和伺服器 可以以任意喜歡的順序讀寫:比如, 伺服器可以在寫入響應前等待接收所有的客戶端訊息,或者可以交替 的讀取和寫入訊息,或者其他讀寫的組合。 每個流中的訊息順序被預留;

本篇概覽

本篇是服務端流型別的gRPC服務實戰,包括以下內容:

  1. 開發一個gRPC服務,型別是服務端流;
  2. 開發一個客戶端,呼叫前面釋出的gRPC服務;
  3. 驗證;
  • 不多說了,開始上程式碼;

原始碼下載

名稱 連結 備註
專案主頁 https://github.com/zq2599/blog_demos 該專案在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該專案原始碼的倉庫地址,https協議
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該專案原始碼的倉庫地址,ssh協議
  • 這個git專案中有多個資料夾,《java版gRPC實戰》系列的原始碼在grpc-tutorials資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • grpc-tutorials資料夾下有多個目錄,本篇文章對應的服務端程式碼在server-stream-server-side目錄下,客戶端程式碼在server-stream-client-side目錄下,如下圖:

在這裡插入圖片描述

開發一個gRPC服務,型別是服務端流

  • 首先要開發的是gRPC服務端,一共要做下圖所示的七件事:

在這裡插入圖片描述

  • 開啟grpc-lib模組,在src/main/proto目錄下新增檔案mall.proto,裡面定一個了一個gRPC方法ListOrders及其入參和返回物件,內容如下,要注意的是返回值要用關鍵字stream修飾,表示該介面型別是服務端流:
syntax = "proto3";

option java_multiple_files = true;
// 生成java程式碼的package
option java_package = "com.bolingcavalry.grpctutorials.lib";
// 類名
option java_outer_classname = "MallProto";

// gRPC服務,這是個線上商城的訂單查詢服務
service OrderQuery {
    // 服務端流式:訂單列表介面,入參是買家資訊,返回訂單列表(用stream修飾返回值)
    rpc ListOrders (Buyer) returns (stream Order) {}
}

// 買家ID
message Buyer {
    int32 buyerId = 1;
}

// 返回結果的資料結構
message Order {
    // 訂單ID
    int32 orderId = 1;
    // 商品ID
    int32 productId = 2;
    // 交易時間
    int64 orderTime = 3;
    // 買家備註
    string buyerRemark = 4;
}
  • 雙擊下圖紅框位置的generateProto,即可根據proto生成java程式碼:

在這裡插入圖片描述

  • 新生成的java程式碼如下圖紅框:

在這裡插入圖片描述

  • 在父工程grpc-turtorials下面新建名為server-stream-server-side的模組,其build.gradle內容如下:
// 使用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')
}
  • 新建配置檔案application.yml:
spring:
  application:
    name: server-stream-server-side
# gRPC有關的配置,這裡只需要配置服務埠號
grpc:
  server:
    port: 9899
  • 啟動類:
package com.bolingcavalry.grpctutorials;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerStreamServerSideApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerStreamServerSideApplication.class, args);
    }
}
  • 接下來是最關鍵的gRPC服務,程式碼如下,可見responseObserver.onNext方法被多次呼叫,用以向客戶端持續輸出資料,最後通過responseObserver.onCompleted結束輸出:
package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.Buyer;
import com.bolingcavalry.grpctutorials.lib.Order;
import com.bolingcavalry.grpctutorials.lib.OrderQueryGrpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
import java.util.ArrayList;
import java.util.List;

@GrpcService
public class GrpcServerService extends OrderQueryGrpc.OrderQueryImplBase {

    /**
     * mock一批資料
     * @return
     */
    private static List<Order> mockOrders(){
        List<Order> list = new ArrayList<>();
        Order.Builder builder = Order.newBuilder();

        for (int i = 0; i < 10; i++) {
            list.add(builder
                    .setOrderId(i)
                    .setProductId(1000+i)
                    .setOrderTime(System.currentTimeMillis()/1000)
                    .setBuyerRemark(("remark-" + i))
                    .build());
        }

        return list;
    }

    @Override
    public void listOrders(Buyer request, StreamObserver<Order> responseObserver) {
        // 持續輸出到client
        for (Order order : mockOrders()) {
            responseObserver.onNext(order);
        }
        // 結束輸出
        responseObserver.onCompleted();
    }
}
  • 至此,服務端開發完成,我們們再開發一個springboot應用作為客戶端,看看如何遠端呼叫listOrders介面,得到responseObserver.onNext方法輸出的資料;

開發一個客戶端,呼叫前面釋出的gRPC服務

  • 客戶端模組的基本功能是提供一個web介面,其內部會呼叫服務端的listOrders介面,將得到的資料返回給前端,如下圖:

在這裡插入圖片描述

  • 在父工程grpc-turtorials下面新建名為server-stream-client-side的模組,其build.gradle內容如下:
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')
}
  • 應用配置資訊application.yml內容如下,可見是埠和gRPC服務端地址的配置:
server:
  port: 8081
spring:
  application:
    name: server-stream-client-side

grpc:
  client:
    # gRPC配置的名字,GrpcClient註解會用到
    server-stream-server-side:
      # gRPC服務端地址
      address: 'static://127.0.0.1:9899'
      enableKeepAlive: true
      keepAliveWithoutCalls: true
      negotiationType: plaintext
  • 服務端的listOrders介面返回的Order物件裡面有很多gRPC相關的內容,不適合作為web介面的返回值,因此定義一個DispOrder類作為web介面返回值:
package com.bolingcavalry.grpctutorials;

import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;

@Data
@AllArgsConstructor
public class DispOrder {
    private int orderId;
    private int productId;
    private String orderTime;
    private String buyerRemark;
}
  • 平淡無奇的啟動類:
package com.bolingcavalry.grpctutorials;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ServerStreamClientSideApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerStreamClientSideApplication.class, args);
    }
}
  • 重點來了,GrpcClientService.java,裡面展示瞭如何遠端呼叫gRPC服務的listOrders介面,可見對於服務端流型別的介面,客戶端這邊通過stub呼叫會得到Iterator型別的返回值,接下來要做的就是遍歷Iterator:
package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.Buyer;
import com.bolingcavalry.grpctutorials.lib.Order;
import com.bolingcavalry.grpctutorials.lib.OrderQueryGrpc;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

@Service
@Slf4j
public class GrpcClientService {

    @GrpcClient("server-stream-server-side")
    private OrderQueryGrpc.OrderQueryBlockingStub orderQueryBlockingStub;

    public List<DispOrder> listOrders(final String name) {
        // gRPC的請求引數
        Buyer buyer = Buyer.newBuilder().setBuyerId(101).build();

        // gRPC的響應
        Iterator<Order> orderIterator;

        // 當前方法的返回值
        List<DispOrder> orders = new ArrayList<>();

        // 通過stub發起遠端gRPC請求
        try {
            orderIterator = orderQueryBlockingStub.listOrders(buyer);
        } catch (final StatusRuntimeException e) {
            log.error("error grpc invoke", e);
            return new ArrayList<>();
        }

        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

        log.info("start put order to list");
        while (orderIterator.hasNext()) {
            Order order = orderIterator.next();

            orders.add(new DispOrder(order.getOrderId(),
                                    order.getProductId(),
                                    // 使用DateTimeFormatter將時間戳轉為字串
                                    dtf.format(LocalDateTime.ofEpochSecond(order.getOrderTime(), 0, ZoneOffset.of("+8"))),
                                    order.getBuyerRemark()));
            log.info("");
        }

        log.info("end put order to list");

        return orders;
    }
}
  • 最後做一個controller類,對外提供一個web介面,裡面會呼叫GrpcClientService的方法:
package com.bolingcavalry.grpctutorials;

import com.bolingcavalry.grpctutorials.lib.Order;
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 List<DispOrder> printMessage(@RequestParam(defaultValue = "will") String name) {
        return grpcClientService.listOrders(name);
    }
}
  • 至此,編碼完成,開始驗證

驗證

  1. 啟動server-stream-server-side,啟動成功後會監聽9989埠:

在這裡插入圖片描述

  1. 啟動server-stream-client-side,再在瀏覽器訪問:http://localhost:8081/?name=Tom ,得到結果如下(firefox自動格式化json資料),可見成功地獲取了gRPC的遠端資料:

在這裡插入圖片描述

至此,服務端流型別的gRPC介面的開發和使用實戰就完成了,接下來的章節還會繼續學習另外兩種型別;

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos