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; }
片段 1: 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

Visual Studio 2022 中的“配置您的新项目”对话框。在此屏幕中,我们在“项目名称”字段中键入“InventoryAPI”,在“位置”字段中选择“C:\MyInventoryService”,并在“解决方案名称”字段中键入“Inventory” .我们未选中“将解决方案和项目放在同一目录中”。

现在,让我们选择 . NET 6.0(长期支持)选项用于我们的框架:

Visual Studio 2022 中的附加信息对话框。在此屏幕中,我们从框架下拉列表中选择了“.NET 6.0(长期支持)”。我们未选中“启用 Docker”。

定义我们的产品服务

现在我们已经创建了项目,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" } }); } } }
片段 2: ProductCatalogService

该服务现在返回一个硬编码的产品。 为了使服务工作,我们只需要更改Program.cs中的服务注册以引用新的服务名称。 在我们的例子中,我们将重命名app.MapGrpcService<GreeterService>();app.MapGrpcService<ProductCatalogService>(); 使我们的新 API 可运行。

公平警告:不是您的标准协议测试

虽然我们可能很想尝试它,但我们无法通过针对其端点的浏览器测试我们的 gRPC 服务。 如果我们尝试这样做,我们会收到一条错误消息,指示必须通过 gRPC 客户端进行与 gRPC 端点的通信。

创建客户端

为了测试我们的服务,让我们使用 VS 的基本 Console App 模板并创建一个 gRPC 客户端来调用 API。 我命名我的InventoryApp

为方便起见,让我们引用一个相对文件路径,我们将通过该路径共享我们的合同。 我们将手动添加引用到.csproj文件。 然后,我们将更新路径并设置Client模式。 注意:我建议您在使用相对引用之前熟悉并相信您的本地文件夹结构。

以下是.proto引用,它们同时出现在服务和客户端项目文件中:

服务项目文件
(复制到客户端项目文件的代码)
客户项目文件
(粘贴和编辑后)
 <ItemGroup> <Content Update="Protos\product.proto" GrpcServices="Server" /> </ItemGroup>
 <ItemGroup> <Protobuf Include="..\InventoryAPI\Protos\product.proto" GrpcServices="Client" /> </ItemGroup>

现在,为了调用我们的服务,我们将替换Program.cs的内容。 我们的代码将实现许多目标:

  1. 创建一个表示服务端点位置的通道(端口可能会有所不同,因此请查阅launchsettings.json文件以获取实际值)。
  2. 创建客户端对象。
  3. 构造一个简单的请求。
  4. 发送请求。
 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();
片段 3:新Program.cs

准备发射

要测试我们的代码,在 VS 中,我们将右键单击解决方案并选择Set Startup Projects 。 在解决方案属性页对话框中,我们将:

  • 选择Multiple startup projects旁边的单选按钮,然后在 Action 下拉菜单中,将两个项目InventoryAPIInventoryApp )设置为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>
片段 4:添加到InventoryAPI服务项目文件的代码

接下来,我们将代码插入到ProductCatalogService.cs文件顶部的代码片段 5 中,以设置一个端点以返回我们的.proto文件:

 using System.Net.Mime; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.FileProviders;
片段 5: 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();
片段 6:使.proto文件可通过 API 访问的代码

添加 Snippets 4-6 后, .proto文件的内容应该在浏览器中可见。

一个新的测试客户端

现在我们要创建一个新的控制台客户端,我们将使用 VS 的依赖向导连接到我们现有的服务器。 问题是该向导不使用 HTTP/2。 因此,我们需要调整我们的服务器以通过 HTTP/1 进行通信并启动服务器。 随着我们的服务器现在可以使用它的.proto文件,我们可以构建一个新的测试客户端,通过 gRPC 向导连接到我们的服务器。

  1. 要更改我们的服务器以通过 HTTP/1 进行通信,我们将编辑我们的appsettings.json JSON 文件:
    1. 调整Protocol字段(位于路径Kestrel.EndpointDefaults.Protocols以读取Https
    2. 保存文件。
  2. 为了让我们的新客户端读取此proto信息,服务器必须正在运行。 最初,我们从 VS 的 Set Startup Projects 对话框启动了之前的客户端和服务器。 调整服务器解决方案以仅启动服务器项目,然后启动解决方案。 (现在我们已经修改了 HTTP 版本,我们的旧客户端无法再与服务器通信。)
  3. 接下来,创建新的测试客户端。 启动另一个 VS 实例。 我们将重复创建 API 项目部分中详述的步骤,但这一次,我们将选择控制台应用程序模板。 我们将把我们的项目和解决方案命名为InventoryAppConnected
  4. 创建客户端机箱后,我们将连接到我们的 gRPC 服务器。 在 VS 解决方案资源管理器中展开新项目。
    1. 右键单击Dependencies ,然后在上下文菜单中选择Manage Connected Services
    2. 在 Connected Services 选项卡上,单击Add a service reference并选择gRPC
    3. 在 Add Service Reference 对话框中,选择URL选项并输入服务地址的http版本(记得从launchsettings.json中获取随机生成的端口号)
    4. 单击完成以添加可以轻松维护的服务引用。

随意对照此示例的示例代码检查您的工作。 因为,在后台,VS 生成了我们在第一轮测试中使用的相同客户端,所以我们可以重用之前服务中的Program.cs文件的内容。

当我们更改合约时,我们需要修改我们的客户端 gRPC 定义以匹配更新的.proto定义。 为此,我们只需要访问 VS 的 Connected Services 并刷新相关的服务条目。 现在,我们的 gRPC 项目已经完成,很容易让我们的服务和客户端保持同步。

您的下一个候选项目:gRPC

我们的 gRPC 实现提供了使用 gRPC 的好处的第一手资料。 REST 和 gRPC 各有自己的理想用例,具体取决于合约类型。 但是,当这两个选项都适合时,我鼓励您尝试 gRPC — 它会让您在 API 的未来处于领先地位。