gRPC 与 REST:最佳 API 协议入门
已发表: 2022-07-22在当今的技术环境中,大多数项目都需要使用 API。 API 在服务之间架起沟通的桥梁,这些服务可能代表一个单一的复杂系统,但也可能驻留在不同的机器上或使用多种不兼容的网络或语言。
许多标准技术解决了分布式系统的服务间通信需求,例如 REST、SOAP、GraphQL 或 gRPC。 虽然 REST 是一种受欢迎的方法,但 gRPC 是一个有价值的竞争者,它提供了高性能、类型化的合约和出色的工具。
REST 概述
具象状态转移 (REST) 是一种检索或操作服务数据的方法。 REST API 通常建立在 HTTP 协议之上,使用 URI 来选择资源并使用 HTTP 动词(例如,GET、PUT、POST)来选择所需的操作。 请求和响应正文包含特定于操作的数据,而它们的标头提供元数据。 为了说明,让我们看一个通过 REST API 检索产品的简化示例。
在这里,我们请求一个 ID 为11
的产品资源,并指示 API 以 JSON 格式响应:
GET /products/11 HTTP/1.1 Accept: application/json
鉴于此请求,我们的响应(省略不相关的标头)可能如下所示:
HTTP/1.1 200 OK Content-Type: application/json { id: 11, name: "Purple Bowtie", sku: "purbow", price: { amount: 100, currencyCode: "USD" } }
虽然 JSON 可能是人类可读的,但在服务之间使用时并不是最佳的。 引用属性名称的重复性(即使是压缩的)也会导致消息臃肿。 让我们看看解决这个问题的替代方案。
gRPC 概述
gRPC 远程过程调用 (gRPC) 是一种开源、基于合约的跨平台通信协议,它通过向外部客户端公开一组功能来简化和管理服务间通信。
gRPC 建立在 HTTP/2 之上,利用双向流和内置传输层安全性 (TLS) 等功能。 gRPC 通过序列化的二进制有效负载实现更高效的通信。 它默认使用协议缓冲区作为结构化数据序列化的机制,类似于 REST 对 JSON 的使用。
然而,与 JSON 不同的是,协议缓冲区不仅仅是一种序列化格式。 它们包括其他三个主要部分:
- 在
.proto
文件中找到的合约定义语言(我们将遵循最新的协议缓冲区语言规范 proto3。) - 生成的访问函数代码
- 特定于语言的运行时库
服务上可用的远程功能(在.proto
文件中定义)列在协议缓冲区文件的服务节点内。 作为开发人员,我们可以使用协议缓冲区的丰富类型系统来定义这些函数及其参数。 该系统支持各种数字和日期类型、列表、字典和可空值来定义我们的输入和输出消息。
这些服务定义需要对服务器和客户端都可用。 不幸的是,除了提供对.proto
文件本身的直接访问之外,没有共享这些定义的默认机制。
这个示例.proto
文件定义了一个函数来返回一个产品条目,给定一个 ID:
syntax = "proto3"; package product; service ProductCatalog { rpc GetProductDetails (ProductDetailsRequest) returns (ProductDetailsReply); } message ProductDetailsRequest { int32 id = 1; } message ProductDetailsReply { int32 id = 1; string name = 2; string sku = 3; Price price = 4; } message Price { float amount = 1; string currencyCode = 2; }
ProductCatalog
服务定义proto3 的严格类型和字段顺序使得消息反序列化比解析 JSON 的负担要小得多。
比较 REST 与 gRPC
回顾一下,比较 REST 与 gRPC 时最重要的一点是:
休息 | gRPC | |
---|---|---|
跨平台 | 是的 | 是的 |
消息格式 | 自定义但通常是 JSON 或 XML | 协议缓冲区 |
消息负载大小 | 中/大 | 小的 |
处理复杂性 | 更高(文本解析) | 较低(定义明确的二进制结构) |
浏览器支持 | 是(本机) | 是(通过 gRPC-Web) |
在预期不太严格的合同和频繁添加有效负载的情况下,JSON 和 REST 非常适合。 当合约倾向于保持静态且速度至关重要时,gRPC 通常会胜出。 在我从事的大多数项目中,gRPC 已被证明比 REST 更轻量级和更高性能。
gRPC 服务实现
让我们构建一个精简的项目来探索采用 gRPC 是多么简单。
创建 API 项目
首先,我们将在 Visual Studio 2022 社区版 (VS) 中创建一个 .NET 6 项目。 我们将选择ASP.NET Core gRPC 服务模板并命名项目(我们将使用InventoryAPI
)和其中的第一个解决方案( Inventory
) 。
现在,让我们选择 . NET 6.0(长期支持)选项用于我们的框架:
定义我们的产品服务
现在我们已经创建了项目,VS 显示了一个名为Greeter
的示例 gRPC 原型定义服务。 我们将重新调整Greeter
的核心文件以满足我们的需求。
- 为了创建我们的合约,我们将用 Snippet 1 替换
greet.proto
的内容,重命名文件product.proto
。 - 为了创建我们的服务,我们将用 Snippet 2 替换
GreeterService.cs
文件的内容,将文件重命名为ProductCatalogService.cs
。
using Grpc.Core; using Product; namespace InventoryAPI.Services { public class ProductCatalogService : ProductCatalog.ProductCatalogBase { public override Task<ProductDetailsReply> GetProductDetails( ProductDetailsRequest request, ServerCallContext context) { return Task.FromResult(new ProductDetailsReply { Id = request.Id, Name = "Purple Bowtie", Sku = "purbow", Price = new Price { Amount = 100, CurrencyCode = "USD" } }); } } }
ProductCatalogService
该服务现在返回一个硬编码的产品。 为了使服务工作,我们只需要更改Program.cs
中的服务注册以引用新的服务名称。 在我们的例子中,我们将重命名app.MapGrpcService<GreeterService>();
到app.MapGrpcService<ProductCatalogService>();
使我们的新 API 可运行。
公平警告:不是您的标准协议测试
虽然我们可能很想尝试它,但我们无法通过针对其端点的浏览器测试我们的 gRPC 服务。 如果我们尝试这样做,我们会收到一条错误消息,指示必须通过 gRPC 客户端进行与 gRPC 端点的通信。
创建客户端
为了测试我们的服务,让我们使用 VS 的基本 Console App 模板并创建一个 gRPC 客户端来调用 API。 我命名我的InventoryApp
。
为方便起见,让我们引用一个相对文件路径,我们将通过该路径共享我们的合同。 我们将手动添加引用到.csproj
文件。 然后,我们将更新路径并设置Client
模式。 注意:我建议您在使用相对引用之前熟悉并相信您的本地文件夹结构。
以下是.proto
引用,它们同时出现在服务和客户端项目文件中:
服务项目文件 (复制到客户端项目文件的代码) | 客户项目文件 (粘贴和编辑后) |
---|---|
|
|
现在,为了调用我们的服务,我们将替换Program.cs
的内容。 我们的代码将实现许多目标:

