多数使用REST进行系统间通信的组件,其负载通常采用JSON格式序列化。截至目前,JSON缺乏一个广泛使用的模式验证标准:JSON Schema并未普及。标准化的模式验证允许将验证任务委托给第三方库处理,从而简化流程。若无统一标准,我们不得不在代码中进行手动验证。更糟的是,验证代码需与模式保持同步。
相比之下,XML内置了模式验证功能:XML文档可以声明必须遵循的语法规则。基于XML的SOAP也因此受益。
其他序列化方案也提供了模式验证选项,例如Avro、Kryo和Protocol Buffers。值得一提的是,gRPC采用Protobuf实现分布式组件间的RPC通信:
gRPC是一种现代的开源高性能远程过程调用(RPC)框架,适用于任何环境。它能在数据中心内部或跨数据中心高效连接服务,支持可插拔的负载均衡、跟踪、健康检查和认证。此外,它也适用于分布式计算的最后一英里,连接设备、移动应用和浏览器至后端服务。
此外,Protocol是一种二进制序列化机制,节省了大量带宽。因此,gRPC是系统间通信的绝佳选择。但如果所有组件都使用gRPC通信,那么简单的客户端如何调用它们呢?本文将构建一个gRPC服务,并展示如何通过cURL调用它。
A Simple gRPC Service
gRPC文档非常详尽,以下是概要总结:
- – gRPC是一个远程过程调用框架。
- – 它支持多种编程语言。
- – 它依赖于Protocol Buffers。
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
- – 它是CNCF项目的一部分,目前处于孵化阶段。
接下来,我们将搭建gRPC服务。我们将使用Java、Kotlin、Spring Boot以及专门的gRPC Spring Boot集成项目。项目结构包含两个子项目:一个用于模型定义,另一个用于代码实现。首先从模型项目开始。
I didn’t want something complicated. Reusing a simple example is enough: the request sends a string, and the response prefixes it with Hello
. We design this model in a dedicated Protobuf schema file:
syntax = "proto3"; //1
package ch.frankel.blog.grpc.model; //2
option java_multiple_files = true; //3
option java_package = "ch.frankel.blog.grpc.model"; //3
option java_outer_classname = "HelloProtos"; //3
service HelloService { //4
rpc SayHello (HelloRequest) returns (HelloResponse) {
}
}
message HelloRequest { //5
string name = 1; //6
}
message HelloResponse { //7
string message = 1; //6
}
-
– Protobuf定义版本
- – 包名
- – Java特有配置
- – 服务定义
- – 请求定义
- – 字段定义:首先是类型,其次是名称,最后是序号。
- – 响应定义
我们将使用Maven生成Java样板代码:
<project>
<dependencies>
<dependency>
<groupId>io.grpc</groupId> <!--1-->
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId> <!--1-->
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId> <!--1-->
<artifactId>jakarta.annotation-api</artifactId>
<version>1.3.5</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId> <!--2-->
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId> <!--3-->
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-plugin.version}</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
- – 编译时依赖
- – 嗅探操作系统信息;用于下一个插件
-
– 从
proto
文件生成Java代码。
编译后,结构应大致如下:
我们将类打包成JAR,并在Web应用程序项目中使用它。后者采用Kotlin编写,仅因它是我钟爱的JVM语言。
我们只需引入特定的Spring Boot启动依赖项,即可将gRPC端点与Spring Boot集成:
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
关键点在于:
@GrpcService //1
class HelloService : HelloServiceImplBase() { //2
override fun sayHello(
request: HelloRequest, //2
observer: StreamObserver<HelloResponse> //3
) {
with(observer) {
val reply = HelloResponse.newBuilder() //2
.setMessage("Hello ${request.name}") //4
.build()
onNext(reply) //5
onCompleted() //5
}
}
}
- 使用
grpc-server-spring-boot-starter
,它能识别注解并施展其魔法。 - 上述项目中生成的参考类
- 方法签名允许包含
StreamObserver
参数,该类源自grpc-stub.jar
。 - 获取请求,并将其前置以构建响应消息。
- 触发事件。
现在,我们可以通过运行./mvnw spring-boot:run
启动Web应用。
测试gRPC服务
本帖的核心观点是,使用常规工具无法访问gRPC服务。因此,测试时需要专用工具。我发现了grpcurl。让我们安装它,并用来列出可用的服务:
grpcurl --plaintext localhost:9090 list #1-2
- 列出所有可用的gRPC服务,无需TLS验证。
- 为避免gRPC与其他通道(如REST)冲突,Spring Boot使用了另一个端口。
ch.frankel.blog.grpc.model.HelloService #1
grpc.health.v1.Health #2
grpc.reflection.v1alpha.ServerReflection #2
- 我们定义的gRPC服务
- 自定义启动器提供的两个额外服务
我们还可以深入探究服务的结构。
grpcurl --plaintext localhost:9090 describe ch.frankel.blog.grpc.model.HelloService
service HelloService {
rpc SayHello ( .ch.frankel.blog.grpc.model.HelloRequest ) returns ( .ch.frankel.blog.grpc.model.HelloResponse );
}
最终,我们可以通过数据调用服务:
grpcurl --plaintext -d '{"name": "John"}' localhost:9090 ch.frankel.blog.grpc.model.HelloService/SayHello
{
"message": "Hello John"
}
使用常规工具访问gRPC服务
设想我们有一个常规的JavaScript客户端应用程序,需要访问gRPC服务。有哪些替代方案呢?
通用方法是借助grpc-web
:
A JavaScript implementation of gRPC for browser clients. For more information, including a quick start, see the gRPC-web documentation.
gRPC-web客户端通过一个特殊代理连接到gRPC服务;默认情况下,gRPC-web使用Envoy。
未来,我们预计gRPC-web将在特定语言的Web框架中得到支持,如Python、Java和Node。详细信息请参阅路线图。
– grpc-web
描述中提到一个限制:目前仅支持JavaScript。然而,还有另一个问题。它相当侵入性。你需要获取proto
文件,生成样板代码,并使你的代码调用它。你必须为每种客户端类型都这样做。更糟糕的是,如果proto文件发生变更,你需要在每个客户端中重新生成代码。
不过,如果你使用API Gateway,存在一种替代方案。我将介绍如何使用Apache APISIX实现,但其他网关或许也能做到。grpc-transcode是一个插件,允许将REST调用转码为gRPC并反向转码。
第一步是在Apache APISIX中注册proto文件:
curl http://localhost:9180/apisix/admin/protos/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d "{ \"content\": \"$(sed 's/"/\\"/g' ../model/src/main/proto/model.proto)\" }"
第二步是使用上述插件创建路由:
curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/helloservice/sayhello", #1
"plugins": {
"grpc-transcode": {
"proto_id": "1", #2
"service": "ch.frankel.blog.grpc.model.HelloService", #3
"method": "SayHello" #4
}
},
"upstream": {
"scheme": "grpc",
"nodes": {
"server:9090": 1
}
}
}'
- 定义一个粒度细化的路由。
- 引用先前命令中定义的proto文件。
- gRPC服务
- gRPC方法
至此,任何客户端都可以向定义的端点发起HTTP请求。Apache APISIX将把调用转码为gRPC,转发至定义的服务,获取响应,并再次转码。
curl localhost:9080/helloservice/sayhello?name=John
{"message":"Hello John"}
与grpc-web
相比,API网关方法允许将proto
文件与单一组件——网关本身共享。
转码的好处
此时,我们可以利用API网关的能力。设想如果未传递name
,我们希望有一个默认值,例如World
。开发者乐于在代码中设置它,但更改值需要完整的构建和部署。如果我们将默认值置于网关的路由处理链中,更改几乎可以即时生效。让我们相应地修改路由:
curl http://localhost:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/helloservice/sayhello",
"plugins": {
"grpc-transcode": {
...
},
"serverless-pre-function": { #1
"phase": "rewrite", #2
"functions" : [
"return function(conf, ctx) #3
local core = require(\"apisix.core\")
if not ngx.var.arg_name then
local uri_args = core.request.get_uri_args(ctx)
uri_args.name = \"World\"
ngx.req.set_uri_args(uri_args)
end
end"
]
}
},
"upstream": {
...
}
}'
- 当没有合适插件时的通用全能插件
- 重写请求。
- 实现功能的神奇Lua代码
现在,我们可以执行带有空参数的请求并获得预期结果:
curl localhost:9080/helloservice/sayhello?name
{"message":"Hello World"}
结论
本文简要介绍了gRPC及其在服务间通信中的优势,并使用Spring Boot和grpc-server-spring-boot-starter
开发了一个简单的gRPC服务。然而,这也带来了一个问题:常规客户端无法访问该服务,我们不得不借助grpcurl
进行测试。对于基于JavaScript的客户端或浏览器也是如此。
为了克服这一限制,我们可以利用API Gateway。我演示了如何配置Apache APISIX与grpc-transcode
插件来实现所需功能。
本文的完整源代码可在GitHub上找到。