Septiembre de 2019

Volumen 34, número 9

[Tecnología de vanguardia]

Métodos de streaming en servicios gRPC de ASP.NET Core

Por Dino Esposito

Dino EspositoEn la entrega anterior de Tecnología de vanguardia, hablé sobre la creación de un nuevo tipo de servicio basado en el marco gRPC, que, aunque lleva tiempo disponible para los desarrolladores, en ASP.NET Core 3.0 debuta como un servicio nativo hospedado directamente por Kestrel. El marco gRPC está indicado para la comunicación binaria punto a punto entre puntos de conexión conectados, principalmente (aunque no necesariamente), microservicios. También admite soluciones técnicas actualizadas, como Google Protobuf para la serialización de contenido y HTTP/2 para el transporte.

Visual Studio 2019 incluye una plantilla de proyecto de ASP.NET Core 3.0 para crear el esqueleto de un servicio gRPC con tan solo algunos clics. Para obtener un manual básico sobre gRPC y el Starter Kit generado por Visual Studio, eche un vistazo a mi columna de julio en msdn.com/Magazine/mt833481. Este mes profundizo un poco más en gRPC. En primer lugar, hablaré de las herramientas subyacentes con un poco más de detalle. De hecho, se necesitan algunas herramientas para analizar el contenido del archivo .proto como clases de C# para usarlas como base de las implementaciones de cliente y de servicio. Además, abordaré los métodos de streaming y las clases de mensajes complejas. Por último, me centraré en cómo integrar los métodos de gRPC transmitidos mediante streaming en la interfaz de usuario de una aplicación web cliente.

Creación del servicio gRPC

La plantilla de proyecto de Visual Studio integrada coloca el archivo de definición de la interfaz del servicio (archivo .proto) en una subcarpeta denominada protos dentro del mismo proyecto de servicio. En este artículo, sin embargo, usaré un enfoque diferente y comenzaré por agregar una nueva biblioteca de clases de .NET Standard 2.0 a la solución inicialmente vacía.

La biblioteca de clases proto no contiene ninguna clase de C# de forma explícita. Todo lo que contiene es uno o varios archivos .proto. Puede organizar los archivos .proto en carpetas y subcarpetas como desee. En la aplicación de ejemplo, tengo un solo archivo .proto para un servicio de ejemplo que se encuentra en la carpeta de proyecto protos. Este es un fragmento del bloque de servicio del archivo .proto de ejemplo:

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

Se espera que el servicio de ejemplo H2H recupere cierta información relacionada con deportes de alguna ubicación remota. El método Details pasa una solicitud de encabezado a encabezado y recibe la puntuación de los últimos partidos entre los equipos o los jugadores especificados. Este es el aspecto que podrían tener los mensajes H2HRequest y H2HReply:

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

El primer tipo de mensaje pasa información sobre los equipos que se deben procesar, mientras que el segundo recibe el historial de los últimos partidos y una marca booleana que indica si la operación se ha realizado correctamente o no. Ningún problema por el momento. Todo lo que se encuentra en los mensajes se define como hemos visto en el artículo anterior. Usando la jerga de gRPC, el método Details es unario, es decir, cada solicitud recibe una (y solo una) respuesta. Sin embargo, esta es la forma más común de programar un servicio gRPC. Vamos a agregar funcionalidad de streaming, así:

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

El nuevo método MultiDetails es un método de streaming del lado servidor, lo que significa que, por cada solicitud que obtiene de algún cliente gRPC, puede devolver varias respuestas. En este ejemplo, el cliente puede enviar una matriz de solicitudes de encabezado a encabezado y recibir respuestas de encabezado a encabezado individuales de forma asincrónica a medida que se elaboran en el extremo del servicio. Para que esto suceda, el método del servicio gRPC se debe etiquetar con la palabra clave stream en la sección returns. Un método de streaming puede requerir también tipos de mensaje ad hoc, como se muestra aquí:

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

Como ya he comentado, el cliente puede solicitar un registro de encabezado a encabezado entre un equipo determinado y una matriz de otros equipos. La palabra clave repeated en el tipo de mensaje denota simplemente que el miembro OpponentTeam puede aparecer más de una vez. En términos de C#, el tipo de mensaje H2HMultiRequest es conceptualmente equivalente al siguiente seudocódigo:

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

Sin embargo, tenga en cuenta que el código generado por las herramientas de gRPC es ligeramente diferente, como se muestra aquí:

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

