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 场景中,为全双工通信维护着持久连接,但 WebSockets 可能在管理各种连接状态时引入复杂性。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 链中。此代码中未说明的是所需的数据验证,以确保您在有效负载中收到的数据与预期模式匹配,并且模式中收到的数据已正确设置为您所需的数据类型,以及处理错误数据的用户体验。
让我们用 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 定义和客户端存根。创建客户端实例,并指定服务 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 的可能性。
- 高效序列化: Protobuf 是 gRPC 使用的序列化格式,比 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 插件为您的服务生成客户端存根代码。(此处是 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 通过定义请求和响应的消息结构充当单一事实来源。这种严格的模式允许在多种语言中自动生成客户端和服务器代码。特别是对于 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 的初步内容,重点介绍了这样做的原因以及如何通过实际代码示例入门。还需要更多工作来充分整合错误处理,并展示性能基准测试,这些超出了本文档的范围。
gRPC 和 gRPC-Web 的许多技术方面,在使用 Envoy 的情况下,可以在现代 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 学院课程。
技术审阅者:Kevin Swiber (Postman),Eryu Xia (Google)