Un poco de historia
En la década de los 70 se empezo con la computación distribuida naciendo el conceptos como el RPC (Remote Procedure Call), Mensajeria, Colas, etc.
Entre las decadas 80 y 90 nacieron otras arquitecturas como COM/DCOM, CORBA y JAVA RMI entre otros y nos plantamos a finales de la decada de los 90, principios del siglo 21 con las Web APIs.
Salio el XMLHTTP, las REST, el SOAP con la definición y transporte de mensajes por XML, en plataformas de desarrollo Microsoft surgió el tan amado y odiado al mismo tiempo WCF ( Windows communication Fundation) . Desde entonces han ido evolucionando las Web APIS desde los engorrosos XML del SOAP, la evolución de las API REST hasta volverse prácticamente el estándar por defecto. Más tarde saldría el GraphQL y por ultimo volvería a surgir el concepto de RPC pero actualizado.
Google se saco de la manga un protocolo de comuniciación/serialización llamado Profobuf, un sistema de comunicación eficiente y rápido. Y del protobuf se retomo el concepto de RPC y nadio gRPC
Hasta aquí esta ultra resumida historia de los sistemas distribuidos
En que consiste
gRPC, como ya he comentado, funciona con protobuf que se basa en el protocolo de comunicación HTTP versión 2. Es un «universal» RPC framework open-source de alto rendimiento. Lo que quiere decir con «universal» es que es un framework que no tiene un unico stack tecnologico, es decir, sirve tanto para .NET como para python. Puedes tener el servidor gRPC (google Remote Procedure Call) en .NET y el cliente en python, go, java,… y viceversa.
Algunas características
- Comunicación binaria
- Basado en contratos
- Disponible para cualquier ecosistema
- Seguro por defecto al usar http v2
- Streaming Unidireccional o bidireccional
¿Entonces la API REST se mueren?
No. ¿Por que?
- Todavía no esta disponible en todos los navegadores
- Es complicado para el parseo en JS
- No va bien, de momento, para aplicaciones web
¿Entonces?. Su escenario ideal en estos momentos es usarlo en comunicación entre servidores o aplicaciones que no dependan de un navegador.
Protobuf
Para trabajar con protobuf (versión 3) debemos trabajar con unos ficheros llamados proto donde se definen los contratos con los que luego se generara código para gRPC. Para que se entienda, es como los archivos de definición OpenAPI/Swagger de las API REST.
Empecemos
Este es el projecto de ejemplo que voy a usar.
Los contratos
- Se generaran los clientes y servidor a partir de ellos
- Se utiliza un lenguaje para definir los contratos (protobuf)
- Lenguaje independiente (agnóstico), no depende de una tecnología especifica (.NET, java,…)
¿Como de rápido es?. Una imagen vale más que mil palabras.
¿Como funciona la serialización?
Esta parte es importante saberla porque te puede ahorrar muchos dolores de cabeza, sobretodo en el futuro si lo tienes en cuenta.
La serialización en protobuf a parte de ser binaria, funciona por posición. No es como el json que trabaja con el par clave-valor. En este caso la posición 1 corresponde con la posición 1 que definamos en el contrato. De esta forma se puede ahorrar mucho espacio.
Ejemplo.
Inicio/cabecera de los archivos proto
syntax = "proto3"; <- versión de protobuf, en este caso la versión 3
option csharp_namespace = "MeterReaderWeb.Services"; <- Esto es algo particular para .NET.
import "enums.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
Valor | Descripción | |
syntax | proto3 | Indicamos que usamos la version 3 de protobuf |
option csharp_namespace | MeterReaderWeb.Services | Esto es algo particular para .NET, le indicamos que namespace queremos cuando se genere el código de gRPC. En otros lenguages usan otros tags |
import | «google/protobuf/timestamp.proto»; | importamos de las librerias de protobuf el timestamp. |
import | «google/protobuf/empty.proto» | Es un tipo «vacio». Nos sirve para cuando llamamos a un methodo que no devuelve nada. Protobuf como tal «siempre» devuelve algo y para los casos que no queremos devolver nada pues simplemente hacemos que devuelva un empty. |
import | «enums.proto» | importamos un archivo propio de protobuf |
syntax = "proto3";
option csharp_namespace = "MeterReaderWeb.Services";
import "enums.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
/*
Definimos un servicio llamado MeterReadingService que se generara con el namespace MeterReaderWeb.Services con 3 metodos.
*/
service MeterReadingService {
/*ReadingPacket: tipo mensaje de entrada, StatusMessage: Tipo de mensaje devuelto*/
rpc AddReading(ReadingPacket) returns (StatusMessage);
/*En este caso el returns es un google.protobuf.Empty, lo que quiere decir que no recibiremos una respuesta o mejor dicho recibiremos como respuesta "vacio" y que es de tipo stream, es decir se mantendra un canal de streaming de envio hacia el servidor donde se mandaran mensajes de tipo ReadingMessage*/
rpc SendDiagnostics(stream ReadingMessage) returns (google.protobuf.Empty);
rpc CreateToken(TokenRequest) returns (TokenResponse);
}
/*Contrato de un mensaje. ¡¡¡Importante!!!! El numero que hay a continuación de la propiedad indica en que posición va.*/
message ReadingPacket {
/* con la palabra "repeated" le indicamos que es una array de tipo ReadingMessage */
repeated ReadingMessage readings = 1;
string notes = 2;
ReadingStatus succesful = 3;
}
message StatusMessage {
string message = 1;
ReadingStatus succesful = 2;
}
message TokenRequest {
string username = 1;
string password = 2;
}
message TokenResponse {
string token = 1;
/*Indicamos que es de tipo timestamp (fecha)*/
google.protobuf.Timestamp expiration = 2;
bool success = 3;
}
message ReadingMessage {
int32 custmerId = 1;
int32 readingValue = 2;
google.protobuf.Timestamp readingTime = 5;
/*Borrar campos puede provocar problemas serios, para tal cosa se puede usar la palabra "reserver" para "reservar" esa posición y el nombre aunque que no la usemos */
reserved 3;
reserved "succesful";
}
Aqui os dejo un link de como se definen los archivos proto
Quiero recalcar un tipo llamado «google.protobuf.Any» que nos puede servir cuando definimos un mensaje que desconocemos el tipo. Esto es util para tipos genericos, por ejemplo, queremos definir un mensaje que dentro contendra una propiedad que conceptualmente para nosotros es un tipo generico.
Imaginate que tu quieres esto
public class Event<TMessage>
{
public Guid Oid { get; set; }
public DateTime Sended { get; set; }
public List<TMessage> Message { get; set; }
}
La traducción podria ser algo como:
message Event
{
string oid = 1;
google.protobuf.Timestamp sended = 2;
repeated google.protobuf.Any message = 3;
}
O para saber que tipo es para la deserealización podríamos incluir un campo más indicando el tipo, algo así como esto
message Event
{
string oid = 1;
google.protobuf.Timestamp sended = 2;
string type = 3;
repeated google.protobuf.Any message = 4;
}
Aqui os dejo un link de como se definen los archivos proto.
Muy bonito todo pero vamos al lio
Creamos un proyecto
Abrimos el Visual Studio y vamos a crear un proyecto nuevo
Yo voy a usar NET7 pero podéis usar NET6 sin problemas.
Estructura del proyecto
Archivo protobuf + servicio que lo implementa
Como crear un «endpoint» o servicio gRPC nuevo
Primero crearemos un archivo protobuf, lo añadiremos dentro de la carpeta «proto»
Definimos en el archivo proto el servicio que se llama «WeatherService» que tendra como input «Location» y devolvera «Temperature».
Definimos los mensajes «Location» y «Temperature».
Location tiene una propiedad de tipo string llamada «City» y otra de tipo Timestamp (DateTime) para indicar el día. Y Temperature tiene una propiedad de tipo double llamada Celsius que nos devolvera la temperatura.
Ahora generaremos el servicio y los mensajes a partir del archivo proto.
1. Cambiar las propiedades del archivo proto
Tenemos que indicarle en el Build Action que es un «Protobuf compiler» y en el gRPC Stub Classes le diremos «Server only«. Porque server only? Porque si le decimos «client and server» nos generara el codigo para el cliente y el servidor a la vez y en este caso solo queremos el de servidor. Cuando querramos llamar a este servicio gRPC tendremos que coger el archivo proto meterlo en nuestro cliente y hacer exactamente lo mismo pero en el gRPC Stub Classes decirle «Client only» para que nos genere solo el cliente. Una vez hecho esto tenemos que compilar para que se generar las clases necesarias.
2. Creamos nuestro servicio
Creamos una clase nueva, en mi caso la llamare «WeatherService» para que coincida con el de proto (no es mandatory). Y la hacemos heredar de GrpcEjemplo.Protos.WeatherService.WeatherServiceBase.
Ahora tenemos que sobre escribir «send».
Ahora solo nos falta añadir nuestro servicio gRPC para que este disponible
Y ya tenemos creado nuestro servicio gRPC
3. Generamos el cliente
Vamos donde queremos consumir el servicio, en mi caso me he creado una aplicación de consola.
Añadimos el paquete gRPC.tools para que nos pueda generar los archivos c# a partir del proto.
Vamos al proyecto y le decimos que queremos añadir un nuevo servicio.
Seleccionamos gRPC
Le decimos donde esta nuestro archivo proto
Y automágicamente nos genera e importa lo que necesitemos.
Fijaros que nos ha creado un nueva carperta con el archivo de proto y con las settings del archivo correctamente.
Ahora creamos una instancia del gRPC client y hacemos una llamada
Ejecutamos y…..
Cositas
gRPC no nos olvidemos que trabaja con https/2 con lo cual todo lo que tenemos en las API REST lo seguimos teniendo.
- Podemos añadirle headers
- autenticación: Se hace exactamente igual que si fuera una API rest, es más si nuestra API le generamos los gRPC la autenticación del gRPC sera la misma que la de la API solo tenemos que marcar los gRPC services con el mismo tag que los controladores [Authorize]
- Se puede realizar una conexión stream donde podamos enviar y recibir información del servidor sin cerrar la conexión
¿Necesitas que te convenza?
He hecho una prueba con un servicio gRPC y una API Rest donde los 2 hacen exactamente lo mismo y aqui podemos ver el benchmark
Si hacemos el cuento de la vieja (870.4/1528.6) *100 = 56.94% eso quiere decir que gRPC tarda la mitad que la API REST asi que va casi el doble de rápido.