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
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.