HTTP 与 Web

构建 gRPC Web 服务

本文档将向您展示如何使用 Vert.x 和 gRPC Web 构建一个浏览器/服务器应用程序。

您将构建什么

该应用程序包含一个客户端(浏览器)和一个 Vert.x 服务器。

  • 用户在文本字段中输入姓名并点击发送按钮

  • 浏览器使用 gRPC Web 协议将姓名发送到服务器

  • 服务器回复问候语

  • 浏览器显示问候语

在服务器端,您将创建一个 Vert.x gRPC 服务器服务,它:

  • 实现 gRPC 服务器存根

  • 配置一个 HTTP 服务器,以响应 gRPC Web 和静态文件请求

在客户端,您将创建一个使用 gRPC Web Javascript 客户端的网页。

run

您需要什么

  • 文本编辑器或 IDE

  • Java 17 或更高版本

您无需安装 protoc 或像 vertx-grpc-protoc-plugin2、protobuf-javascript 和 protoc-gen-grpc-web 这样的 protoc 插件,它们将由 Maven 插件管理。

创建项目

gRPC 服务定义

gRPC Greeter 服务包含一个单独的 SayHello rpc 方法。 HelloRequest 消息包含客户端发送的姓名。 HelloReply 消息包含服务器生成的问候语。

service.proto 文件
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.vertx.howtos.grpcweb";
option java_outer_classname = "HelloWorldProto";

package helloworld;

