

RPC - Sistemas Distribuídos
O Conceito de RPC
RPC (Remote Procedure Call) é um paradigma de comunicação entre processos que permite que um programa invoque procedimentos ou funções em outra máquina remota como se fossem locais, abstraindo a complexidade da comunicação em rede.
Analogia: Imagine fazer um pedido em um restaurante - você (cliente) chama o garçom (stub cliente), que leva seu pedido (requisição) para a cozinha (servidor). O chef (procedimento remoto) prepara o prato (processa a requisição) e o garçom traz a resposta (resultado) para você, sem que você precise saber como a comida foi preparada.
Características Principais
- Transparência de localização: O programa cliente não precisa saber onde o servidor está fisicamente
- Abstração de rede: A complexidade da comunicação fica escondida nos stubs
- Suporte a múltiplas linguagens: Sistemas RPC modernos permitem interoperabilidade entre linguagens diferentes
Histórico
O conceito de RPC foi introduzido nos anos 1980 por Birrell e Nelson como parte do sistema Cedar na Xerox PARC. Tornou-se popular com a implementação do Sun RPC (agora conhecido como ONC RPC), que ainda é usado em sistemas NFS.
Funcionamento Detalhado do RPC
Fluxo Básico
- Chamada do cliente: O programa cliente chama uma função como se fosse local
- Serialização (marshalling): O stub cliente converte os parâmetros em um formato de rede
- Transmissão: A requisição é enviada via protocolo de transporte (TCP/UDP)
- Deserialização (unmarshalling): O stub servidor converte os dados para o formato local
- Execução: O procedimento real é executado no servidor
- Retorno: O resultado segue o caminho inverso até o cliente
Componentes Arquiteturais
- Stub Cliente: Representa localmente o procedimento remoto
- Runtime RPC: Gerencia a comunicação de rede
- Stub Servidor: Recebe chamadas e as repassa para a implementação real
- Esqueleto (Skeleton): Estrutura que despacha chamadas para os métodos corretos
Exemplo de Código
Interface (IDL - Interface Definition Language):
interface Calculadora {
double soma(in double a, in double b);
}
Cliente (pseudocódigo):
resultado = calculadora_remota.soma(5.2, 3.8);
print(resultado); // 9.0
Servidor (pseudocódigo):
implementação soma(double a, double b) { return a + b; }
Considerações de Projeto
- Semântica de chamada: "No máximo uma vez", "pelo menos uma vez" ou "exatamente uma vez"
- Tratamento de erros: Como lidar com falhas de rede ou servidor indisponível
- Segurança: Autenticação e criptografia das chamadas remotas
Exemplos Práticos de RPC
1. Sistema Bancário (gRPC)
Conceitos-chave: Definição de interfaces, Serialização binária, Chamadas síncronas
service BankService {
rpc GetBalance(AccountRequest) returns (BalanceResponse);
rpc Transfer(TransferRequest) returns (TransferResponse);
}
Aqui definimos o contrato do serviço usando Protocol Buffers:
- service declara um conjunto de operações remotas
- rpc define cada procedimento remoto com seus tipos de mensagem
- As mensagens (AccountRequest/BalanceResponse) serão definidas separadamente
class BankServicer(bank_pb2_grpc.BankServiceServicer):
def GetBalance(self, request, context):
# 1. Extrai o ID da conta da requisição
account_id = request.account_id
# 2. Consulta o banco de dados (simulado)
balance = database.get_balance(account_id)
# 3. Constrói e retorna a resposta
return bank_pb2.BalanceResponse(
balance=balance.amount,
currency=balance.currency
)
No servidor:
- Herda-se da classe gerada pelo compilador gRPC
- Cada método RPC recebe:
- request: dados desserializados
- context: metadados da chamada
- A lógica de negócios é executada como em uma função local
- O retorno é automaticamente serializado para envio ao cliente
2. Autenticação (JSON-RPC)
Conceitos-chave: RPC baseado em texto, Formato independente de linguagem, Semântica requisição-resposta
{
"jsonrpc": "2.0",
"method": "AuthService.Authenticate",
"params": {
"username": "joao",
"password": "senha123"
},
"id": 1
}
Estrutura da requisição JSON-RPC:
Campo | Finalidade |
---|---|
jsonrpc | Versão do protocolo (deve ser "2.0") |
method | Nome do serviço e método a ser chamado |
params | Objeto com os parâmetros da chamada |
id | Identificador único para correlacionar respostas |
{
"jsonrpc": "2.0",
"result": {
"authenticated": true,
"user_id": 1023,
"access_token": "eyJhbGciOi..."
},
"id": 1
}
Resposta bem-sucedida contém:
result: dados retornados pelo método - id: mesmo ID da requisição correspondente
Em caso de erro, haveria um campo error em vez de result com detalhes do problema.
3. Sistema de Recomendações (Apache Thrift)
Conceitos-chave: Definição de interfaces com IDL, Tipos complexos, Tratamento de erros remotos
service RecommendationService {
list<Product> GetRecommendations(
1: i32 user_id,
2: i16 num_items,
3: map<string, string> context
) throws (1: RecommendationError error)
}
Características importantes:
- Tipos de parâmetros são explicitamente definidos (i32, i16)
- Suporte a coleções complexas (list, map)
- Declaração explícita de erros que podem ser lançados (throws)
- Números de campo (1:, 2:) permitem evolução da API
4. Sistema de Arquivos Distribuídos (ONC RPC)
Conceitos-chave: RPC clássico, Serialização XDR, Operações remotas em sistemas de arquivos
readargs args = {
.file = file_handle,
.offset = 0,
.count = 1024
};
readres *result = nfsproc_read_1(&args, cl);
if (result != NULL) {
fwrite(result->filedata, 1, result->count, stdout);
}
Fluxo da operação:
Ferramentas Populares para RPC
Por que usar frameworks RPC? Eles abstraem a complexidade da comunicação de rede, oferecendo serialização eficiente, descoberta de serviços e tratamento de erros.
🅰️ gRPC
Criado por: Google
Linguagens: Multiplataforma (C++, Java, Python, Go, etc)
Destaque: Usa Protocol Buffers (binário) e HTTP/2
// Exemplo de definição
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
Melhor para: Microserviços de alta performance e streaming bidirecional
☕ Java RMI
Criado por: Sun Microsystems
Linguagens: Apenas Java
Destaque: Integração nativa com JVM
// Interface remota
public interface Calculator extends Remote {
int add(int a, int b) throws RemoteException;
}
Melhor para: Aplicações corporativas Java puras
🔀 Apache Thrift
Criado por: Facebook
Linguagens: Multiplataforma
Destaque: Suporte a tipos complexos e herança
// Exemplo Thrift IDL
service Calculator {
i32 add(1:i32 num1, 2:i32 num2)
}
Melhor para: Sistemas com múltiplas linguagens interoperando
📄 XML-RPC / JSON-RPC
Padrão: Aberto/SOAP
Linguagens: Qualquer com parser XML/JSON
Destaque: Legibilidade humana
// Exemplo JSON-RPC
{
"jsonrpc": "2.0",
"method": "subtract",
"params": [42, 23],
"id": 1
}
Melhor para: Integrações web simples e APIs públicas
🐍 Pyro5 (Python)
Criado por: Comunidade Python
Linguagens: Python
Destaque: Simplicidade e Python puro
# Exemplo Pyro5
import Pyro5.api
@Pyro5.api.expose
class Calculadora:
def soma(self, a, b):
return a + b
Melhor para: Prototipagem rápida e sistemas Python homogêneos
Comparativo Técnico
Ferramenta | Serialização | Transporte | Overhead | Complexidade |
---|---|---|---|---|
gRPC | Protocol Buffers (binário) | HTTP/2 | Baixo | Média |
Java RMI | Serialização Java | JRMP | Alto | Alta |
Apache Thrift | Binary/JSON/... | TCP/HTTP | Médio | Média |
JSON-RPC | JSON (texto) | HTTP | Alto | Baixa |
Pyro5 | Serpent (Python) | TCP | Médio | Baixa |
Exemplo: RPC com Python e XML-RPC
XML-RPC é um protocolo RPC que usa XML para codificação e HTTP como transporte. Ideal para integrações simples entre sistemas heterogêneos.
Arquitetura do Exemplo
Servidor XML-RPC
from xmlrpc.server import SimpleXMLRPCServer
class MathOperations:
def square(self, num):
"""Calcula o quadrado de um número"""
return num * num
def cube(self, num):
"""Calcula o cubo de um número"""
return num ** 3
# 1. Cria instância do servidor na porta 8000
server = SimpleXMLRPCServer(("localhost", 8000), allow_none=True)
# 2. Registra funções remotas
server.register_function(MathOperations().square, "square")
server.register_function(MathOperations().cube, "cube")
# 3. Ativa introspecção (lista métodos disponíveis)
server.register_introspection_functions()
print("Servidor XML-RPC rodando em http://localhost:8000")
try:
# 4. Mantém servidor ativo
server.serve_forever()
except KeyboardInterrupt:
print("\nEncerrando servidor...")
Funcionamento Detalhado:
- SimpleXMLRPCServer: Classe que implementa o servidor RPC básico
- Registro de métodos: Cada função deve ser explicitamente registrada
- Introspecção: Permite clientes descobrirem métodos disponíveis
- Segurança: Por padrão só aceita conexões locais (altere o endereço para '0.0.0.0' para rede)
Cliente XML-RPC
import xmlrpc.client
# 1. Cria proxy para conexão com servidor
proxy = xmlrpc.client.ServerProxy("http://localhost:8000")
# 2. Chama métodos remotos como se fossem locais
try:
number = 5
print(f"Quadrado de {number}: {proxy.square(number)}")
print(f"Cubo de {number}: {proxy.cube(number)}")
# 3. Lista métodos disponíveis (se introspecção ativada)
print("\nMétodos disponíveis:")
print(proxy.system.listMethods())
except ConnectionError as e:
print(f"Erro de conexão: {e}")
except Exception as e:
print(f"Erro durante chamada RPC: {e}")
Características do Cliente:
- ServerProxy: Representação local do servidor remoto
- Transparência: Chamadas remotas parecem locais
- Tratamento de erros: Importante para lidar com falhas de rede
- Introspecção: system.listMethods() mostra operações disponíveis
Tópicos Avançados
1. Servidor Multithread
from xmlrpc.server import SimpleXMLRPCServer
from socketserver import ThreadingMixIn
class ThreadedXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
pass
server = ThreadedXMLRPCServer(("localhost", 8000), allow_none=True)
Permite lidar com múltiplas requisições simultaneamente.
2. Chamadas Assíncronas
import xmlrpc.client
import threading
def async_call(proxy, method, args, callback):
def worker():
result = getattr(proxy, method)(*args)
callback(result)
threading.Thread(target=worker).start()
def print_result(result):
print("Resultado:", result)
async_call(proxy, "square", (5,), print_result)
Implementa padrão callback para não bloquear a thread principal.
Notas de Segurança
⚠️ Atenção: O XML-RPC padrão não é seguro para uso em redes públicas!
- Não use allow_none=True em produção
- Para ambiente real, considere:
- XML-RPC sobre HTTPS
- Autenticação básica HTTP
- Restrição de IPs clientes
- Usar alternativas como JSON-RPC ou gRPC
Testando o Servidor XML-RPC com Postman
O Postman pode ser usado para testar chamadas XML-RPC manualmente, enviando requisições HTTP brutas com o payload XML.
Passo a Passo para Testar
1. Configurar a Requisição
Método: POST
URL:http://localhost:8000
Headers:
- Content-Type: text/xml
- User-Agent: Postman

