Construyendo mi primer servicio gRPC con .NET Core.

Siempre que se diseña una aplicación basada en microservicios, uno de los mayores retos es garantizar la comunicación entre los microservicios. Al final de esta etapa siempre se arriba a dos soluciones, utilizar un servicio de mensajería para lograr una comunicación asíncrona entre nuestros servicios y por otra parte en menor medida se ejecutan llamadas entre los propios microservicios. Piense en la última solución, como se hace habitualmente, empleamos una petición HTTP, se debe parsear nuestra data al contenido JSON, manejar los verbos, enviar la petición y manejar las respuestas del servicio consumido. Si se piensa bien, es mucho trabajo para una simple llamada.  Entonces se debe cambiar la manera en la que se hace este tipo de comunicación, y llegar a una solución mucho más flexible, productiva y escalable, por ello en este artículo se propone usar gRPC como una alternativa para la comunicación directa entre los microservicios.

Requerimientos para este artículo.

  • Visual Studio 2019
  • .NET Core 3.0 SDK o superior.

Puede descargar el código usado para este artículo aquí.

¿Por qué gRPC?

Es un marco moderno RPC de alto rendimiento y código abierto, que puede ejecutarse en cualquier entorno. Puede conectar eficientemente servicios en centros de datos con soporte para balanceo de carga, rastreo, verificación de estado y autenticación. Las siguientes características define a gRPC como una de las mejores alternativas para la comunicación entre los microservicios:

  • Altamente eficiente y con un marco de definición de servicio simple.
  • Transmisión bidireccional con transporte basado en http/2. Permite manejar las peticiones de manera asíncrona.
  •  Soporta autenticación, seguimiento, equilibrio de carga y comprobación de estados.
  • Conexión de dispositivos móviles, clientes de navegador a servicios de backend.
  • Conexión eficiente de servicios políglotas en la arquitectura de estilo de microservicios.
  • Propone un marco simple de definición para los servicios.

¿Cómo se definen los servicios?

De forma predeterminada, gRPC usa Protocol Buffers, el mecanismo de código abierto de Google para serializar datos estructurados. El enfoque que utiliza es “Contrato Primero”. Se debe empezar por adicionar a la solución el archivo *.proto, donde se define el servicio y a su vez los mensajes de peticiones y respuestas. Este fichero se utiliza en ambas soluciones, los clientes y los servidores, de tal manera que siempre deben contener el mismo contenido.  

Veamos un poco de código, se desea desarrollar un microservicio capaz de acceder a una determinada fuente de datos de vehículos y devolver información sobre los mismos. Primer paso, crear el fichero *.proto para este servicio, en el cual se define el servicio Vehicle con los siguientes detalles:

  • Una llamada para obtener un vehículo (GetVehicle) a través de su identificador, esta recibe en la petición un mensaje de tipo SingleRequest, el mensaje solo define un atributo de tipo string, el cual especifica el id del vehículo que se desea obtener. La respuesta de esta llamada es un mensaje de tipo VehicleModel, definido en el fichero, el cual contiene la información básica de un auto.
  • La segunda llamada es usada para retornar varios vehículos (GetAll), esta llamada recibe un mensaje de tipo Request, el cual solo contiene una propiedad nombrada query, y es usada para filtrar en nuestra fuente datos. Como respuesta se define el mensaje QueryResponse, el cual contiene el mensaje de tipo Request y además los resultados de la búsqueda, es decir un listado de los vehículos encontrados.
syntax = "proto3";
option csharp_namespace = "VehicleGrpcService";
package vehicle;
service Vehicle {
  rpc GetVehicle (SingleRequest) returns (VehicleModel);
  rpc GetAll (Request) returns (QueryResponse);
}
message SingleRequest {
  string id = 1;
}
message Request {
  string query = 1;
}
message QueryResponse {
  Request request = 1;
  repeated VehicleModel results = 2;
}
message VehicleModel {
  string id = 1;
  string model = 2;
  string manufacturer = 3;
  int32 modelYear = 4;
}

Se puede notar la simplicidad que ofrece Protocol Buffers para definir los servicios, es un lenguaje común que es soportado por casi todos los lenguajes de programación. Para obtener más información sobre la sintaxis de los archivos protobuf, consulte la documentación oficial.

Implementación del servicio.

Hasta este punto ya está definido el servicio, entonces nuestro siguiente paso es ir a nuestra aplicación C# y adicionar el fichero .proto al grupo de elementos <Protobuf> en el archivo del proyecto:

<ItemGroup>
  <Protobuf Include="Protos\vehicle.proto" GrpcServices="Server" />
</ItemGroup>

Requiere el paquete de herramientas Grpc.Tools para generar los archivos de C# a partir de los ficheros *.proto.

  • Se generan según sea necesario cada vez que se construye el proyecto.
  • No se agregan al proyecto ni se registran en el control de versiones.
  • Son artefactos de compilación contenidos en el directorio obj.

El paquete de herramientas genera los tipos de C# que representan los mensajes definidos en los archivos * .proto incluidos.

En el caso del fichero vehicle.proto, en el servidor se genera un tipo base de servicio abstracto (Vehicle.VechicleBase). El tipo base contiene las definiciones de todas las llamadas gRPC contenidas en el archivo .proto. Se crea una implementación concreta del servicio que hereda de este tipo base e implemente la lógica para las llamadas gRPC. La clase VehicleService es la implementación concreta que anula los métodos e implementa la lógica que maneja la llamada gRPC.