Observe que, de hecho, cualquier clase generada a partir de un tipo de mensaje T de gRPC implementa los miembros de la interfaz Google.ProtoBuf.IMessage<T>. El tipo de mensaje de respuesta debe diseñarse para describir los datos reales devueltos en cada paso de la fase de streaming. Por tanto, cada respuesta debe hacer referencia a una respuesta de encabezado a encabezado individual, entre el equipo principal y uno de los equipos oponentes especificados en la matriz, de este modo:

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

El tipo de mensaje H2HItem indica cuántos partidos ha ganado el equipo determinado contra el otro equipo especificado en la solicitud.

Antes de avanzar para ver la implementación de un método de streaming, echemos un vistazo a las dependencias que requiere la biblioteca de clases compartidas que inserta la definición de proto. El proyecto de Visual Studio debe hacer referencia a los paquetes NuGet de la figura 1.

Dependencias NuGet de la biblioteca de clases compartidas proto
Figura 1. Dependencias NuGet de la biblioteca de clases compartidas proto

El proyecto que incluye el archivo de código fuente .proto debe hacer referencia al paquete Grpc.Tools, así como a los paquetes Grpc.Net.Client (agregado en .NET Core 3.0 Preview 6) y Google.Protobuf que requiere cualquier proyecto de gRPC (ya sea de cliente, de servicio o de biblioteca). En última instancia, el paquete de herramientas es responsable de analizar el archivo .proto y de generar las clases de C# necesarias en tiempo de compilación. Un bloque de grupo de elementos en el archivo .csproj le indica al sistema de herramientas cómo proceder. Este es el código:

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

La parte que más nos interesa del bloque ItemGroup es el nodo Protobuf y, en particular, el atributo GrpcServices. El token Server con el valor de cadena asignado indica que las herramientas deben generar la clase de servicio para el prototipo de interfaz. El token Client indica que también se espera que cree la clase de cliente base para invocar al servicio. Con esto, el archivo .dll resultante contiene clases de C# para los tipos de mensaje, la clase de servicio base y la clase de cliente. Tanto el proyecto de servicio como el proyecto de cliente (ya sea de consola, web o de escritorio) solo tienen que hacer referencia al prototipo de archivo .dll para interactuar con el servicio gRPC.

Implementación del servicio

El servicio gRPC es un proyecto de ASP.NET Core con alguna configuración especial que se especifica en la clase Startup. Además de la plataforma de servidor de ASP.NET Core y del prototipo de ensamblado, también hace referencia al marco gRPC de ASP.NET Core y al paquete Google.Protobuf. La clase Startup agrega el servicio en tiempo de ejecución de gRPC en el método Configure y anexa puntos de conexión de gRPC en el método ConfigureServices, como se muestra en la figura 2.

Figura 2. Configuración del servicio 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>();
  });
}

La clase de servicio se hereda de la clase de servicio base que crearon las herramientas basándose en el contenido del archivo .proto, así:

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

Los métodos unarios, como el método Details, tienen una signatura más sencilla que los métodos de streaming. Devuelven un objeto Task<TReply> y aceptan un objeto TRequest y una instancia de ServerCallContext para acceder a los detalles principales de la solicitud entrante. Un método de streaming del lado servidor tiene un parámetro stream de respuesta adicional que usa el código de implementación para transmitir los paquetes de vuelta. La figura 3 muestra la implementación del método de streaming MultiRequest.

Figura 3. Método de servicio gRPC de streaming del lado servidor

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;
}

Como puede ver, en comparación con los métodos unarios clásicos, el método de streaming toma un parámetro adicional de tipo IServerStreamWriter<TReply>. Este es el flujo de salida que el método usará para devolver mediante streaming los resultados a medida que estén listos. En el código de la figura 3, el método entra en un bucle para cada una de las operaciones solicitadas (en este caso, una matriz de equipos para obtener los últimos partidos). Después, transmite los resultados mediante streaming a medida que los devuelve la consulta en una base de datos local o remota, o en un servicio web. Una vez finalizado, el método vuelve y el entorno en tiempo de ejecución cierra el flujo.

Escritura de un cliente para un método de streaming

En el código de ejemplo, la aplicación cliente es una aplicación de ASP.NET Core 3.0 simple. Contiene referencias al paquete Google.Protobuf y al paquete Grpc.Net.Client, además del prototipo de biblioteca compartido. La interfaz de usuario presenta un botón con algún código de JavaScript asociado que se envía a un método de controlador. Tenga en cuenta que no tiene por qué dejar de usar un formulario HTML clásico, salvo que el uso de Ajax para publicar mensajes facilite la recepción de notificaciones de las respuestas y la actualización de la interfaz de usuario de una manera más fluida. En la figura 4 se muestra el código.

