Article image
Léo Medeiros
Léo Medeiros21/01/2025 23:03
Compartilhe

Começando com gRPC

  • #Flutter
  • #Python
  • #API

O que é gRPC (Google Remote Procedure Call)?

Explicando de forma simples, o gRPC é um Estilo de Arquitetura para solução de APIs. Para fazer um paralelo, podemos comparar ela com outro Estilo de Arquitetura muito conhecido, o REST, porém as duas arquiteturas possuem grandes diferenças de implementação.

Enquanto o REST utiliza protocolo HTTP/1.1 e os formatos JSON e XML, o gRPC faz uso do HTTP/2 para o transporte de informações e Buffers de Protocolo para serialização.

Outro ponto muito importante, que pode ser decisivo na sua escolha entre as duas arquiteturas é que o gRPC tem transmissão bidirecional entre cliente e servidor, enquanto o REST funciona apenas no esquema de request-response.

Comparação de casos de uso reais: streaming bidirecional gRPC vs. request-response REST:

Atualizações do mercado de ações ao vivo

Uma plataforma de negociação fornece atualizações de preços de ações em tempo real para clientes e permite que os usuários enviem ordens de compra/venda simultaneamente.

1. Usando gRPC:

Uma conexão persistente é estabelecida entre o cliente (aplicativo de negociação) e o servidor (plataforma do mercado de ações).

O cliente transmite ordens de compra/venda para o servidor enquanto recebe continuamente atualizações de preços de ações ao vivo.

Exemplo:

Cliente: envia uma ordem de compra para ações "AAPL" a US$ 150/ação.

Servidor: transmite continuamente atualizações ao vivo dos preços das ações "AAPL" (por exemplo, US$ 149,8, US$ 150,2, US$ 151,0).

Cliente: atualiza a ordem para comprar a US$ 151/ação com base nas alterações de preço, enquanto ainda recebe atualizações.

Servidor: processa a ordem atualizada e confirma a negociação.

2. Usando REST:

O cliente pesquisa periodicamente o servidor para atualizações de preços de ações usando solicitações HTTP repetidas.

O cliente envia ordens de compra/venda como solicitações HTTP separadas.

Exemplo:

Cliente: Envia uma solicitação GET para buscar o preço atual das ações "AAPL".

Servidor: Responde com o preço: $ 150/ação.

Cliente: Envia uma solicitação POST para fazer uma ordem de compra para "AAPL" a $ 150/ação.

Servidor: Confirma a ordem.

Implementação simples Full Stack de um Front-end em Flutter e um Back-end em Python

Uma das maiores vantagens de gRPC é fazer conexões entre cliente e servidor para várias linguagens diferentes de forma eficiente e bidirecional. Esta arquitetura utiliza protocol buffers (buffers de protocolo) tanto como sua Interface Definition Language (IDL) quanto para seu formato de troca de mensagens.

Para esse exemplo, iremos utilizar o repositório flutter_python_starter de maxim-saplin, que nos ajuda a usar código Python juntamente com todas plataformas que o Flutter suporta. Esse repositório tem vários scripts que facilitam a configuração dos arquivos necessários para a configuração efetiva da comunicação entre cliente Flutter e server Python.

Pré-requisitos:

  • Flutter SDK
  • Python 3.9+
  • Gerenciador de pacotes Chocolately e Git Bash (para Windows)
  • VSCode é recomendado como IDE

Começamos criando a pasta raiz do nosso projeto e clonando o repositório acima (se estiver no Windows sugiro usar o terminal do GitBash)

$ mkdir helloworld && cd $_ && git clone git@github.com:maxim-saplin/flutter_python_starter.git
$ cp -r ./flutter_python_starter/starter-kit ./
$ rm -rf flutter_python_starter

Agora criamos as pastas necessárias para nosso projeto

$ mkdir app server protos

A hierarquia de pastas que iremos usar no projeto segue o seguinte formato:

/helloworld/
/-- app/ (Flutter)
/-- server/ (Python)
/-- protos/ (ProtoBufs)
/-- starter-kit

Podemos inicializar nosso projeto Flutter dentro da pasta app, usando apenas windows e linux

$ flutter create ./app --empty --platforms=windows,linux

Já dentro da pasta protos, criaremos nosso primeiro ProtoBuff que definira o serviço da nossa API e a estrutura da nossa requisição e resposta.

$ touch ./protos/service.proto && nano $_

O código abaixo permitirá que o ProtoBuff gere em tempo de compilação código específico para interagir com bibliotecas específicas de certas linguagens.


syntax = "proto3";

// Service for API
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// Resquest
message HelloRequest {
  string name = 1;
}

// Response
message HelloReply {
  string message = 1;
}

O comando abaixo gerará os arquivos Dart/Flutter e Python necessários a partir do ProtoBuff acima

$ chmod 755 ./starter-kit/prepare-sources.sh; chmod 755 ./starter-kit/bundle-python.sh
$ ./starter-kit/prepare-sources.sh --proto ./protos/service.proto --flutterDir ./app --pythonDir ./server