// The greeting service definition.
service Greeter {
  // Ask for a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greeting
message HelloReply {
  string message = 1;
}

代码生成

根据服务定义,必须生成几个文件:

  • Java 消息和服务器类

  • Javascript 文件

  • gRPC Web 特定(Javascript)文件。

protoc 的调用由 protobuf-maven-plugin 管理。

使用 protobuf-maven-plugin 进行代码生成
<plugin>
  <groupId>io.github.ascopes</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>3.2.0</version>
  <configuration>
    <protocVersion>4.29.3</protocVersion>
    <sourceDirectories>src/main/proto</sourceDirectories>
    <javaEnabled>false</javaEnabled>
  </configuration>
  <executions>
    <execution>
      <id>compile-java</id>
      <configuration>
        <javaEnabled>true</javaEnabled>
        <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
        <jvmMavenPlugins>
          <jvmMavenPlugin>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-grpc-protoc-plugin2</artifactId>
            <version>${vertx.version}</version>
            <mainClass>io.vertx.grpc.plugin.VertxGrpcGenerator</mainClass>
            <jvmArgs>
              <jvmArg>--grpc-client=false</jvmArg>
              <jvmArg>--grpc-service</jvmArg>
              <jvmArg>--service-prefix=Vertx</jvmArg>
              <jvmArg>--vertx-codegen=false</jvmArg>
            </jvmArgs>
          </jvmMavenPlugin>
        </jvmMavenPlugins>
      </configuration>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
    <execution>
      <id>compile-javascript</id>
      <configuration>
        <outputDirectory>${project.basedir}/src/main/web</outputDirectory>
        <binaryUrlPlugins>
          <binaryUrlPlugin>
            <url>${protoc.gen.js.url}</url>
            <options>import_style=commonjs</options>
          </binaryUrlPlugin>
        </binaryUrlPlugins>
      </configuration>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
    <execution>
      <id>compile-javascript-web</id>
      <configuration>
        <outputDirectory>${project.basedir}/src/main/web</outputDirectory>
        <binaryUrlPlugins>
          <binaryUrlPlugin>
            <url>${protoc.gen.grpc.web.url}</url>
            <options>import_style=typescript,mode=grpcwebtext</options>
          </binaryUrlPlugin>
        </binaryUrlPlugins>
      </configuration>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
</plugin>
1 我们选择使用 CommonJS 模块生成方式而不是闭包。

服务器端

我们需要一些项目编译所需的依赖项

项目依赖项
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-stack-depchain</artifactId>
      <version>${vertx.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-web</artifactId>
  </dependency>
  <dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-grpc-server</artifactId>
  </dependency>

  <dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>${protobuf.version}</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-api</artifactId>
    <version>${grpc.version}</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>${grpc.version}</version>
  </dependency>
  <dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>${grpc.version}</version>
  </dependency>
</dependencies>

服务器端代码在一个单独的 ServerVerticle 类中。

首先是 gRPC 服务器存根实现。

gRPC 服务器存根实现
VertxGreeterGrpcService service = new VertxGreeterGrpcService() {
  @Override
  public Future<HelloReply> sayHello(HelloRequest request) {
    return Future.succeededFuture(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build());
  }
};

GrpcServer grpcServer = GrpcServer.server(vertx);
grpcServer.addService(service);

这里没有什么与 gRPC Web 特别相关的内容。

GrpcServer 默认启用 gRPC Web 协议支持。

然后我们必须配置一个 Vert.x Web Router 以接受 gRPC Web 和静态文件请求。

路由和服务器配置
Router router = Router.router(vertx);
router.route()
  .consumes("application/grpc-web-text") (1)
  .handler(rc -> grpcServer.handle(rc.request()));

router.get().handler(StaticHandler.create()); (2)

return vertx.createHttpServer()
  .requestHandler(router)
  .listen(8080);
1 所有内容类型为 application/grpc-web-text 的请求都将交由 grpcServer 处理。
2 所有其他 GET 请求将由 Vert.x Web StaticHandler 处理。

客户端

在编写代码之前,我们必须设置项目以构建客户端代码。为了简单起见,我们选择使用 esbuild-maven-plugin。简而言之,它是一个 Maven 插件,封装了 esbuild,一个用于 Web 的快速打包工具。

需要几个依赖项,我们通过 mvnpm 将它们作为 Maven 依赖项获取:

使用 esbuild-maven-plugin 构建客户端代码
<plugin>
  <groupId>io.mvnpm</groupId>
  <artifactId>esbuild-maven-plugin</artifactId>
  <version>0.0.2</version>
  <executions>
    <execution>
      <id>esbuild</id>
      <goals>
        <goal>esbuild</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <entryPoint>index.js</entryPoint>
    <outputDirectory>${project.build.outputDirectory}/webroot/js</outputDirectory> (1)
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.mvnpm</groupId>
      <artifactId>grpc-web</artifactId>
      <version>1.5.0</version>
    </dependency>
    <dependency>
      <groupId>org.mvnpm</groupId>
      <artifactId>google-protobuf</artifactId>
      <version>3.21.4</version>
    </dependency>
  </dependencies>
</plugin>
1 webroot 是 Vert.x Web StaticHandler 提供静态文件的默认基本目录。

用户界面代码在一个单独的 index.html 文件中。

用户界面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Echo Example</title>
  <script type="module">
    import {sayHello} from "/js/index.js"; (1)

    window.sayHello = sayHello; (2)
  </script>
</head>
<body>
<div>
  <p>Type a name in the input field and press enter, or click the send button.</p>
  <div class="input-group">
    <!--    Invoke javascript function on submit  -->
    <form onsubmit="return sayHello();">
      <input type="text" id="name">
      <input type="submit" value="Send">
    </form>
  </div>
  <p id="msg"></p>
</div>
</body>
</html>
1 从我们的 Javascript 模块中导入 sayHello 函数(见下文)。
2 使 sayHello 函数成为全局函数。

最后但同样重要的是,让我们实现 sayHello 函数:

使用 gRPC Web 客户端
const {HelloRequest} = require("./service_pb"); (1)
const {GreeterClient} = require("./service_grpc_web_pb"); (2)

const greeterClient = new GreeterClient("http://" + window.location.hostname + ":8080", null, null); (3)

export function sayHello() {

  const request = new HelloRequest();
  request.setName(document.getElementById("name").value);

  greeterClient.sayHello(request, {}, (err, response) => {
    const msgElem = document.getElementById("msg");
    if (err) {
      msgElem.innerText = `Unexpected error for sayHello: code = ${err.code}` + `, message = "${err.message}"`;
    } else {
      msgElem.innerText = response.getMessage();
    }
  });

  return false; // prevent form posting
}
1 从 Javascript 生成的文件中导入 HelloRequest 对象。
2 从 gRPC Web (Javascript) 生成的文件中导入 GreeterClient 对象。
3 配置客户端以向 Web 服务器发送请求。

运行应用程序

您可以使用 Maven 运行应用程序

./mvnw compile exec:java

您应该看到:

Server started, browse to https://:8080

您现在可以浏览到 https://:8080 并按照说明操作。

使用浏览器的开发工具检查 gRPC Web 流量。

devtools

总结

本文档涵盖了

  1. 实现一个响应静态文件和 gRPC Web 请求的 Vert.x web 服务器,

  2. 创建一个使用 gRPC Web 客户端的网页。