Figura 4. Llamada al servicio 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);
}

Recordemos que el puerto del servicio gRPC depende del proyecto de Visual Studio, mientras que la clase de llamador de cliente se define en el prototipo de biblioteca. Para preparar la solicitud a un método de streaming del lado servidor, solo tiene que rellenar el tipo de mensaje de entrada. Como ya he comentado, la colección OpponentTeam es un tipo de .NET enumerable y se puede rellenar con AddRange o con llamadas repetidas a Add. El tipo real no es uno de los tipos de colección de .NET Core, pero sigue siendo un tipo de colección a pesar de implementarse en el paquete Google.Protobuf.

A medida que un método del lado servidor transmite paquetes mediante streaming hasta el final del flujo, la llamada real al método devuelve un objeto de flujo. A continuación, el código de cliente enumera los paquetes que esperan el final de la respuesta. Cada iteración del bucle while de la figura 4 captura un solo paquete de respuesta del servicio gRPC. Lo que sucede después depende de la aplicación cliente. En general, hay tres situaciones distintas.

Una es cuando la aplicación cliente tiene su propia interfaz de usuario, pero puede esperar para recopilar toda la respuesta antes de mostrar algo nuevo al usuario. En este caso, se cargan los datos que lleva el objeto de respuesta actual en el modelo de vista devuelto por el método de controlador. El segundo escenario es cuando no hay ninguna interfaz de usuario (por ejemplo, si el cliente es un microservicio de trabajo). En este caso, los datos recibidos se procesan tan pronto como están disponibles. Por último, en el tercer escenario, la aplicación cliente tiene su propia interfaz de usuario con capacidad de respuesta y les puede presentar los datos a los usuarios a medida que los recibe del servidor. En este caso, puede asociar un punto de conexión de SignalR Core a la aplicación cliente y enviar una notificación a la interfaz de usuario en tiempo real (vea la figura 5).

La aplicación de ejemplo en acción
Figura 5. La aplicación de ejemplo en acción

El fragmento de código siguiente muestra cómo cambia el código de cliente cuando se usa un centro de conectividad de SignalR con la llamada gRPC:

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

Puede consultar el código fuente para obtener todos los detalles de la solución. Hablando de SignalR, hay un par de puntos que merece la pena analizar. En primer lugar, el código de SignalR solo lo usa la aplicación cliente que se conecta al servicio gRPC. El centro de conectividad se inserta en el controlador de la aplicación cliente, no en el servicio gRPC. Y en segundo lugar, en lo que respecta a la transmisión mediante streaming, cabe señalar que SignalR Core tiene también su propia API de streaming.

Otros tipos de flujos gRPC

En este artículo me he centrado en los métodos de streaming gRPC del lado servidor, pero esta no es la única opción. El marco gRPC admite también métodos de streaming del lado cliente (varias solicitudes/una respuesta) y de streaming bidireccional (varias solicitudes/varias respuestas). En el caso del streaming del lado cliente, la única diferencia es el uso de IAsyncStreamReader como flujo de entrada en el método de servicio, como se muestra en este código:

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

Un método bidireccional devolverá void y no tomará ningún parámetro, ya que leerá y escribirá datos de entrada y de salida a través de flujos de entrada y de salida.

En Resumen, gRPC es un marco completo para conectar dos puntos de conexión (cliente y servidor) a través de un protocolo binario, flexible y de código abierto. La compatibilidad con gRPC que ofrece ASP.NET Core 3.0 es fascinante y mejorará con el tiempo, por lo que ahora es un momento ideal para comenzar a usar gRPC y experimentar con él, especialmente en la comunicación entre microservicios.


Dino Esposito ha escrito más de 20 libros y más de 1000 artículos en su carrera de 25 años. Autor de “The Sabbatical Break”, un espectáculo de estilo teatral, Esposito se dedica a escribir software para un mundo más verde como estratega digital de BaxEnergy. Puede seguirle en Twitter: @despos.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: John Luo, James Newton-King
James Newton-King es ingeniero del equipo de ASP.NET Core y trabaja en gRPC para .NET Core

John Luo es ingeniero del equipo de ASP.NET Core y trabaja en gRPC para .NET Core


Comente este artículo en el foro de MSDN Magazine