四时宝库

程序员的知识宝库

通过 JSON 转码以多种方式访问 .NET gRPC 端点

介绍

gRPC 是一种通信协议,使应用程序能够以非常有效的方式交换直接消息。 效率主要通过两个因素实现:

  • gRPC使用的序列化格式protobuf,旨在保证每条消息占用尽可能少的空间
  • gRPC 基于 HTTP/2,因此它使用其核心功能来提高性能,例如多路复用

遗憾的是,并非所有客户端类型都可以使用完整的 HTTP/2 功能集。 这甚至包括常用的客户端类型,例如 Web 浏览器。 尽管大多数现代浏览器都支持 HTTP/2,但它们仍然不支持 gRPC 所需的特定 HTTP/2 功能,例如多路复用。 因此,并非所有客户端都可以使用标准形式的 gRPC 框架。 有多种方法可以解决这个问题。 但如果我们的 gRPC 服务实现是用 .NET 编写的,那么目前最简单的解决方案是 JSON 转码。

但这并不是使用 JSON 转码的唯一好处。 尽管 gRPC 比 REST(网络上最流行的通信机制之一)具有性能优势,但由于其强类型模式以及对客户端和服务器上的其他库的依赖,gRPC 更难应用。 而这也是JSON转码解决的另一个问题。

JSON 转码是 .NET 7 中添加到 gRPC 的一项功能。它允许通过 REST API 访问 gRPC 端点。 这使得 gRPC 端点可以从任何 HTTP 客户端访问,包括浏览器。 无需将 gRPC 端点复制为 REST API 端点。 而且它比撰写本文时可用的任何替代技术(例如 gRPC-Gateway 和 gRPC-Web)都更容易设置。

gRPC-Gateway 在其理念和用例上类似于 JSON 转码。 这两种技术都将 gRPC 端点转变为 REST API 端点。 然而,虽然 JSON 转码只是通过向服务器应用程序添加一个相对轻量级的库来实现,但 gRPC-Gateway 是通过反向代理运行请求来实现的。 除了比 JSON 转码更难设置外,gRPC-Gateway 的效率也较低,因为每个请求都会添加一些额外的跃点。

gRPC-Web 应用了不同的理念; gRPC 在客户端中实现,因此客户端向服务器发出标准 gRPC 调用而不是 REST API 请求。 但它也有一些主要缺点。 首先,由于依赖于强类型消息模式,gRPC-Web 客户端的配置难度要大得多。 它还需要使用外部工具来生成合适的代码存根。 最后,就像 gRPC-Gateway 一样,它依赖于反向代理将请求从 HTTP/1.1 转换为 HTTP/2,反之亦然,这降低了性能。

使用 JSON 转码,我们不需要编写单独的 REST API 端点或在客户端执行一些特殊设置。 我们需要做的就是添加一些库,向我们的接口定义添加一些选项,并将几行代码应用于我们的请求处理中间件。 这真的很简单,本文将带您完成整个过程。

当然,使用 JSON 转码也有一些缺点。 例如,它降低了消息传递合同的保真度。 由于它不在客户端强制执行任何模式,因此它可能不适合某些在客户端首选强类型模式的用例,例如安全关键型应用程序。 但由于其使用方便,与任何其他在 HTTP/1.1 上启用 gRPC 的机制相比,它可能利多于弊。

现在我们将完成在 ASP.NET Core 应用程序中使用 JSON 转码的过程。 我们将使用一个 gRPC 服务项目,我们将修改该项目以在其中启用 JSON 转码。

先决条件

了解 ASP.NET Core 基础知识和对 HTTP 的基本了解对于理解本文中提供的信息是绝对必要。 本文还假定读者已经对 .NET 中的 gRPC 实现有所了解。 如果没有,Microsoft 的官方教程包含您需要的所有信息。

如果我们想在本地复制本文中的设置,我们需要在开发机器上安装 .NET SDK 7 或更新版本。 这可以从其官方页面下载。

