gRPC 能否取代 REST 和 WebSockets 用于 Web 应用通信?
在快速发展的 Web 开发领域,效率和性能常常是新技术被采用的关键因素。正在开发的 gRPC-Web 库标志着一个重要的转变,它使开发者能够将 gRPC 的速度和强大能力应用于 Web 应用的客户端-服务器通信,取代 REST 和 WebSockets 的某些方面。让我们来看看与传统 RESTful 调用和 WebSocket 连接的比较分析,并提供 gRPC-Web 的实际代码示例,以便对各种方法进行对比。
理解 gRPC-Web 及其在现代 Web 开发中的位置
gRPC-Web 的起源在于寻求构建响应更迅速、延迟更低的 Web 应用。它将 gRPC(一个高性能、开源的通用 RPC 框架)的能力扩展到浏览器,使得浏览器能够与通常用于服务器间通信的 gRPC 服务直接通信。gRPC 围绕一种称为 Protocol Buffers (Protobuf) 的序列化格式构建,这种格式有助于减小有效载荷大小和提供明确的接口描述,从而简化开发流程。
在深入探讨技术细节之前,让我们审视一下 gRPC-Web 所代表的潜在转变。与需要 HTTP/2 的原生 gRPC 协议不同,gRPC-Web 放宽了这一要求,使其能够支持浏览器环境中可用的任何 HTTP/* 协议。在 WebSocket 场景中,维护一个持久连接以实现全双工通信,但这可能会引入管理各种连接状态的复杂性。gRPC-Web 凭借其服务器流式传输能力提供了一个引人注目的替代方案,从而实现更高效的实时数据流。目前,由于浏览器限制,gRPC-Web 不支持客户端流式传输。
gRPC-Web 的工作原理
要将 gRPC-Web 集成到 Web 应用中,必须采用特定的架构。该架构的核心是 Envoy 代理,它充当 Web 应用和 gRPC 服务器之间的桥梁(这是为了抽象网络协议所必需的)。Envoy 将 gRPC-Web 调用转换为 gRPC 调用,处理 HTTP/1.1 到 HTTP/2 的转换,从而使浏览器能够享受 gRPC 的优势。
让我们分解一下这种通信模型涉及的步骤:
- 浏览器发起 gRPC-Web 客户端调用。
- Envoy 代理接收调用,其中包含 Protobuf 定义的请求。
- 然后 Envoy 将其转换为 HTTP/2 gRPC 调用并转发给 gRPC 服务器。
- gRPC 服务器处理请求并将响应返回给 Envoy。
- Envoy 将 gRPC 响应转换回 gRPC-Web 格式并发送给客户端。
通过这种代理系统,gRPC-Web 促成了一种健壮的客户端-服务器交互,它具有高性能,并且由于 Protobuf 的强数据类型特性,提供了数据的清晰度和精确性。
从 REST 过渡到 gRPC-Web 的理论
对于习惯使用 REST 的开发者来说,转向 gRPC-Web 可能看起来具有挑战性。但是,通过对相关组件的适当理解和循序渐进的方法,过渡过程可以很顺利。
考虑一个典型的 RESTful fetch 调用:
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Accept': 'application/json',
},
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
上述代码从 RESTful API 服务中检索 JSON 数据。注意使用了 fetch API、HTTP 方法和内容类型头部。响应被处理为 JSON 对象,并且错误处理内置于 promise 链中。这段代码中未说明的是所需的数据验证,以确保你在有效载荷中接收到的数据与预期的 schema 匹配,并且 schema 中接收到的数据被正确地设置为你所需的数据类型,以及处理错误数据的用户体验。
让我们用 gRPC-Web 重新构想一下:
const { ExampleRequest, ExampleResponse } = require('./generated/example_pb.js');
const { ExampleServiceClient } = require('./generated/example_grpc_web_pb.js');
const client = new ExampleServiceClient('https://api.example.com');
const request = new ExampleRequest();
client.getExampleData(request, {}, (err, response) => {
if (err) {
console.error('Error:', err);
} else {
console.log(response.toObject());
}
});
在这个 gRPC-Web 示例中,我们首先导入所需的 Protobuf 定义和客户端 stub。创建一个客户端实例,指定服务 URL。我们构建一个请求对象,然后在客户端上调用 getExampleData 方法,传入请求以及一个用于处理响应或错误的回调函数。
请注意方法上的显著差异:gRPC-Web 调用是强类型的,并且序列化/反序列化由库处理,而不是由开发者手动完成。这种类型安全和自动化可以极大地减少人为错误的潜力,并简化开发流程。如果你接收到一个对象,它已经经过了充分验证。
gRPC-Web 相较于 REST 的优势
虽然 REST 多年来一直是 Web API 的基石,但在处理复杂的 Web 应用时,它的简单性有时会成为限制。虽然 gRPC-Web 可以与浏览器中支持的任何 HTTP/* 协议配合使用,但 gRPC-Web 利用了许多 HTTP/2 特性,带来了诸多改进。以下是 HTTP/2 和 gRPC-Web 的一些优势:
- 兼容现有服务:除了 Envoy 代理之外,无需新建任何内容,因此实现 gRPC-Web 将使你能够访问任何现有的 gRPC 服务。这对于使用 JavaScript 库的应用(包括移动应用)来说是一项优势。
- 类型安全:使用 gRPC-Web,请求和响应都是基于 Protobuf 定义的强类型。客户端和服务器之间的契约是明确的,从而减少了通信错误和 bug 的可能性。
- 高效序列化:gRPC 使用的序列化格式 Protobuf 比 JSON 或 XML 更高效,能够更快地序列化并减小消息大小。这对于性能尤其有利,并能节省带宽成本。HTTP/1.1 允许以文本模式或二进制模式发送数据,但不能同时使用两者。HTTP/2 仅支持二进制,将二进制编码/解码为文本比将二进制文件编码/解码为文本并在 REST 上发送混合有效载荷更不容易出错。
- 清晰的 API 契约:使用 Protobuf 定义服务可以创建一个清晰、与语言无关的 API 契约。这可以用于生成多种语言的客户端和服务器代码,为开发者提供流畅的体验。
设置 gRPC-Web 环境
开始使用 gRPC-Web 需要使用 Protobuf 定义服务和消息有效载荷,设置 gRPC 后端服务(或者暂时使用模拟服务器),并配置 Envoy 代理以在 gRPC-Web 和 gRPC 之间进行转换。
首先,在 .proto 文件中定义服务:
syntax = "proto3";
package example;
service ExampleService {
rpc GetExampleData(ExampleRequest) returns (ExampleResponse);
}
message ExampleRequest {
string query = 1;
}
message ExampleResponse {
repeated string data = 1;
}
这个 .proto
文件定义了一个简单的服务,包含一个 RPC 方法 GetExampleData
,以及请求和响应消息格式。由于该操作在请求中发送一个 ExampleRequest
消息,并在响应中期望接收一个 ExampleResponse
消息,这种一元 RPC 调用模仿了 RESTful 请求。
接下来,使用 protoc 命令行工具和适当的 gRPC-Web 插件为服务生成客户端 stub 代码。(这里有一个示例来自 gRPC-Web 快速入门文档)。这个过程将创建进行 gRPC-Web 调用所需的 JavaScript 客户端文件。
在用你选择的语言实现 gRPC 服务器后,你需要配置一个 Envoy 代理。这里还有另一个示例来自 gRPC-Web 快速入门文档。
以下是 Envoy 配置的一些 YAML 语法,它作为上面链接的更大配置的一部分启用了 gRPC-Web。
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.router
部署好这些元素后,你就可以开始从你的 Web 应用发起 gRPC-Web 调用了。
使用 Protobuf 定义服务方法
在定义服务方法时,Protobuf 通过定义请求和响应的消息结构充当单一事实来源。这种严格的 schema 允许自动生成多种语言的客户端和服务器代码。特别是对于 JavaScript,这种代码生成简化了浏览器客户端的调用过程。
使用上面示例的 .proto 文件,生成的 JavaScript 客户端代码将使用这些定义来确保只发送和接收正确的数据类型。这个过程处理了许多在使用 RESTful 服务时容易出错的手动数据验证和解析工作。
使用 gRPC-Web 替代典型的 WebSocket 连接
WebSockets 通过一个持久连接提供全双工通信通道。在 gRPC-Web 由于缺乏客户端流式传输能力而无法完全替代 WebSockets 的场景中,它仍然可以用于高效的服务器到客户端流式传输。
以下是一个典型的 WebSocket 实现示例:
const socket = new WebSocket('ws://example.com/data');
socket.onmessage = function(event) {
const receivedData = JSON.parse(event.data);
console.log(receivedData);
};
socket.onerror = function(error) {
console.error('WebSocket Error:', error);
};
WebSocket API 很直观,但管理连接的状态和生命周期可能会变得复杂。
现在,让我们探讨一下使用 gRPC-Web 的服务器端流式传输是什么样的:
const { Empty } = require('./generated/common_pb.js');
const { DataServiceClient } = require('./generated/data_grpc_web_pb.js');
const client = new DataServiceClient('https://api.example.com');
const request = new Empty();
const stream = client.dataStream(request, {});
stream.on('data', (response) => {
console.log(response.toObject());
});
stream.on('error', (err) => {
console.error('Stream Error:', err);
});
stream.on('end', () => {
console.log('Stream ended.');
});
虽然用 gRPC-Web 替代 WebSockets 需要涉及更多代码,但你可以设置一个服务器端流式传输调用,让服务器可以持续向客户端发送消息。客户端使用事件监听器来处理接收到的消息、错误和流的结束。这与 WebSockets 是不同的范式,但在支持的用例中,它可以更高效且更容易管理。
许多基于 WebSockets 的聊天应用利用客户端单次发送和服务器流式传输事件,这可以用 gRPC-Web 替代。即使在开发多人游戏的场景中,“MoveCharacters” 的 RPC 调用可以接收浏览器发送的关于你角色移动的单个消息,并流式传输回其他玩家或电脑控制角色的所有移动信息。
是时候替代 REST 和 WebSockets 了吗?
本文初步探讨了使用 gRPC-Web 替代 REST 和 WebSockets 的可能性,重点介绍了这样做的原因以及如何通过实际代码示例入门。要完全整合错误处理并展示性能基准测试,还需要更多工作,这些超出了本文档的范围。
使用 Envoy 的 gRPC 和 gRPC-Web 在许多技术方面可以替代现代 Web 应用开发中的 REST 和 WebSockets。虽然目前公开的 gRPC API 并不多,但随着时间推移,我们必将看到更多公司采用基于 HTTP/2 和 HTTP/3 的高性能 API,并考虑用于 Web 应用的其他新兴技术。
Postman 对 gRPC 的支持
如果您使用 API,您可能正在使用 Postman。您知道 Postman 支持 gRPC 吗?在您构建应用程序时,我们的 VS Code 扩展也支持 gRPC 请求。今年我们很高兴参加了 gRPC Conf 并与社区成员交流。请关注我们的博客,了解即将发布的 gRPC 相关更新和文章。如果您想了解 gRPC、Protobuf 以及它们如何在 Postman 中使用的历史,您还可以查看我们的 Postman Academy 课程。
技术评审:Kevin Swiber (Postman),Eryu Xia (Google)