2. Criar o Corpo da Requisição (XML)
<?xml version="1.0"?>
<methodCall>
<methodName>square</methodName>
<params>
<param>
<value><i4>8</i4></value>
</param>
</params>
</methodCall>
No Postman:
- Selecione a tab Body
- Escolha raw
- Selecione XML no dropdown
- Cole o XML acima
3. Enviar e Verificar a Resposta
Resposta esperada (sucesso):
<?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><i4>64</i4></value>
</param>
</params>
</methodResponse>
Em caso de erro:
<?xml version='1.0'?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><i4>105</i4></value>
</member>
<member>
<name>faultString</name>
<value><string>Method not found</string></value>
</member>
</struct>
</value>
</fault>
</methodResponse>
Coleção Postman Pronta
Importe esta coleção pronta para testar rapidamente:
{
"info": {
"_postman_id": "a1b2c3d4-e5f6-7890",
"name": "XML-RPC Test Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Square Method",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "text/xml"
}
],
"body": {
"mode": "raw",
"raw": "<?xml version=\"1.0\"?>\n<methodCall>\n <methodName>square</methodName>\n <params>\n <param>\n <value><i4>8</i4></value>\n </param>\n </params>\n</methodCall>",
"options": {
"raw": {
"language": "xml"
}
}
},
"url": {
"raw": "http://localhost:8000",
"protocol": "http",
"host": ["localhost"],
"port": "8000"
}
}
},
{
"name": "List Methods",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "text/xml"
}
],
"body": {
"mode": "raw",
"raw": "<?xml version=\"1.0\"?>\n<methodCall>\n <methodName>system.listMethods</methodName>\n <params></params>\n</methodCall>",
"options": {
"raw": {
"language": "xml"
}
}
},
"url": {
"raw": "http://localhost:8000",
"protocol": "http",
"host": ["localhost"],
"port": "8000"
}
}
}
]
}
Como Importar:
- No Postman, clique em Import
- Selecione a tab Raw Text
- Cole o JSON acima
- Clique em Import
A coleção inclui exemplos para:
- Chamar o método square
- Listar todos os métodos disponíveis
Dicas Avançadas de Teste
1. Teste de Carga Básica
Use o Postman Runner para enviar múltiplas requisições sequenciais:
- Crie um ambiente com variável {{number}}
- Na aba Tests, adicione:
// Gera número aleatório entre 1-100 pm.environment.set("number", Math.floor(Math.random() * 100) + 1);
- No corpo XML, use <i4>{{number}}</i4>
- Execute a coleção 10-20 vezes
2. Validação de Resposta
Adicione scripts de teste na aba Tests:
// Verifica se a resposta é XML válido
pm.test("Content-Type is XML", function() {
pm.expect(pm.response.headers.get("Content-Type")).to.include("text/xml");
});
// Verifica se o status é 200
pm.test("Status code is 200", function() {
pm.response.to.have.status(200);
});
// Parseia o XML e verifica o resultado
const response = xml2Json(pm.response.text());
pm.test("Result is valid", function() {
pm.expect(response.methodResponse.params.param.value.i4).to.exist;
});
Exemplo Avançado: Sistema de Notificações com gRPC
gRPC é um framework RPC moderno que usa Protocol Buffers para serialização e HTTP/2 como transporte, ideal para sistemas em tempo real.
1. Definição do Serviço (arquivo .proto)
syntax = "proto3";
service NotificationService {
// Streaming bidirecional para notificações em tempo real
rpc Subscribe (SubscriptionRequest) returns (stream Notification);
// Chamada unária tradicional
rpc SendNotification (SendRequest) returns (SendResponse);
}
message SubscriptionRequest {
string user_id = 1; // ID do usuário assinante
}
message Notification {
string from_user = 1; // Remetente
string message = 2; // Conteúdo
string timestamp = 3; // Data/hora no formato ISO
}
message SendRequest {
string from_user = 1; // Quem envia
string to_user = 2; // Destinatário
string message = 3; // Conteúdo
}
message SendResponse {
bool success = 1; // Confirmação de entrega
}
Componentes Principais:
stream Notification
: Indica fluxo contínuo de mensagensmessage
: Define estruturas de dados serializáveisrpc
: Declara os endpoints remotos
2. Implementação do Servidor (Python)
import time
import threading
from concurrent import futures
class NotificationService(notification_pb2_grpc.NotificationServiceServicer):
def __init__(self):
self.subscriptions = {}
self.lock = threading.Lock()
def Subscribe(self, request, context):
"""Implementação do streaming server-side"""
user_id = request.user_id
# Adiciona usuário à lista de assinantes
with self.lock:
if user_id not in self.subscriptions:
self.subscriptions[user_id] = []
# Mantém conexão ativa enquanto o cliente estiver conectado
while context.is_active():
with self.lock:
if self.subscriptions[user_id]:
yield self.subscriptions[user_id].pop(0)
# Pausa para evitar consumo excessivo de CPU
time.sleep(1)
def SendNotification(self, request, context):
"""Método unário tradicional"""
with self.lock:
if request.to_user in self.subscriptions:
notification = notification_pb2.Notification(
from_user=request.from_user,
message=request.message,
timestamp=datetime.now().isoformat()
)
self.subscriptions[request.to_user].append(notification)
return notification_pb2.SendResponse(success=True)
return notification_pb2.SendResponse(success=False)
Pontos-chave:
threading.Lock
: Garante thread-safety nas operaçõescontext.is_active()
: Verifica se conexão está válidayield
: Envia mensagens conforme disponíveis (streaming)
3. Cliente Python
import grpc
import notification_pb2
import notification_pb2_grpc
def run_client():
channel = grpc.insecure_channel('localhost:50051')
stub = notification_pb2_grpc.NotificationServiceStub(channel)
# 1. Inicia assinatura (streaming)
user_id = "user_123"
response_stream = stub.Subscribe(
notification_pb2.SubscriptionRequest(user_id=user_id)
# Thread para receber notificações
def listen_notifications():
try:
for notification in response_stream:
print(f"\nNova notificação de {notification.from_user}:")
print(f"{notification.message}")
print(f"Em {notification.timestamp}")
except grpc.RpcError as e:
print(f"Conexão falhou: {e.code()}")
# 2. Envia notificação (unário)
def send_message():
response = stub.SendNotification(
notification_pb2.SendRequest(
from_user="user_456",
to_user="user_123",
message="Olá, como vai você?"
)
)
print("Notificação enviada com sucesso!" if response.success else "Falha ao enviar")
# Executa em threads separadas
threading.Thread(target=listen_notifications, daemon=True).start()
threading.Thread(target=send_message).start()
Fluxo do Cliente:
4. Recursos Avançados
Interceptores
class AuthInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metadata = dict(handler_call_details.invocation_metadata)
if not validate_token(metadata.get('authorization')):
raise grpc.RpcError(grpc.StatusCode.UNAUTHENTICATED)
return continuation(handler_call_details)
# Adiciona ao servidor:
server = grpc.server(
futures.ThreadPoolExecutor(),
interceptors=[AuthInterceptor()]
)
Adiciona autenticação JWT antes de processar chamadas.
Deadlines
# No cliente:
try:
response = stub.SendNotification(
request,
timeout=10 # 10 segundos de timeout
)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
print("Servidor não respondeu a tempo")
Evita chamadas bloqueantes indefinidamente.
Laboratório Prático: Sistema de Leilão com Pyro5
Este laboratório demonstra um sistema de leilão distribuído usando Pyro5, um framework RPC puro para Python. O sistema permite:
- Criação de leilões com itens e tempo determinado
- Lances concorrentes de múltiplos clientes
- Consulta de resultados ao final do leilão
1. Classe Principal do Serviço
@Pyro5.api.expose
class AuctionService:
def __init__(self):
self.auctions = {} # Armazena todos os leilões
self.lock = Lock() # Sincronização de threads
Elementos Chave:
- @Pyro5.api.expose: Torna a classe acessível remotamente
- self.auctions: Dicionário que mantém o estado dos leilões
- self.lock: Garante segurança com múltiplos clientes
2. Criação de Leilão
def create_auction(self, item_name, starting_price, duration):
with self.lock:
auction_id = str(int(time.time())) # ID único
self.auctions[auction_id] = {
'item': item_name,
'current_price': starting_price,
'end_time': time.time() + duration,
'bids': [] # Histórico de lances
}
return auction_id
Fluxo:
- Recebe dados do item e duração
- Gera ID usando timestamp
- Armazena estrutura inicial do leilão
- Retorna ID para referência futura
Uso: create_auction("Relógio Vintage", 250.00, 3600)
(1 hora)
3. Realização de Lances
def place_bid(self, auction_id, bidder_name, amount):
with self.lock:
auction = self.auctions.get(auction_id)
if not auction:
return False, "Leilão não encontrado"
if time.time() > auction['end_time']:
return False, "Leilão encerrado"
if amount <= auction['current_price']:
return False, "Lance muito baixo"
auction['current_price'] = amount
auction['bids'].append((bidder_name, amount))
return True, "Lance aceito"
Validações:
Retorno: Tupla (success: bool, message: str)
4. Consulta de Resultados
def get_auction_result(self, auction_id):
with self.lock:
auction = self.auctions.get(auction_id)
if not auction:
return None, "Leilão não encontrado"
if time.time() < auction['end_time']:
return None, "Leilão ainda em andamento"
if not auction['bids']:
return None, "Nenhum lance registrado"
winner = max(auction['bids'], key=lambda x: x[1])
return {
'item': auction['item'],
'winner': winner[0],
'winning_bid': winner[1],
'total_bids': len(auction['bids'])
}, "Resultado obtido"
Processamento:
- max(): Encontra o maior lance
- Estrutura de retorno:
{ "item": str, "winner": str, "winning_bid": float, "total_bids": int }
Implementação Completa do Servidor
import Pyro5.api
import time
from threading import Lock
def start_server():
daemon = Pyro5.api.Daemon()
ns = Pyro5.api.locate_ns()
uri = daemon.register(AuctionService)
ns.register("auction.service", uri)
print("Servidor de leilão pronto. URI:", uri)
daemon.requestLoop()
if __name__ == "__main__":
start_server()
Componentes Pyro5:
Elemento | Função |
---|---|
Daemon() | Recebe chamadas remotas |
locate_ns() | Conecta ao servidor de nomes |
register() | Disponibiliza o serviço |
Atividade Prática
Exercício 1: Cliente Básico
Crie um cliente que:
- Conecta ao servidor de leilão
- Cria um novo leilão para um item fictício
- Realiza 3 lances sequenciais
- Verifica o resultado
Atividades Propostas
Notificações em Tempo Real
Estender o sistema de leilão com notificações push para participantes quando:
- Novo lance é realizado
- Leilão está prestes a encerrar
- Resultado final é definido
Sugestão de implementação:
@Pyro5.api.expose
class AuctionCallback:
def notify(self, message):
print("Notificação:", message)
# Registrar callback no cliente
callback = AuctionCallback()
daemon.register(callback)
Benchmark gRPC vs Pyro5
Comparar desempenho com:
- Medição de latência (tempo de resposta)
- Throughput (operações por segundo)
- Uso de recursos (CPU, memória)
Métrica | gRPC | Pyro5 |
---|---|---|
Latência | X ms | Y ms |
Throughput | X ops/s | Y ops/s |
Sistema de Autenticação
Implementar:
- Registro de usuários
- Login com token JWT
- Autorização por níveis de acesso
def place_bid(self, auction_id, amount, token):
if not validate_jwt(token):
raise PermissionError("Acesso negado")
# Restante da lógica
Testes de Resiliência
Simular e tratar:
- Queda do servidor durante operações
- Perda de conexão de rede
- Timeout em chamadas remotas
Cenários para testar:
# Simular timeout
try:
response = stub.call_method(request, timeout=5)
except grpc.RpcError as e:
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
# Lógica de retry