最后,我们需要一个集成开发环境 (IDE) 或代码编辑器。 根据您的首选操作系统,合适的选项包括 Visual Studio、Visual Studio for Mac、Visual Studio Code 或 JetBrains Rider,它们可以从各自的网站下载。

初始设置

我们将从一个基于 gRPC 服务模板的 ASP.NET Core 项目开始。 我们的应用程序代表待办事项列表的处理程序。 我们可以查看列表、查看其中的每个项目、编辑每个项目、添加新项目以及从列表中删除条目。 我们的列表通过以下界面进行管理:

namespace JsonTranscodingExample;

public interface ITodosRepository
{
    IEnumerable<(int id, string description)> GetTodos();
    string GetTodo(int id);
    void InsertTodo(string description);
    void UpdateTodo(int id, string description);
    void DeleteTodo(int id);
}

在此接口中,我们为上述每个操作提供了一个方法。 该接口的实现如下所示。 我们将待办事项存储在字典中,其中整数用作唯一键,类似于标识列在数据库中的工作方式。


internal class TodosRepository : ITodosRepository
{
    private readonly Dictionary<int, string> todos = 
        new Dictionary<int, string>();
    private int currentId = 1;

    public IEnumerable<(int id, string description)> GetTodos()
    {
        var results = new List<(int id, string description)>();

        foreach (var item in todos)
        {
            results.Add((item.Key, item.Value));
        }

        return results;
    }

    public string GetTodo(int id)
    {
        return todos[id];
    }

    public void InsertTodo(string description)
    {
        todos[currentId] = description;
        currentId++;
    }

    public void UpdateTodo(int id, string description)
    {
        todos[id] = description;
    }

    public void DeleteTodo(int id)
    {
        todos.Remove(id);
    }
}

我们有一个 gRPC 端点,它允许我们访问上面概述的每个方法。 我们放在 todo.proto 文件中的 Protobuf 定义如下所示:

syntax = "proto3";

import "google/protobuf/empty.proto";

package todo;

service Todo {

  rpc GetAll (google.protobuf.Empty) returns (GetTodosReply);
  rpc Get (GetTodoRequest) returns (GetTodoReply);
  rpc Post (PostTodoRequest) returns (google.protobuf.Empty);
  rpc Put (PutTodoRequest) returns (google.protobuf.Empty);
  rpc Delete (DeleteTodoRequest) returns (google.protobuf.Empty);
}

message GetTodoRequest {
  int32 id = 1;
}

message GetTodosReply {
  repeated GetTodoReply todos = 1;
}

message GetTodoReply {
  int32 id = 1;
  string description = 2;
}

message PostTodoRequest {
  string description = 1;
}

message PutTodoRequest {
  int32 id = 1;
  string description = 2;
}

message DeleteTodoRequest {
  int32 id = 1;
}

这个 Protobuf 定义是由 TodoService 类实现的,它有以下内容:

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Todo;

namespace JsonTranscodingExample.Services;

public class TodoService : Todo.Todo.TodoBase
{
    private readonly ITodosRepository repository;

    public TodoService(ITodosRepository repository)
    {
        this.repository = repository;
    }

    public override Task<GetTodosReply> GetAll(
        Empty request,
        ServerCallContext context)
    {
        var result = new GetTodosReply();

        result.Todos.AddRange(repository.GetTodos()
            .Select(i => new GetTodoReply
            {
                Id = i.id,
                Description = i.description
            }));

        return Task.FromResult(result);
    }

    public override Task<GetTodoReply> Get(
        GetTodoRequest request,
        ServerCallContext context)
    {
        var todoDescription = repository.GetTodo(request.Id);

        return Task.FromResult(new GetTodoReply
        {
            Id = request.Id,
            Description = todoDescription
        });
    }

    public override Task<Empty> Post(
        PostTodoRequest request,
        ServerCallContext context)
    {
        repository.InsertTodo(request.Description);

        return Task.FromResult(new Empty());
    }