Após o termino da instação das bibliotecas e criação dos arquivos necessários, alguns arquivos foram criados nas pastas app e server. A pasta server deverá estar com essa hierarquia:

helloworld/
|-- server/ (Python)
  |-- grpc_generated/
  |-- requirements.txt
  |-- server.py

Na etapa anterior, o compilador protoc criou stubs Python em grpc_generated/, adicionou requirements.txt com dependências gRPC e copiou o código do modelo server.py que inicia um novo servidor gRPC.

Agora criaremos nosso módulo de negócio service.py que implementará o serviço definido em grpc_generated/service_pb2_grpc.py e grpc_generated/service_pb2.py

$ touch ./server/service.py && nano $_

O código abaixo servirá como nosso módulo de serviço

from concurrent import futures
from grpc_generated import service_pb2_grpc
from grpc_generated import service_pb2


class GreeterService(service_pb2_grpc.GreeterServicer):
  def SayHello(self, request, context):
      return service_pb2.HelloReply(message="Hello, %s!" % request.name)

Atualize o arquivo server.py para incluir a implementação de seu módulo de serviço

...
# TODO, import your service implementation
from service import GreeterService
...
def serve():
...
# TODO, add your gRPC service to self-hosted server, e.g.
service_pb2_grpc.add_GreeterServicer_to_server(GreeterService(), server)

Você pode tentar executar o server.py no terminal. Se tudo deu certo, você receberá uma mensagem de que ele está escutando no localhost:

$ python3 ./server/server.py 
gRPC server started and listening on localhost:50055

Para conectar nosso aplicativo Flutter ao server Python, só teremos que fazer alterações no arquivo main.dart

Importar as ligações gRPC necessárias e os arquivos auxiliares no início de main.dart

import 'package:flutter/material.dart';
import 'dart:ui';
import 'package:app/grpc_generated/init_py.dart';
import 'package:app/grpc_generated/client.dart';
import 'package:app/grpc_generated/service.pbgrpc.dart';

Inicializar o Python alterando a função main()

Future<void> pyInitResult = Future(() => null);

void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();


runApp(const MainApp());
}

initPy() é o método auxiliar que cuida de rodar o servidor e configurar os canais do cliente.

Note que o método retorna um Future que não é aguardado, mas sim salvo em uma var global. Isso é feito de propósito, já que a inicialização do servidor Pyhton pode ser demorada e não queremos que a UI fique irresponsiva. Além disso, evita a ocorrência de erros.

Adicione WidgetsBindingObserver para responder ao evento de fechamento do aplicativo e desligar o servidor Python

class MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
  shutdownPyIfAny();
  return super.didRequestAppExit();
}


@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
}
...

Observe que shutdownPyIfAny() é a função que emite um comando do sistema operacional para fechar o processo do servidor.

Usamos o FutureBuilder para exibir o status da inicialização do Python

...
SizedBox(
height: 50,
child:
// Add FutureBuilder that awaits pyInitResult
    FutureBuilder<void>(
  future: pyInitResult,
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Stack(
        children: [
          SizedBox(height: 4, child: LinearProgressIndicator()),
          Positioned.fill(
            child: Center(
              child: Text(
                       'Loading Python...',
               ),
             ),
           ),
         ],
       );
     } else if (snapshot.hasError) {
       // If error is returned by the future, display an error message
       return Text('Error: ${snapshot.error}');
     } else {
       // When future completes, display a message saying that Python has been loaded
       // Set the text color of the Text widget to green
       return const Text(
              'Python has been loaded',
         style: TextStyle(
           color: Colors.green,
         ),
       );
     }
   },
 ),
 ),
...

E finalmente chame o cliente gRPC fazendo a solicitação

...
const SizedBox(height: 16),
Text(
title,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
 onPressed: () {
   GreeterServiceClient(getClientChannel())
       .sayHello(HelloRequest(name: 'world'))
       .then((p0) => setState(() => title = p0.message));
 },
 style: ElevatedButton.styleFrom(
   minimumSize:
       const Size(140, 36), // Set minimum width to 120px
 ),
 child: const Text('Requisição gRPC'),
 ),
],
...

Aqui está o main.dart completo com as chamadas via Python

import 'package:flutter/material.dart';
import 'dart:ui';
import 'package:app/grpc_generated/init_py.dart';
import 'package:app/grpc_generated/client.dart';
import 'package:app/grpc_generated/service.pbgrpc.dart';


Future<void> pyInitResult = Future(() => null);
void main() {
WidgetsFlutterBinding.ensureInitialized();
pyInitResult = initPy();


runApp(const MainApp());
}


class MainApp extends StatefulWidget {
const MainApp({super.key});


@override
State<MainApp> createState() => _MainAppState();
}


class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
@override
Future<AppExitResponse> didRequestAppExit() {
  shutdownPyIfAny();
  return super.didRequestAppExit();
}


@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addObserver(this);
}


String title = 'Testando Hello World';


