Si solo necesitas el código puedes consultar este repo 👇

GitHub - flutterwtf/Flutter-gRPC-Sample
Contribute to flutterwtf/Flutter-gRPC-Sample development by creating an account on GitHub.

¿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:

GitHub - flutterwtf/Flutter-gRPC-Sample
Contribute to flutterwtf/Flutter-gRPC-Sample development by creating an account on 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 objeto ProtoPost como parámetro y devuelve el objeto Empty (que definimos en general.proto);
  • GetAllPosts que toma el objeto  Empty como parámetro y devuelve un stream de objetos ProtoPost objects (streaming del lado del servidor);
  • DeletePost que toma el objeto ProtoPostId como parámetro y devuelve el objeto Empty (que definimos en general.proto).
💡
Otros archivos .proto se encuentran en el proyecto.

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.

💡
Debe ejecutar este comando para cada archivo .proto que desee compilar.

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 .

💡
Probablemente habrás notado que en este archivo tenemos varias importaciones de otros archivos proto-generados y otros servicios. El código fuente completo está disponible en el repositorio de GitHub. El enlace está arriba.

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).
Share this post