    public override Task<Empty> Put(
        PutTodoRequest request,
        ServerCallContext context)
    {
        repository.UpdateTodo(request.Id, request.Description);

        return Task.FromResult(new Empty());
    }

    public override Task<Empty> Delete(
        DeleteTodoRequest request,
        ServerCallContext context)
    {
        repository.DeleteTodo(request.Id);

        return Task.FromResult(new Empty());
    }
}

在这个类中,我们正在注入 ITodosRepository 接口。 然后,每个 gRPC 端点方法调用接口上的适当方法并向调用者返回适当的响应。

我们的 Program.cs 文件的内容如下所示。 在这里,我们注册了所有适当的 gRPC 依赖项,将 TodosRepository 类映射为用于依赖项注入的 ITodosRepository 接口的实现,并注册了 TodoService 类的 gRPC 端点。

using JsonTranscodingExample;
using JsonTranscodingExample.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddSingleton<ITodosRepository, TodosRepository>();

var app = builder.Build();

app.MapGrpcService<TodoService>();

app.Run();

我们可以从任何可以完全使用 HTTP/2 的 gRPC 客户端调用 gRPC 端点。 但是,如果我们想从浏览器或只能使用 HTTP/1.1 的客户端调用它怎么办? 这就是 JSON 转码发挥作用的地方。 接下来我们将添加所有适当的依赖项。

添加 gRPC JSON 转码依赖

要在我们的应用程序中启用 gRPC JSON 转码,我们需要安装以下 NuGet 包:

Microsoft.AspNetCore.Grpc.JsonTranscoding
Microsoft.AspNetCore.Grpc.Swagger

第一个 NuGet 包添加了核心 JSON 转码功能。 第二个包添加了将 Swagger 与从 gRPC 端点创建的 REST API 端点一起使用的功能,从而自动执行创建 API 文档的过程。

一旦我们安装了这些 NuGet 包,我们将把 google 文件夹放在我们项目的根文件夹中。 然后我们将在其中创建 api 文件夹。 在撰写本文时,我们需要将一些原型文件放入此文件夹中。 但是,将来不再需要这样做,因为这些文件将存在于框架本身中。

首先,我们需要在新创建的 api 文件夹中创建 http.proto 文件。以下是已删除所有注释的完整内容:

syntax = "proto3";

package google.api;

option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";

message Http {
  repeated HttpRule rules = 1;
  bool fully_decode_reserved_expansion = 2;
}

message HttpRule {
  string selector = 1;
  oneof pattern {
    string get = 2;
    string put = 3;
    string post = 4;
    string delete = 5;
    string patch = 6;
    CustomHttpPattern custom = 8;
  }
  string body = 7;
  string response_body = 12;
  repeated HttpRule additional_bindings = 11;
}

message CustomHttpPattern {
  string kind = 1;
  string path = 2;
}

接下来,我们需要在同一文件夹中创建 annotations.proto 文件。 如下

syntax = "proto3";

package google.api;

import "google/api/http.proto";
import "google/protobuf/descriptor.proto";

option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";

extend google.protobuf.MethodOptions {
  HttpRule http = 72295728;

创建这两个文件后,我们可以打开原始的 todo.proto 文件并在 package 关键字上方添加以下条目:

import “google/api/annotations.proto”;

然后,要将 JSON 转码功能添加到我们所有的端点,我们需要修改每个 rpc 定义,如下所示:

rpc GetAll (google.protobuf.Empty) returns (GetTodosReply) {
  option (google.api.http) = {
    get: "/todos"
  };
}

rpc Get (GetTodoRequest) returns (GetTodoReply) {
  option (google.api.http) = {
    get: "/todos/{id}"
  };
}

rpc Post (PostTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    post: "/todos/{description}"
  };
}

rpc Put (PutTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    put: "/todos/{id}/{description}"
  };
}

rpc Delete (DeleteTodoRequest) returns (google.protobuf.Empty) {
  option (google.api.http) = {
    delete: "/todos/{id}"
  };
}

