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

Começando com gRPC

    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 (0)