HTTP 与 Web 开发

构建 gRPC-Web 服务端

本文档将向你展示如何利用 Vert.x 与 gRPC-Web 构建现代浏览器端到服务端的全栈应用。

你将构建什么

该应用涉及一个浏览器前端客户端以及一个 Vert.x 后端服务器:

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

  • 浏览器通过 gRPC-Web 协议将姓名发送至服务器;

  • 服务器响应问候消息;

  • 浏览器在界面上渲染该问候语。

在服务端,你将创建一个 Vert.x gRPC 服务:

  • 实现 gRPC 服务端存根(Stub);

  • 配置同时响应 gRPC-Web 与静态前端资源文件的 HTTP 服务器。

在客户端,你将创建一个集成 gRPC-Web JavaScript 客户端的前端网页。

run

前置要求

  • 文本编辑器或 IDE

  • Java 17 或更高版本

你无需在本地手动安装 protoc 编译器或 vertx-grpc-protoc-plugin2、protobuf-javascript、protoc-gen-grpc-web 等插件,因为它们都已由 Maven 插件自动下载与管理。

创建工程项目

gRPC 服务契约定义

gRPC 的 Greeter 服务仅包含单个 SayHello RPC 方法。 协议文件位于 src/main/proto/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;
}

服务端实现

服务端通过继承生成的 VertxGreeterGrpcService.GreeterVertxImplBase 抽象类实现业务逻辑:

package io.vertx.howtos.grpcweb;

import io.vertx.core.Future;
import io.vertx.core.Completable;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.streams.ReadStream;
import io.vertx.core.streams.WriteStream;
import io.vertx.grpc.common.GrpcStatus;
import io.vertx.grpc.common.ServiceName;
import io.vertx.grpc.common.ServiceMethod;
import io.vertx.grpc.common.GrpcMessageDecoder;
import io.vertx.grpc.common.GrpcMessageEncoder;
import io.vertx.grpc.server.GrpcServerRequest;
import io.vertx.grpc.server.GrpcServer;
import io.vertx.grpc.server.Service;
import io.vertx.grpc.server.ServiceBuilder;

import com.google.protobuf.Descriptors;

import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>Provides support for RPC methods implementations of the Greeter gRPC service.</p>
 *
 * <p>The following methods of this class should be overridden to provide an implementation of the service:</p>
 * <ul>
 *   <li>SayHello</li>
 * </ul>
 */
public class VertxGreeterService implements VertxGreeter {

  /**
   * Override this method to implement the SayHello RPC.
   */
  public Future<io.vertx.howtos.grpcweb.HelloReply> sayHello(io.vertx.howtos.grpcweb.HelloRequest request) {
    throw new UnsupportedOperationException("Not implemented");
  }

  protected void sayHello(io.vertx.howtos.grpcweb.HelloRequest request, Completable<io.vertx.howtos.grpcweb.HelloReply> response) {
    sayHello(request).onComplete(response);
  }
}

在 ServerVerticle 中,我们配置 Vert.x HTTP 服务器并挂载 gRPC 处理器与静态资源处理器:

package io.vertx.howtos.grpcweb;

import io.vertx.core.Future;
import io.vertx.core.VerticleBase;
import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.grpc.server.GrpcServer;

public class ServerVerticle extends VerticleBase {

  @Override
  public Future<?> start() {
    // tag::grpcServer[]
    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);
    // end::grpcServer[]

    // tag::routerAndServer[]
    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);
    // end::routerAndServer[]
  }

  // tag::main[]
  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new ServerVerticle()).await();
    System.out.println("Server started, browse to http://localhost:8080");
  }
  // end::main[]
}

网页端实现

网页端 src/main/resources/webroot/index.html 引入打包好的前端 JS 脚本并调用 gRPC 服务:

<!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>

前端脚本 src/main/web/index.js 初始化客户端存根并发送请求:

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
}

运行与调试

执行 Maven 命令启动服务端:

./mvnw clean compile exec:java
devtools

总结

本文档涵盖了:

  • 使用 Protobuf 契约定义 gRPC 接口并由插件自动化编译;

  • 在 Vert.x 中实现 gRPC-Web 兼容的服务端端点;

  • 在浏览器前端通过 JavaScript gRPC-Web 客户端直接调用后端服务。