@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      body: Container(
        padding: const EdgeInsets.all(20),
        alignment: Alignment.center,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text.rich(
              TextSpan(
                children: [
                  const TextSpan(
                    text: 'Using ',
                  ),
                  TextSpan(
                    text: '$defaultHost:$defaultPort',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  TextSpan(
                    text:
                        ', ${localPyStartSkipped ? 'skipped launching local server' : 'launched local server'}',
                  ),
                ],
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 50,
              child:
                  // Add FutureBuilder that awaits pyInitResult
                  FutureBuilder<void>(
                future: pyInitResult,
                builder: (context, snapshot) {
                  if (snapshot.connectionState == ConnectionState.waiting) {
                    return const Stack(
                      children: [
                        SizedBox(height: 4, child: LinearProgressIndicator()),
                        Positioned.fill(
                          child: Center(
                            child: Text(
                              'Loading Python...',
                            ),
                          ),
                        ),
                      ],
                    );
                  } else if (snapshot.hasError) {
                    // If error is returned by the future, display an error message
                    return Text('Error: ${snapshot.error}');
                  } else {
                    // When future completes, display a message saying that Python has been loaded
                    // Set the text color of the Text widget to green
                    return const Text(
                      'Python has been loaded',
                      style: TextStyle(
                        color: Colors.green,
                      ),
                    );
                  }
                },
              ),
            ),
            const SizedBox(height: 16),
            Text(
              title,
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () {
                GreeterServiceClient(getClientChannel())
                    .sayHello(HelloRequest(name: 'world'))
                    .then((p0) => setState(() => title = p0.message));
              },
              style: ElevatedButton.styleFrom(
                minimumSize:
                    const Size(140, 36), // Set minimum width to 120px
              ),
              child: const Text('Requisição gRPC'),
            ),
          ],
        ),
      ),
    ),
  );
}
}

Agrupando o executável Python

Execute o script bundle-python.sh para criar um executável Python independente (usando o PyInstaller) e agrupe-o como um ativo no projeto Flutter

./starter-kit/bundle-python.sh --flutterDir ./app --pythonDir ./server

Se você estiver usando o VSCode, poderá executar o aplicativo via F5 como um aplicativo de desktop e obter a seguinte IU

imageimage

Aqui está uma maneira simplificada de começar a implementar gRPC nas suas aplicações!

Quaisquer dúvidas me procurem no linkedn que eu tento ajudar no que eu conseguir.

Referências:

Observable Flutter: gRPC

Github grpc

Github flutter_python_starter

Devto Integrating Flutter and Python

Introduction to gRPC

Compartilhe
Comentários (4)
Léo Medeiros
Léo Medeiros - 22/01/2025 14:27

Oi DIO!

A maior dificuldade que tive trabalhando com gRPC pela primeira vez definitivamente foi entender como os ProtoBuffs, com uma linguagem fortemente tipada, poderia gerar soluções robustas e escaláveis de serviço e mensageria entre cliente e servidor. O formato dos ProtoBuffs é relativamente simples e sua capacidade de gerar arquivos de messangem (*_pb2.*) e serviço (*_pb2_grpc.*) para multiplas plataformas realmente foi um desafio inicial para eu conseguir capturar de forma mais concreta como e onde cada serviço e mensageria seriam utilizados e a forma mais eficiente de trabalhar com eles.

A oportunidade de usar esse tipo de arquitetura surgiu da necessidade de rodar um modelo de machine learning em uma aplicação de controle financeiro que estou desenvolvendo atualmente com front end em Flutter. Dentre as opções disponíveis que pesquisei a mais eficaz seria o uso da arquitetura gRPC ao invés de tentar rodar código python dentro da lógica de negócios através de bibliotecas disponíveis atualmente no pub.dev como chaquopy.

Nessa aplicação de controle financeiro preciso enviar um buffer de dados extraídos de uma imagem capturada pela câmera ao modelo de machine learning para realizar alguns procedimentos de extração de dados. A natureza bidirecional do gRPC foi a forma que decidi integrar o front-end Flutter com o back-end em Python.

DIO Community
DIO Community - 22/01/2025 13:49

Parabéns pelo artigo sobre gRPC, Léo! Você conseguiu abordar um tema técnico de forma clara e prática, introduzindo os conceitos fundamentais do gRPC e explicando como essa tecnologia pode ser aplicada para criar sistemas mais eficientes e escaláveis. A maneira como você destacou as vantagens do gRPC é extremamente útil para quem deseja explorar alternativas modernas ao REST.

Conta aí, qual foi o maior desafio que você enfrentou ao trabalhar com gRPC pela primeira vez? Tem alguma dica ou exemplo prático que você recomendaria para quem está começando a implementar essa tecnologia?

Léo Medeiros
Léo Medeiros - 22/01/2025 10:15

Obrigado William! Sim realmente parece com WebSocket! Obrigado por comentar e bons estudos!

William Silva
William Silva - 22/01/2025 06:13

Parece que essa é uma variação da gRPC padrão é a  gRPC Bidirectional Streaming se assemelha muito com a WebSocket, ja agradeço por compartilhar essa info e desejo bons estudos 🎉