Para mantener simple el ejemplo, nuestra fuente de datos es una simple lista con algunos vehículos de ejemplo.

public class VehicleService : Vehicle.VehicleBase
  {
    private readonly ILogger<VehicleService> _logger;
    public VehicleService(ILogger<VehicleService> logger)
    {
      _logger = logger;
    }
    public override Task<VehicleModel> GetVehicle(SingleRequest request, ServerCallContext context)
    {
      _logger.LogInformation($"Search vehicle with id: {request.Id}");
      var response = Storage.Vehicles.FirstOrDefault(e => e.Id == request.Id);
      return Task.FromResult(response);
    }
    public override Task<QueryResponse> GetAll(Request request, ServerCallContext context)
    {
      _logger.LogInformation("Search vehicles from request.", request);
      var result = string.IsNullOrWhiteSpace(request.Query)
        ? Storage.Vehicles
        : Storage.Vehicles.Where(e => e.Model.Contains(request.Query)
          || e.Manufacturer.Contains(request.Query));
      var response = new QueryResponse
      {
        Request = request,
      };
      response.Results.AddRange(result);
      return Task.FromResult(response);
    }
  }

¿Cómo consumir el servicio?

Para consumir el servicio vamos a usar una simple aplicación de consola. Adicione una nueva aplicación de consola de .NET Core e instale las siguientes dependencias:

  • Grpc.Tools se menciona anteriormente en la implementación del servidor, contiene el soporte para C# de los archivos protobuf.
  • Google.Protobuf, contiene API de mensajes de protobuf para C#.
  • Grpc.Net.Client, cliente gRPC para .NET Core.

Adicione el mismo fichero vehicle.proto en la aplicación cliente de la siguiente manera.

<ItemGroup>
  <Protobuf Include="Protos\vehicle.proto" GrpcServices="Client" />
</ItemGroup>

Construya el proyecto y adicione el siguiente código en el archivo Program.cs. El cual contiene la lógica para consumir el servicio Vehicle creado anteriormente. En este caso solo realiza la llamada a obtener el listado de autos, en el cual se puede especificar un filtro.

class Program
  {
    static async Task Main(string[] args)
    {
      try
      {
        Console.WriteLine("Search:");
        var query = Console.ReadLine();
        using var channel = GrpcChannel.ForAddress("https://localhost:5001");
        var client = new Vehicle.VehicleClient(channel);
        var vehicles = await client.GetAllAsync(new Request() { Query = query });
        Console.WriteLine($"Founded: {vehicles.Results.Count}");
        vehicles.Results.ToList().ForEach(e =>
        {
          Console.WriteLine(JsonSerializer.Serialize(e));
        });
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
      }
      finally
      {
        Console.ReadKey();
      }
    }
  }

Los clientes gRPC son tipos de clientes concretos que se generan a partir de los archivos * .proto. El cliente gRPC tiene métodos que se traducen al servicio gRPC en el archivo * .proto.

Se crea un cliente gRPC desde un canal. Comience usando GrpcChannel.ForAddress para crear un canal, y luego use el canal para crear el cliente gRPC.

Un canal representa una conexión de larga duración a un servicio gRPC. Cuando se crea un canal, se puede configurar con opciones relacionadas con la llamada al servicio. Por ejemplo, el tamaño máximo de mensaje de envío y recepción, el registro puede especificarse en GrpcChannelOptions y usarse con GrpcChannel.ForAddress. Para obtener una lista completa de opciones, consulte las opciones de configuración para crear el cliente.

Consideraciones para el uso de los canales:

  • Crear un canal puede ser una operación costosa. La reutilización de un canal para llamadas de gRPC proporciona beneficios de rendimiento.
  • Los clientes gRPC se crean con canales. Son objetos ligeros y no necesitan ser almacenados en caché o reutilizados.
  • Se pueden crear múltiples clientes gRPC desde un canal, incluidos diferentes tipos de clientes.
  • Los clientes creados desde el canal pueden realizar múltiples llamadas simultáneas.

Una llamada a gRPC se inicia llamando a un método en el cliente. El cliente gRPC manejará la serialización de los mensajes y dirigirá la llamada al servicio correcto. gRPC define diferentes métodos, la forma en que utiliza el cliente para realizar una llamada gRPC depende del tipo de método al que está llamando.Los tipos de métodos:

  • Unario, comienza con el cliente enviando una petición y cuando el servicio finaliza envía la respuesta al cliente.
  • Transmisión del servidor, el cliente envía una petición al servidor y va recibiendo mensajes transmitidos desde el servicio. El proceso no culmina hasta que el servidor no finaliza el envío de mensajes.
  • Transmisión de cliente, se inicia la transmisión cuando el cliente comienza a enviar un conjunto de mensajes. Cuando este termina notifica al servicio que ya no se enviarán más mensajes. La llamada finaliza cuando el servicio devuelve un mensaje de respuesta.
  • Transmisión bidireccional, el cliente y el servicio pueden enviarse mensajes entre sí en cualquier momento.

El ejemplo empleado en el artículo emplea el método Unario para realizar la llamada.

La demanda de los servicios en la actualizad obliga a los desarrolladores a mejorar su código, con el objetivo de alcanzar el rendimiento y la escalibilidad que requieren estos tiempos. Con este artículo ya tiene una introducción a esta nueva alternativa para la comunicación entre los microservicios. gRPC ofrece muchas facilidades y mejoras, estoy dispuesto a seguir indagando en las características y traerlas acá para seguir compartiendo en nuestra comunidad.

Happy Coding!