构建 gRPC-Web 服务端
本文档将向你展示如何利用 Vert.x 与 gRPC-Web 构建现代浏览器端到服务端的全栈应用。
你将构建什么
该应用涉及一个浏览器前端客户端以及一个 Vert.x 后端服务器:
-
用户在文本框中输入姓名并点击发送按钮;
-
浏览器通过 gRPC-Web 协议将姓名发送至服务器;
-
服务器响应问候消息;
-
浏览器在界面上渲染该问候语。
在服务端,你将创建一个 Vert.x gRPC 服务:
-
实现 gRPC 服务端存根(Stub);
-
配置同时响应 gRPC-Web 与静态前端资源文件的 HTTP 服务器。
在客户端,你将创建一个集成 gRPC-Web JavaScript 客户端的前端网页。
前置要求
-
文本编辑器或 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
打开浏览器访问 http://localhost:8080/,在文本框中输入姓名并点击按钮,即可在开发者工具控制台与界面上查看到基于 gRPC-Web 传输的问候语!
总结
本文档涵盖了:
-
使用 Protobuf 契约定义 gRPC 接口并由插件自动化编译;
-
在 Vert.x 中实现 gRPC-Web 兼容的服务端端点;
-
在浏览器前端通过 JavaScript gRPC-Web 客户端直接调用后端服务。