2019 年 9 月

第 34 卷,第 9 期

[领先技术]

ASP.NET Core gRPC 服务中的流式处理方法

作者 Dino Esposito

Dino Esposito上一期的《领先技术》中,我介绍了构建一种基于 gRPC 框架的新型服务,该服务在 ASP.NET Core 3.0 中作为由 Kestrel 直接托管的本机服务首次亮相(尽管其已向 C# 开发人员提供了一段时间)。gRPC 框架适用于已连接的终结点(主要是但不一定是微服务)之间的对等二进制通信。它还支持最新的技术解决方案,例如用于序列化内容的 Google Protobuf 以及用于运输的 HTTP/2。

Visual Studio 2019 附带一个 ASP.NET Core 3.0 项目模板,通过该模板,只需单击几下即可创建 gRPC 服务的框架。有关 gRPC 和由 Visual Studio 生成的初学者工具包的入门读本,可在 msdn.com/magazine/mt833481 上查看我的 7 月专栏。这个月我将进一步探索 gRPC。首先,我将更为详细地对基础工具进行介绍。事实上,需要一些工具将 .proto 文件的内容分析为 C# 类,以将其用作客户端和服务实现的基础。此外,我还将介绍流式处理方法和复杂的消息类。最后,我将重点介绍如何在 Web 客户端应用程序的 UI 中集成流式处理的 gRPC 方法。

构建 gRPC 服务

内置的 Visual Studio 项目模板将服务接口定义文件(.proto 文件)放置在位于同一服务项目的名为 protos 的子文件夹中。但在本文中,我将采用不同的方法,首先向最初为空的解决方案中添加一个全新的 .NET Standard 2.0 类库。

proto 类库不显式包含任何 C# 类。而只包含一个或多个 .proto 文件。你可在闲暇时整理文件夹和子文件夹中的 .proto 文件。在示例应用程序中,我有一个 .proto 文件,其适用于位于 protos 项目文件夹下的示例服务。以下是示例 .proto 文件服务块的摘要:

service H2H {
  rpc Details (H2HRequest) returns (H2HReply) {}
}

H2H 示例服务预计将从某个远程位置检索一些与运动相关的信息。Details 方法传递面对面请求,并接收指定玩家或团队之间过去比赛的分数。以下是 H2HRequest 和 H2HReply 消息可能呈现的形式:

message H2HRequest {
  string Team1 = 1;
  string Team2 = 2;
}
message H2HReply {
  uint32 Won1 = 1;
  uint32 Won2 = 2;
  bool Success = 3;
}

第一种消息类型传递有关要处理的团队的信息,第二种消息类型接收过去比赛的历史记录以及表示操作成功或失败的布尔标志。至此不会有什么问题。消息中的所有内容都定义为我们在上一篇文章中介绍的内容。Details 方法使用 gRPC 专业术语,是一种一元方法,这意味着每个请求都会收到一个(且只有一个)响应。但是,这是编码 gRPC 服务的最常用方式。让我们添加流式处理功能,如下所示:

rpc MultiDetails (H2HMultiRequest) returns (stream H2HMultiReply) {}

新的 MultiDetails 方法是一种服务器端流式处理方法,这意味着对于从某个 gRPC 客户端获取的每个请求,它可能都会返回多个响应。在此示例中,客户端可能会发送一系列面对面请求,并在服务端对响应进行详细阐述时以异步方式接收单个面对面响应。要做到这点,必须在返回部分中使用流关键字标记 gRPC 服务方法。流式处理方法可能也需要临时消息类型,如下所示:

message H2HMultiRequest {
  string Team = 1;
  repeated string OpponentTeam = 2;
}

如前所述,客户端可能会要求建立特定团队与一组其他团队之间的面对面记录。消息类型中的重复关键字仅表示 OpponentTeam 成员可能多次出现。在纯 C# 术语中,H2HMultiRequest 消息类型在概念上等效于以下伪代码:

class H2HMultiRequest
{  string Team {get; set;}  IEnumerable<string> OpponentTeam {get; set;}}

但请注意,由 gRPC 工具生成的代码略有不同,如下所示:

public RepeatedField<string> OpponentTeam {get; set;}

请注意,事实上,从 gRPC 消息类型 T 生成的任何类都会实现 Google.ProtoBuf.IMessage<T> 接口的成员。响应消息类型应设计为描述在流式处理阶段的每个步骤返回的实际数据。因此,每个回复必须引用主要团队和组中指定的其中一个对手团队之间的单个面对面响应,如下所示:

message H2HMultiReply {
  H2HItem Team1 = 1;
  H2HItem Team2 = 2;
}
message H2HItem {
  string Name = 1;
  uint32 Won = 2;
}

H2HItem 消息类型表示给定团队在与请求中指定的其他团队的比赛中赢得的次数。

在进一步研究流式处理方法的实现之前,让我们先看看嵌入 proto 定义的共享类库所需的依赖项。Visual Studio 项目必须引用图 1 中的 NuGet 包。

The NuGet Dependencies of the Proto Shared Class Library
图 1 Proto 共享类库的 NuGet 依赖项

包含源 .proto 文件的项目必须引用 Grpc.Tools 包以及任何 gRPC 项目(无论是客户端、服务还是库)所需的 Grpc.Net.Client(在 .NET Core 3.0 预览版 6 中添加)和 Google.protobuf 包。工具包最终负责分析 .proto 文件并在编译时生成任何必要的 C# 类。.csproj 文件中的项组块指示工具系统如何继续进行。代码如下:

<ItemGroup>
  <Protobuf Include="Protos\h2h.proto"
            GrpcServices="Server, Client"
            Generator="MSBuild:Compile" />
  <Content Include="@(Protobuf)" />
  <None Remove="@(Protobuf)" />
</ItemGroup>

项组块中最相关的部分是 Protobuf 节点,特别是 GrpcServices 属性。其已分配字符串值中的服务器令牌表示工具必须为原型接口生成服务类。客户端令牌表示它还需要创建基客户端类以调用服务。完成此操作后,生成的 DLL 包含适用于消息类型、基服务类和客户端类的 C# 类。服务项目和客户端项目(无论是控制台、Web 还是桌面)都只需引用原型 DLL 即可处理 gRPC 服务。

实现服务

gRPC 服务是 ASP.NET Core 项目,在启动类中进行了一些特殊配置。除 ASP.NET Core 服务器平台和原型程序集外,它还引用了 ASP.NET Core gRPC 框架和 Google.Protobuf 包。启动类在 Configure 方法中添加 gRPC 运行时服务,并在 ConfigureServices 方法中附加 gRPC 终结点,如图 2 所示。

图 2 配置 gRPC 服务

public void ConfigureServices(IServiceCollection services)
{
  services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{  // Some other code here   ...
  app.UseRouting();
  app.UseEndpoints(endpoints =>
  {
    endpoints.MapGrpcService<H2HService>();
  });
}

服务类从基服务类继承基于 .proto 文件内容创建的工具,如下所示:

public class H2HService : Sample.H2H.H2HBase
{
  // Unary method Details
  public override Task<H2HReply> Details(
              H2HRequest request, ServerCallContext context)
  {
    ...
  }
  ...
}

一元方法(如 Details 方法)具有比流式处理方法更简单的签名。它们返回 Task<TReply> 对象,并接受 TRequest 对象和 ServerCallContext 实例,以访问传入请求的细微细节。服务器端流式处理方法具有一个额外的响应流参数,实现代码使用该参数流式传输回数据包。图 3**** 展示了 MultiRequest 流式处理方法的实现。

图 3 服务器端流式处理 gRPC 服务方法

public override async Task MultiDetails(      H2HMultiRequest request,
      IServerStreamWriter<H2HMultiReply> responseStream,
      ServerCallContext context)
{  // Loops through the batch of operations embedded   // in the current request
  foreach (var opponent in request.OpponentTeam)
  {
    // Grab H2H data to return
    var h2h = GetHeadToHead_Internal(request.Team, opponent);
    // Copy raw data into an official reply structure    // Raw data is captured in some way: an external REST service
    // or some local/remote database    var item1 = new H2HItem {
     Name = h2h.Id1, Won = (uint) h2h.Record1};
    var item2 = new H2HItem {
     Name = h2h.Id2, Won = (uint) h2h.Record2};
    var reply = new H2HMultiReply { Team1 = item1, Team2 = item2 };
    // Write back via the output response stream
    await responseStream.WriteAsync(reply);
  }
  return;
}

如你所见,与传统的一元方法相比,流式处理方法采用了额外参数类型 IServerStreamWriter<TReply>。这是一个输出流,该方法将使用此输出流在结果准备就绪时将其流式传输回来。在图 3 的代码中,该方法在循环中输入请求的每个操作(在本示例中,是一组过去进行过比赛的团队)。然后,当针对本地/远程数据库或 Web 服务的查询返回时,它会流式传输回结果。完成后,该方法会返回,基础运行时环境会关闭该流。

为流式处理方法编写客户端

在示例代码中,客户端应用程序是一个纯 ASP.NET Core 3.0 应用程序。它包含对 Google.Protobuf 包和 Grpc.Net.Client 包的引用以及共享的原型库。UI 展示了一个附加了一些 JavaScript 的按钮,该按钮会发布到控制器方法。(请注意,你可以自由使用经典 HTML 表单,但使用 Ajax 进行发布可能会更容易地接收回复通知以及更顺畅地更新 UI。) 图 4**** 展示了该代码。

图 4 调用 gRPC 服务

[HttpPost]
public async Task<IActionResult> Multi()
{
  // Call the RPC service
  var serviceUrl = "http://localhost:50051";
    AppContext.SetSwitch(
                "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport",
                true);
    var httpClient = new HttpClient() {BaseAddress = new Uri(serviceUrl) };
  var client = GrpcClient.Create<H2H.H2HClient>(httpClient);
  var request = new H2HMultiRequest() { Team = "AF-324" };
  request.OpponentTeam.AddRange(new[] { "AW-367", "AD-683", "AF-510" });
  var model = new H2HMultiViewModel();
  using (var response = client.MultiDetails(request))
  {
    while (await response.ResponseStream.MoveNext())
    {
      var reply = response.ResponseStream.Current;      // Do something here ...
    }  }
  return View(model);
}

值得回顾的是,gRPC 服务的端口依赖于 Visual Studio 项目,而客户端调用方类则在原型库中进行定义。准备对服务器端流式处理方法的请求时,除填充输入消息类型之外,无需执行任何其他操作。如前所述,OpponentTeam 集合是一个可枚举的 .NET 类型,可使用 AddRange 进行填充或重复调用到 Add。虽然实际类型不属于 .NET Core 集合类型,但它仍然是一个集合类型,尽管其已经在 Google Protobuf 包中得到了实现。

当服务器端方法流式传输回数据包直到该流结束时,对该方法的实际调用将返回一个流对象。接下来,客户端代码会枚举等待响应结束的数据包。图 4 中 while 循环的每次迭代都会从 gRPC 服务捕获单个回复数据包。接下来的情况取决于客户端应用程序。总的来说,有三种不同的情况。

一种是客户端应用程序有其自己的 UI,但可以在向用户显示某些新鲜内容之前等待收集整个响应。在这种情况下,可将当前答复对象携带的数据加载到控制器方法返回的视图模型中。第二种场景是没有 UI(例如客户端为正在工作的微服务)。在这种情况下,接收的数据在其可用后会立即得到处理。最后,在第三种场景中,客户端应用程序具有其自己的响应式 UI,且能够在数据来自服务器时向用户展示数据。在这种情况下,可将 SignalR Core 终结点附加到客户端应用程序并实时通知 UI(参见 图 5****)。

The Sample Application in Action
图 5 操作中的示例应用程序

以下代码片段显示了在 gRPC 调用顶部使用 SignalR 集线器时客户端代码的变化:

var reply = response.ResponseStream.Current;
await _h2hHubContext.Clients
                    .Client(connId)
                    .SendAsync("responseReceived",
        reply.Player1.Name,
        reply.Player1.Won,
        reply.Player2.Name,
        reply.Player2.Won);

可查看源代码以获取解决方案的完整详细信息。说到 SignalR,有几点值得探索。首先,SignalR 代码仅由连接到 gRPC 服务的客户端应用程序使用。集线器注入客户端应用程序的控制器,而不是 gRPC 服务中。其次,就流式处理而言,值得注意的是 SignalR Core 也有其自己的流式处理 API。

其他类型的 gRPC 流

在本文中,我重点介绍了服务器端的 gRPC 流式处理方法,但这不是唯一的选择。gRPC 框架还支持客户端流式处理方法(多个请求/一个响应)和双向流式处理(多个请求/多个响应)。对于客户端流式处理,唯一的区别是使用 IAsyncStreamReader 作为服务方法中的输入流,如以下代码所示:

public override async Task<H2HReply> Multi(
         IAsyncStreamReader<H2HRequest> requestStream,
         ServerCallContext context){  while (await requestStream.MoveNext())
  {
    var requestPacket = requestStream.Current;   
      // Some other code here
      ...  } }

双向方法将返回 void,且不采用任何参数,因为它将通过输入和输出流读取和写入输入和输出数据。

总之,gRPC 是一个完整的框架,其可通过二进制、灵活和开源协议连接两个终结点(客户端和服务器)。你从 ASP.NET Core 3.0 中获得的对 gRPC 的支持将是惊人的,而且这份支持将随着时间的推移而增强,因此,现在就是入门并试用 gRPC 的绝佳时机,特别是在微服务到微服务通信方面。


Dino Esposito 在他 25 年的职业生涯中撰写了超过 20 本的书籍和 1000 篇的文章。Esposito 不仅是舞台剧《事业中断》的作者,还是 BaxEnergy 的数字策略分析师,正忙于编写有助于建设环保世界的软件。可以在 Twitter 上关注他 (@despos)。

衷心感谢以下 Microsoft 技术专家对本文的审阅:John Luo、James Newton-King
James Newton-King 是 ASP.NET Core 团队的工程师,负责适用于 .NET Core 的 gRPC 方面的工作

John Luo 是 ASP.NET Core 团队的工程师,负责适用于 .NET Core 的 gRPC 方面的工作


在 MSDN 杂志论坛讨论这篇文章