- 创建一个表示服务端点位置的通道(端口可能会有所不同,因此请查阅
launchsettings.json
文件以获取实际值)。 - 创建客户端对象。
- 构造一个简单的请求。
- 发送请求。
using System.Text.Json; using Grpc.Net.Client; using Product; var channel = GrpcChannel.ForAddress("https://localhost:7200"); var client = new ProductCatalog.ProductCatalogClient(channel); var request = new ProductDetailsRequest { Id = 1 }; var response = await client.GetProductDetailsAsync(request); Console.WriteLine(JsonSerializer.Serialize(response, new JsonSerializerOptions { WriteIndented = true })); Console.ReadKey();
Program.cs
准备发射
要测试我们的代码,在 VS 中,我们将右键单击解决方案并选择Set Startup Projects 。 在解决方案属性页对话框中,我们将:
- 选择Multiple startup projects旁边的单选按钮,然后在 Action 下拉菜单中,将两个项目(
InventoryAPI
和InventoryApp
)设置为Start 。 - 单击确定。
现在我们可以通过单击 VS 工具栏中的开始(或按F5键)来启动解决方案。 将显示两个新的控制台窗口:一个告诉我们服务正在侦听,另一个向我们显示检索到的产品的详细信息。
gRPC 合约共享
现在让我们使用另一种方法将 gRPC 客户端连接到我们的服务定义。 客户最容易访问的合同共享解决方案是通过 URL 提供我们的定义。 其他选项要么非常脆弱(通过路径共享文件),要么需要更多努力(通过本机包共享合同)。 通过 URL 共享(就像 SOAP 和 Swagger/OpenAPI 所做的那样)是灵活的并且需要更少的代码。
首先,将.proto
文件作为静态内容提供。 我们将手动更新我们的代码,因为构建操作的 UI 设置为“Protobuf Compiler”。 此更改指示编译器复制.proto
文件,以便可以从 Web 地址提供它。 如果通过 VS UI 更改此设置,则构建将中断。 那么,我们的第一步是将 Snippet 4 添加到InventoryAPI.csproj
文件中:
<ItemGroup> <Content Update="Protos\product.proto"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup> <Content Include="Protos\product.proto" CopyToPublishDirectory="PreserveNewest" /> </ItemGroup>
InventoryAPI
服务项目文件的代码接下来,我们将代码插入到ProductCatalogService.cs
文件顶部的代码片段 5 中,以设置一个端点以返回我们的.proto
文件:
using System.Net.Mime; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders;
Namespace
导入现在,我们在app.Run()
之前添加 Snippet 6,也在ProductCatalogService.cs
文件中:
var provider = new FileExtensionContentTypeProvider(); provider.Mappings.Clear(); provider.Mappings[".proto"] = MediaTypeNames.Text.Plain; app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Protos")), RequestPath = "/proto", ContentTypeProvider = provider }); app.UseRouting();
.proto
文件可通过 API 访问的代码添加 Snippets 4-6 后, .proto
文件的内容应该在浏览器中可见。
一个新的测试客户端
现在我们要创建一个新的控制台客户端,我们将使用 VS 的依赖向导连接到我们现有的服务器。 问题是该向导不使用 HTTP/2。 因此,我们需要调整我们的服务器以通过 HTTP/1 进行通信并启动服务器。 随着我们的服务器现在可以使用它的.proto
文件,我们可以构建一个新的测试客户端,通过 gRPC 向导连接到我们的服务器。
- 要更改我们的服务器以通过 HTTP/1 进行通信,我们将编辑我们的
appsettings.json
JSON 文件:- 调整
Protocol
字段(位于路径Kestrel.EndpointDefaults.Protocols
)以读取Https
。 - 保存文件。
- 调整
- 为了让我们的新客户端读取此
proto
信息,服务器必须正在运行。 最初,我们从 VS 的 Set Startup Projects 对话框启动了之前的客户端和服务器。 调整服务器解决方案以仅启动服务器项目,然后启动解决方案。 (现在我们已经修改了 HTTP 版本,我们的旧客户端无法再与服务器通信。) - 接下来,创建新的测试客户端。 启动另一个 VS 实例。 我们将重复创建 API 项目部分中详述的步骤,但这一次,我们将选择控制台应用程序模板。 我们将把我们的项目和解决方案命名为
InventoryAppConnected
。 - 创建客户端机箱后,我们将连接到我们的 gRPC 服务器。 在 VS 解决方案资源管理器中展开新项目。
- 右键单击Dependencies ,然后在上下文菜单中选择Manage Connected Services 。
- 在 Connected Services 选项卡上,单击Add a service reference并选择gRPC 。
- 在 Add Service Reference 对话框中,选择URL选项并输入服务地址的
http
版本(记得从launchsettings.json
中获取随机生成的端口号) 。 - 单击完成以添加可以轻松维护的服务引用。
随意对照此示例的示例代码检查您的工作。 因为,在后台,VS 生成了我们在第一轮测试中使用的相同客户端,所以我们可以重用之前服务中的Program.cs
文件的内容。
当我们更改合约时,我们需要修改我们的客户端 gRPC 定义以匹配更新的.proto
定义。 为此,我们只需要访问 VS 的 Connected Services 并刷新相关的服务条目。 现在,我们的 gRPC 项目已经完成,很容易让我们的服务和客户端保持同步。
您的下一个候选项目:gRPC
我们的 gRPC 实现提供了使用 gRPC 的好处的第一手资料。 REST 和 gRPC 各有自己的理想用例,具体取决于合约类型。 但是,当这两个选项都适合时,我鼓励您尝试 gRPC — 它会让您在 API 的未来处于领先地位。