在这些 RPC 中的每一个中,我们都添加了一个从 google.api.http 包导出的选项,该包由我们之前创建的 http.proto 文件表示。 在此选项中,我们使用 HTTP 动词(get、post、delete 等)作为键。 然后我们有 URL 路径,允许我们通过普通 HTTP 请求访问端点。 如果我们需要在 gRPC 端点的请求消息中添加一个表示字段的值,我们将字段名称括在大括号中。 例如,可以通过 DELETE HTTP 请求访问以下 URL。

delete: "/todos/{id}"

此路径位于基本 URL 之后。 例如,如果应用程序的基本 URL 是 https://localhost,那么我们应该用来删除 ID 为 1 的项目的完整路径是 https://localhost/todos/1。 路径的 {id} 部分是请求消息类型中 id 字段值的占位符,如下所示:

message DeleteTodoRequest {
  int32 id = 1;
}

接下来,我们需要在请求处理中间件中添加适当的配置以启用 JSON 转码。 为此,我们将首先在 Program.cs 文件的开头添加以下语句:

using Microsoft.OpenApi.Models;

然后我们将找到调用 AddGrpc 方法的行并添加对 AddJsonTranscoding 方法的调用,因此它现在看起来如下:

builder.Services.AddGrpc().AddJsonTranscoding();

然后,在我们从 builder 变量构建 app 变量之前,我们添加以下行以导入所有 Swagger 依赖项:

builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1",
        new OpenApiInfo { Title = "TODO API", Version = "v1" });
});

最后,我们将在 app 变量初始化之后添加以下行,以将 Swagger 端点添加到中间件:

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});

这就是我们需要做的。 JSON 转码功能现已在我们的应用程序中完全启用。 我们现在可以开始使用它了。

通过 REST API 调用 gRPC 端点

为了测试我们的应用程序,我们可以启动它并将 HTTP 请求发送到我们在 todo.proto 文件中映射的任何路径。 我们可以通过多种方式启动它。 我们可以在项目文件夹中执行 dotnet run 命令。 我们可以在适当的服务器上发布应用程序。 或者我们可以从 IDE 启动它。

启动应用程序后,我们可以找到它的基本 URL 并将 HTTP 请求发送到映射路径。 如果我们在调试模式下运行我们的应用程序,则可以在项目属性文件夹中的 launchSettings.json 文件中找到基本 URL。 然后我们可以使用任何可用的工具向它发送请求,例如 Postman、Fiddler 或 curl。 但最简单的方法是在浏览器中启动其 Swagger 页面,其中每个 URL 都将显示在直观的用户界面中。

要启动 Swagger 页面,我们可以输入应用程序的基本 URL,然后输入 /swagger 路径。 我们应该看到一个如下所示的页面:


所有映射的 API 端点都将显示在其上。 要向其中任何一个发送请求,我们需要展开它并单击“try”按钮。 然后我们可以输入参数并单击“Execute”。 然后将向服务器发出具有适当动作的正确构造的 HTTP 请求。 我们将看到以 JSON 格式显示在页面上的响应。

gRPC JSON 转码的主要限制

尽管 JSON 转码非常容易应用,但它有一些重要的限制,开发人员必须意识到这一点。 第一个是请求是通过 HTTP/1.1 而不是 HTTP/2 发出的。 与标准 gRPC 请求相比,这将不可避免地导致性能下降。

JSON 转码的第二个主要限制是它不适用于客户端流。 因此,客户端流式传输和双向流式传输调用都无法使用它。 不过,支持服务器流。 使用时,客户端将收到一组 JSON 对象作为响应。

这两个限制都要求 JSON 转码只能在无法使用标准 gRPC 时用作后备。 但是,这些限制同样适用于 gRPC-Web 和 gRPC-Gateway。 因此JSON转码仍然是一项有用的技术。 而且因为它比任何替代方案都更容易设置,所以当需要从无法完全使用 HTTP/2 的客户端访问 gRPC 端点时,它可能是最佳选择。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接