Si solo necesitas el código puedes consultar este repo 👇
¿Qué es gRPC?
gRPC es un moderno framework RPC (Remote Procedure Call) de código abierto de Google. Se puede ejecutar en cualquier entorno y permite que diferentes servicios se comuniquen entre sí de una manera muy conveniente y eficaz. Con este framework puedes olvidarte de las desventajas de HTTP 1.1, del bajo rendimiento de JSON y de la "eterna" elección del método HTTP a utilizar. También es compatible con 14 lenguajes de programación, es independiente de la plataforma y tiene generación de código incorporada. ¿Te parece bien? Hablemos de todo ello con más detalle.
Fundamentos y ventajas de gRPC
Búferes de protocolo
Actualmente, JSON es el formato de intercambio de datos más popular. Es legible por humanos y puede funcionar con cualquier plataforma o lenguaje. Pero tiene una desventaja: en algunos casos es demasiado lento.
En gRPC este problema está resuelto. A diferencia de REST, gRPC utiliza Protocol Búferes (más tarde Protobuf) como formato de intercambio de datos. Al igual que JSON, es independiente del lenguaje y de la plataforma, pero es mucho más rápido y eficiente.
Protobuf también soporta más tipos de datos que JSON (por ejemplo, enums y funciones que este último no soporta). Los mensajes Protobuf tienen formato binario. Estos mensajes no sólo incluyen el mensaje en sí, sino también el conjunto de reglas y herramientas para definirlos e intercambiarlos.
HTTP 2.0
El uso de HTTP/2 es otra de las razones por las que gRPC es tan eficaz.
La diferencia clave entre HTTP/2 y HTTP/1.1 es la capa de entramado binario.
En un protocolo HTTP tradicional, no es posible enviar varias solicitudes u obtener varias respuestas a la vez en una única conexión. Será necesario crear una nueva conexión para cada una de ellas. Este tipo de multiplexación solicitud/respuesta es posible en HTTP/2 gracias a la capa de enmarcado binario.
Para nosotros, los desarrolladores, eso significa que podemos seguir gestionando peticiones cliente-servidor básicas como en HTTP/1.1, cuando el cliente envía una petición y el servidor envía una respuesta. Pero también podemos abrir conexiones de larga duración entre cliente y servidor, lo que nos permite hacer muchas más cosas en una sola petición.
El streaming
El streaming es uno de los conceptos principales de gRPC. Como se mencionó anteriormente, este enfoque nos permite enviar y/o recibir varios mensajes en una sola solicitud.
En gRPC, existen 3 tipos de streaming:
- Streaming del lado del servidor: cuando el cliente envía una única petición y el servidor puede enviar múltiples respuestas (flujo de respuestas);
- Streaming del lado del cliente: cuando el cliente envía múltiples peticiones (flujo de peticiones) y el servidor sólo devuelve una única respuesta;
- Streaming bidireccional: cuando el cliente y el servidor se envían mensajes al mismo tiempo sin esperar respuesta.
Generación de código
gRPC tiene incorporada la funcionalidad de generación de código, por lo que no es necesario utilizar terceras herramientas como Swagger. El compilador Protobuf (suministrado por gRPC) es una herramienta de línea de comandos para compilar el código IDL en los archivos .proto y generar código fuente en el lenguaje especificado. Es compatible con muchos lenguajes de programación (como Java, C#, Dart y muchos otros).
Esto hace de gRPC una gran tecnología para entornos multi-idioma donde se conectan muchos microservicios diferentes escritos en diferentes idiomas.
Cómo utilizarlo
Ahora que ya conocemos los fundamentos de gRPC vamos a aprender a usarlo.
Como ejemplo tomaré este proyecto en GitHub:
Allí podemos encontrar una implementación de gRPC con un servidor Dart y un cliente Flutter.
Es una aplicación sencilla donde el usuario puede crear su perfil, escribir publicaciones y dejar comentarios en ellas.
Vamos a echarle un vistazo.
Pasos de preparación:
1. En nuestro caso lo primero que tenemos que hacer es comprobar si tenemos Dart y Flutter instalados correctamente en nuestro ordenador. Para ello, introduce el siguiente comando en tu terminal/cmd: flutter --version
;
2. El siguiente paso es instalar el compilador protobuf mencionado anteriormente. Aquí están las instrucciones sobre cómo hacerlo;
3. Un paso más antes de empezar a escribir código es instalar protoc_plugin para Dart/Flutter (obviamente esto es sólo para desarrolladores de Dart/Flutter).
Así que ya estamos preparados.
Cómo escribir Proto
Primero vamos a crear nuestros archivos .proto donde definiremos nuestros servicios.
El primer archivo .proto que vamos a crear se llamará general.proto
syntax = "proto3";
message Empty {}
enum ProtoAction {
CREATE = 0;
DELETE = 1;
}
Como puedes ver aquí sólo definimos algunos tipos de datos.
El primero llamado Empty
e define con la palabra clave message
y no tiene nada dentro. Hablaremos de él más adelante. El ProtoAction
es un simple enum.
Ahora vamos a crear otro archivo llamado post.proto :
syntax = "proto3";
import "google/protobuf/timestamp.proto";
import "general.proto";
service PostService {
rpc CreatePost(ProtoPost) returns (Empty);
rpc GetAllPosts(Empty) returns (stream ProtoPost);
rpc DeletePost (ProtoPostId) returns (Empty);
}
message ProtoPostId {
int64 id = 1;
}
message ProtoPost {
int64 id = 1;
int64 user_id = 2;
google.protobuf.Timestamp date = 3;
string text = 4;
string user_name = 5;
ProtoAction action = 6;
}
Utilizamos la palabra clave message
para definir nuestros propios tipos de datos. Cada tipo de mensaje tiene campos y cada campo tiene un número para identificarlo de forma única en el tipo de mensaje. En este archivo creamos sólo dos tipos de datos, ProtoPostId
y ProtoPost
.
Utilizamos la palabra clave service
para definir nuestros servicios. Para llamar a un método, éste debe ser referenciado por su servicio. Esto es análogo a la class
y methods
. En este archivo creamos solo un servicio, PostsService
.
Usamos la palabra clave rpc
para definir métodos dentro de los servicios. En este archivo tenemos 3 métodos dentro de nuestro servicio, ellos son:
CreatePost
que toma el objetoProtoPost
como parámetro y devuelve el objeto Empty (que definimos en general.proto);GetAllPosts
que toma el objetoEmpty
como parámetro y devuelve un stream de objetosProtoPost
objects (streaming del lado del servidor);DeletePost
que toma el objetoProtoPostId
como parámetro y devuelve el objetoEmpty
(que definimos en general.proto).
Compilation
Después de esto tenemos que compilar nuestros archivos .proto. Ejecute el siguiente comando para compilar el archivo general.proto
:
protoc -I=. --dart_out=grpc:. post.proto
El subcomando I=.
le dice al compilador de la carpeta que archivo proto
estamos tratando de compilar.
El subcomando dart_out=grpc:.
le dice al compilador protoc que estamos generando código fuente Dart desde las definiciones post.proto
y usándolo para gRPC =grpc:
. El .
le dice al compilador que escriba los archivos dart en la carpeta raíz desde la que estamos operando.
Este comando generará los siguientes archivos:
post.pb.dart
;post.pbenum.dart
;post.pbgrpc.dart
;post.pbjson.dart
.
Uno de los archivos más importantes es post.pb.dart
, que contiene el código fuente Dart para las estructuras de datos de mensajes en el archivo post.proto
.
Otro importante es post.pbgrpc.dart
que contiene la clase PostServiceClient
que usaremos para crear instancias para llamar a los métodos rpc
y una interfaz PostServiceBase
. Esta interfaz será implementada por el servidor para añadir las implementaciones de los métodos.
Código de Dart
El siguiente paso es crear nuestro servidor dart (implementar interfaces). Aquí hay un código de post_service.dart :
class PostService extends PostServiceBase {
final DatabaseDataSource _databaseDataSource = DatabaseDataSource();
final CommentService _commentService = CommentService();
final StreamController<ProtoPost> _postsStream = StreamController.broadcast();
@override
Future<Empty> createPost(ServiceCall call, ProtoPost request) async {
final post = _databaseDataSource.createPost(request);
post.freeze();
var createdPost = post.rebuild((post) => post.action = ProtoAction.CREATE);
_postsStream.add(createdPost);
return Empty();
}
@override
Future<Empty> deletePost(ServiceCall call, ProtoPostId request) async {
var post = _databaseDataSource.getPost(request.id);
post.freeze();
var deletedPost = post.rebuild((post) => post.action = ProtoAction.DELETE);
_postsStream.sink.add(deletedPost);
for (var comment in _databaseDataSource.getCommentsByPostId(request.id)) {
await _commentService.deleteComment(call, ProtoCommentId(id: comment.id));
}
_databaseDataSource.deletePost(request.id);
return Empty();
}
@override
Stream<ProtoPost> getAllPosts(ServiceCall call, Empty request) async* {
for (var post in _databaseDataSource.getAllPosts()) {
yield post;
}
await for (var post in _postsStream.stream) {
yield post;
}
}
}
Como puedes ver hemos implementado nuestra interfaz PostServiceBase
y hemos sobreescrito todos los métodos que estaban definidos en nuestro archivo post.proto
.
Y finalmente veamos como podemos llamar a nuestros métodos del servidor en nuestro cliente. Este es un código del archivo remote_data_provider.dart:
class RemoteDataProvider {
UserServiceClient? _userServiceClient;
PostServiceClient? _postServiceClient;
CommentServiceClient? _commentServiceClient;
late final ClientChannel _channel;
RemoteDataProvider() {
_createChannel();
}
UserServiceClient get userServiceClient {
if (_userServiceClient != null) return _userServiceClient!;
_userServiceClient = UserServiceClient(_channel);
return _userServiceClient!;
}
PostServiceClient get postServiceClient {
if (_postServiceClient != null) return _postServiceClient!;
_postServiceClient = PostServiceClient(_channel);
return _postServiceClient!;
}
CommentServiceClient get commentServiceClient {
if (_commentServiceClient != null) return _commentServiceClient!;
_commentServiceClient = CommentServiceClient(_channel);
return _commentServiceClient!;
}
void _createChannel() {
_channel = ClientChannel(
host,
port: port,
options: const ChannelOptions(credentials: ChannelCredentials.insecure()),
);
}
void dispose() async {
await _channel.shutdown();
}
}
Como ya he mencionado anteriormente, para poder llamar a nuestros métodos rpc desde el servidor necesitamos crear instancias de [Name]ServiceClient
. El momento importante aquí es que también necesitamos crear la instancia ClientChannel
que luego pasaremos a cada uno de nuestros objetos [Name]ServiceClient
. Y luego simplemente los usamos (abajo hay algunos ejemplos de uso de PostServiceBase):
@override
Future<void> createPost(Post post) async {
await _postServiceClient.createPost(_postMapper.toProto(post));
}
@override
Future<void> deletePost(int postId) async {
await _postServiceClient.deletePost(_postMapper.toProtoPostId(postId));
}
Cuándo utilizarlo
Ya sabemos mucho sobre gRPC. Pensemos en casos reales de uso:
- Conexión de microservicios: debido al alto rendimiento de gRPC, esta tecnología será una gran solución para conectar arquitecturas compuestas por microservicios ligeros;
- Sistemas multi-idioma: con su generación de código incorporada, gRPC será muy útil a la hora de gestionar conexiones dentro de un entorno políglota;
- Streaming en tiempo real: dado que gRPC se basa en HTTP 2, se convierte en la tecnología más conveniente para aquellos casos en los que la comunicación en tiempo real es un requisito;
- Redes de bajo consumo y bajo ancho de banda: el uso de mensajes serializados Protobuf por parte de gRPC ofrece una mensajería ligera, mayor eficiencia y velocidad para redes de bajo consumo y ancho de banda limitado (especialmente si se compara con JSON).