Introdução
O Tonic é o framework gRPC mais maduro e popular do ecossistema Rust. Ele permite criar serviços gRPC de alta performance com suporte completo a Protocol Buffers, streaming bidirecional, interceptors, TLS e muito mais. Construído sobre Hyper e Tokio, o Tonic se integra perfeitamente ao ecossistema assíncrono do Rust.
O gRPC (Google Remote Procedure Call) é um framework de comunicação entre serviços que utiliza Protocol Buffers como formato de serialização e HTTP/2 como protocolo de transporte. Comparado com APIs REST tradicionais, o gRPC oferece serialização binária mais eficiente, contratos de API fortemente tipados, streaming bidirecional nativo e geração automática de código para múltiplas linguagens.
Por que usar Tonic?
- Performance: serialização binária com Protocol Buffers sobre HTTP/2
- Type safety: contratos de API verificados em tempo de compilação
- Streaming: suporte nativo a streaming unário, de servidor, de cliente e bidirecional
- Interoperabilidade: compatível com qualquer implementação gRPC (Go, Java, Python, etc.)
- Geração de código: código de servidor e cliente gerado automaticamente a partir de arquivos
.proto - Middleware: integração com Tower para interceptors e middleware composáveis
Instalação
Adicione as dependências ao seu Cargo.toml:
[dependencies]
tonic = "0.12"
prost = "0.13"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
[build-dependencies]
tonic-build = "0.12"
O tonic-build é usado no script de build (build.rs) para compilar arquivos .proto em código Rust.
Instalando o compilador protobuf
O Tonic precisa do compilador protoc instalado no sistema:
# Ubuntu/Debian
sudo apt install protobuf-compiler
# macOS
brew install protobuf
# Arch Linux
sudo pacman -S protobuf
# Ou via cargo
cargo install protobuf-codegen
Uso Básico
Definindo o serviço com Protocol Buffers
Crie o arquivo proto/tarefas.proto na raiz do projeto:
syntax = "proto3";
package tarefas;
// Serviço de gerenciamento de tarefas
service TarefaService {
// Operações unárias
rpc CriarTarefa (CriarTarefaRequest) returns (Tarefa);
rpc ObterTarefa (ObterTarefaRequest) returns (Tarefa);
rpc ListarTarefas (ListarTarefasRequest) returns (ListarTarefasResponse);
rpc AtualizarTarefa (AtualizarTarefaRequest) returns (Tarefa);
rpc DeletarTarefa (DeletarTarefaRequest) returns (DeletarTarefaResponse);
}
message Tarefa {
uint64 id = 1;
string titulo = 2;
string descricao = 3;
bool concluida = 4;
Prioridade prioridade = 5;
}
enum Prioridade {
BAIXA = 0;
MEDIA = 1;
ALTA = 2;
}
message CriarTarefaRequest {
string titulo = 1;
string descricao = 2;
Prioridade prioridade = 3;
}
message ObterTarefaRequest {
uint64 id = 1;
}
message ListarTarefasRequest {
int32 pagina = 1;
int32 por_pagina = 2;
}
message ListarTarefasResponse {
repeated Tarefa tarefas = 1;
int64 total = 2;
}
message AtualizarTarefaRequest {
uint64 id = 1;
optional string titulo = 2;
optional string descricao = 3;
optional bool concluida = 4;
optional Prioridade prioridade = 5;
}
message DeletarTarefaRequest {
uint64 id = 1;
}
message DeletarTarefaResponse {
bool sucesso = 1;
}
Configurando o build.rs
Crie o arquivo build.rs na raiz do projeto:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/tarefas.proto")?;
Ok(())
}
Para mais controle sobre a geração de código:
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(true)
.build_client(true)
.out_dir("src/generated")
.compile_protos(&["proto/tarefas.proto"], &["proto"])?;
Ok(())
}
Implementando o servidor
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tonic::{transport::Server, Request, Response, Status};
// Importar o código gerado pelo tonic-build
pub mod tarefas {
tonic::include_proto!("tarefas");
}
use tarefas::tarefa_service_server::{TarefaService, TarefaServiceServer};
use tarefas::*;
// Estado do serviço
struct MeuServicoTarefas {
tarefas: Arc<Mutex<HashMap<u64, Tarefa>>>,
proximo_id: Arc<Mutex<u64>>,
}
impl MeuServicoTarefas {
fn new() -> Self {
Self {
tarefas: Arc::new(Mutex::new(HashMap::new())),
proximo_id: Arc::new(Mutex::new(1)),
}
}
}
#[tonic::async_trait]
impl TarefaService for MeuServicoTarefas {
async fn criar_tarefa(
&self,
request: Request<CriarTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let req = request.into_inner();
let mut proximo_id = self.proximo_id.lock().await;
let id = *proximo_id;
*proximo_id += 1;
let tarefa = Tarefa {
id,
titulo: req.titulo,
descricao: req.descricao,
concluida: false,
prioridade: req.prioridade,
};
self.tarefas.lock().await.insert(id, tarefa.clone());
println!("Tarefa criada: {} (ID: {})", tarefa.titulo, id);
Ok(Response::new(tarefa))
}
async fn obter_tarefa(
&self,
request: Request<ObterTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let id = request.into_inner().id;
let tarefas = self.tarefas.lock().await;
match tarefas.get(&id) {
Some(tarefa) => Ok(Response::new(tarefa.clone())),
None => Err(Status::not_found(format!(
"Tarefa com ID {} não encontrada",
id
))),
}
}
async fn listar_tarefas(
&self,
request: Request<ListarTarefasRequest>,
) -> Result<Response<ListarTarefasResponse>, Status> {
let req = request.into_inner();
let pagina = req.pagina.max(1) as usize;
let por_pagina = req.por_pagina.max(1).min(100) as usize;
let tarefas = self.tarefas.lock().await;
let total = tarefas.len() as i64;
let lista: Vec<Tarefa> = tarefas
.values()
.skip((pagina - 1) * por_pagina)
.take(por_pagina)
.cloned()
.collect();
Ok(Response::new(ListarTarefasResponse {
tarefas: lista,
total,
}))
}
async fn atualizar_tarefa(
&self,
request: Request<AtualizarTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let req = request.into_inner();
let mut tarefas = self.tarefas.lock().await;
match tarefas.get_mut(&req.id) {
Some(tarefa) => {
if let Some(titulo) = req.titulo {
tarefa.titulo = titulo;
}
if let Some(descricao) = req.descricao {
tarefa.descricao = descricao;
}
if let Some(concluida) = req.concluida {
tarefa.concluida = concluida;
}
if let Some(prioridade) = req.prioridade {
tarefa.prioridade = prioridade;
}
Ok(Response::new(tarefa.clone()))
}
None => Err(Status::not_found(format!(
"Tarefa com ID {} não encontrada",
req.id
))),
}
}
async fn deletar_tarefa(
&self,
request: Request<DeletarTarefaRequest>,
) -> Result<Response<DeletarTarefaResponse>, Status> {
let id = request.into_inner().id;
let mut tarefas = self.tarefas.lock().await;
let sucesso = tarefas.remove(&id).is_some();
if !sucesso {
return Err(Status::not_found(format!(
"Tarefa com ID {} não encontrada",
id
)));
}
Ok(Response::new(DeletarTarefaResponse { sucesso }))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let servico = MeuServicoTarefas::new();
println!("Servidor gRPC rodando em {}", addr);
Server::builder()
.add_service(TarefaServiceServer::new(servico))
.serve(addr)
.await?;
Ok(())
}
Implementando o cliente
use tarefas::tarefa_service_client::TarefaServiceClient;
use tarefas::*;
pub mod tarefas {
tonic::include_proto!("tarefas");
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Conectar ao servidor
let mut client = TarefaServiceClient::connect("http://[::1]:50051").await?;
// Criar uma tarefa
let resposta = client
.criar_tarefa(CriarTarefaRequest {
titulo: "Aprender Tonic".to_string(),
descricao: "Estudar gRPC em Rust".to_string(),
prioridade: Prioridade::Alta as i32,
})
.await?;
let tarefa = resposta.into_inner();
println!("Tarefa criada: {:?}", tarefa);
// Listar tarefas
let resposta = client
.listar_tarefas(ListarTarefasRequest {
pagina: 1,
por_pagina: 10,
})
.await?;
let lista = resposta.into_inner();
println!("Total de tarefas: {}", lista.total);
for t in &lista.tarefas {
println!(" [{}] {} - concluída: {}", t.id, t.titulo, t.concluida);
}
// Atualizar tarefa
let resposta = client
.atualizar_tarefa(AtualizarTarefaRequest {
id: tarefa.id,
titulo: None,
descricao: None,
concluida: Some(true),
prioridade: None,
})
.await?;
println!("Tarefa atualizada: {:?}", resposta.into_inner());
// Deletar tarefa
let resposta = client
.deletar_tarefa(DeletarTarefaRequest { id: tarefa.id })
.await?;
println!("Deletada: {}", resposta.into_inner().sucesso);
Ok(())
}
Recursos Avançados
Streaming de servidor
O servidor envia múltiplas mensagens para uma única requisição do cliente:
// No arquivo .proto
service MonitorService {
rpc ObservarEventos (ObservarRequest) returns (stream Evento);
}
message ObservarRequest {
string filtro = 1;
}
message Evento {
string tipo = 1;
string mensagem = 2;
int64 timestamp = 3;
}
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};
// Implementação do server streaming
#[tonic::async_trait]
impl MonitorService for MeuMonitor {
type ObservarEventosStream = ReceiverStream<Result<Evento, Status>>;
async fn observar_eventos(
&self,
request: Request<ObservarRequest>,
) -> Result<Response<Self::ObservarEventosStream>, Status> {
let filtro = request.into_inner().filtro;
println!("Cliente observando eventos com filtro: {}", filtro);
let (tx, rx) = mpsc::channel(128);
// Spawn uma task que envia eventos periodicamente
tokio::spawn(async move {
let mut contador = 0;
loop {
contador += 1;
let evento = Evento {
tipo: "info".to_string(),
mensagem: format!("Evento #{} (filtro: {})", contador, filtro),
timestamp: chrono::Utc::now().timestamp(),
};
if tx.send(Ok(evento)).await.is_err() {
// Cliente desconectou
break;
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
Ok(Response::new(ReceiverStream::new(rx)))
}
}
Streaming de cliente
O cliente envia múltiplas mensagens e recebe uma única resposta:
service UploadService {
rpc EnviarDados (stream DadosChunk) returns (UploadResposta);
}
message DadosChunk {
bytes conteudo = 1;
int32 numero_chunk = 2;
}
message UploadResposta {
int64 bytes_recebidos = 1;
int32 chunks_processados = 2;
}
use tokio_stream::StreamExt;
use tonic::{Request, Response, Status, Streaming};
#[tonic::async_trait]
impl UploadService for MeuUpload {
async fn enviar_dados(
&self,
request: Request<Streaming<DadosChunk>>,
) -> Result<Response<UploadResposta>, Status> {
let mut stream = request.into_inner();
let mut bytes_total: i64 = 0;
let mut chunks = 0;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
bytes_total += chunk.conteudo.len() as i64;
chunks += 1;
println!(
"Chunk #{}: {} bytes recebidos",
chunk.numero_chunk,
chunk.conteudo.len()
);
}
println!(
"Upload concluído: {} bytes em {} chunks",
bytes_total, chunks
);
Ok(Response::new(UploadResposta {
bytes_recebidos: bytes_total,
chunks_processados: chunks,
}))
}
}
Streaming bidirecional
Ambos cliente e servidor enviam e recebem mensagens simultaneamente:
service ChatService {
rpc Chat (stream ChatMensagem) returns (stream ChatMensagem);
}
message ChatMensagem {
string usuario = 1;
string texto = 2;
int64 timestamp = 3;
}
use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, StreamExt};
use tonic::{Request, Response, Status, Streaming};
#[tonic::async_trait]
impl ChatService for MeuChat {
type ChatStream = ReceiverStream<Result<ChatMensagem, Status>>;
async fn chat(
&self,
request: Request<Streaming<ChatMensagem>>,
) -> Result<Response<Self::ChatStream>, Status> {
let mut stream_entrada = request.into_inner();
let (tx, rx) = mpsc::channel(128);
tokio::spawn(async move {
while let Some(resultado) = stream_entrada.next().await {
match resultado {
Ok(msg) => {
println!("[{}]: {}", msg.usuario, msg.texto);
// Eco: enviar resposta de volta
let resposta = ChatMensagem {
usuario: "Servidor".to_string(),
texto: format!("Recebi: '{}'", msg.texto),
timestamp: chrono::Utc::now().timestamp(),
};
if tx.send(Ok(resposta)).await.is_err() {
break;
}
}
Err(e) => {
eprintln!("Erro no stream: {}", e);
break;
}
}
}
});
Ok(Response::new(ReceiverStream::new(rx)))
}
}
Interceptors (middleware)
use tonic::{
metadata::MetadataValue,
transport::Server,
Request, Status,
};
// Interceptor de autenticação
fn autenticar(req: Request<()>) -> Result<Request<()>, Status> {
let token = req
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok());
match token {
Some(t) if t.starts_with("Bearer ") => {
let jwt = &t[7..];
// Aqui você validaria o JWT
if jwt == "token-valido" {
Ok(req)
} else {
Err(Status::unauthenticated("Token inválido"))
}
}
_ => Err(Status::unauthenticated(
"Token de autenticação ausente. Envie via header 'authorization: Bearer <token>'"
)),
}
}
// Interceptor de logging
fn logging(req: Request<()>) -> Result<Request<()>, Status> {
let metodo = req.metadata().get("grpc-method")
.and_then(|v| v.to_str().ok())
.unwrap_or("desconhecido");
println!("[LOG] Requisição recebida: {}", metodo);
Ok(req)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let servico = MeuServicoTarefas::new();
// Aplicar interceptors ao serviço
let servico_com_auth = TarefaServiceServer::with_interceptor(
servico,
autenticar,
);
Server::builder()
.add_service(servico_com_auth)
.serve(addr)
.await?;
Ok(())
}
Metadata (cabeçalhos gRPC)
use tonic::{metadata::MetadataValue, Request};
// Cliente enviando metadata
async fn enviar_com_metadata(
client: &mut TarefaServiceClient<tonic::transport::Channel>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut request = Request::new(ListarTarefasRequest {
pagina: 1,
por_pagina: 10,
});
// Adicionar metadata (cabeçalhos)
request.metadata_mut().insert(
"authorization",
MetadataValue::try_from("Bearer meu-token")?,
);
request.metadata_mut().insert(
"x-request-id",
MetadataValue::try_from("req-12345")?,
);
let resposta = client.listar_tarefas(request).await?;
// Ler metadata da resposta
let headers = resposta.metadata();
if let Some(versao) = headers.get("x-api-version") {
println!("Versão da API: {}", versao.to_str()?);
}
println!("Tarefas: {:?}", resposta.into_inner());
Ok(())
}
TLS e segurança
use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cert = tokio::fs::read("server.pem").await?;
let key = tokio::fs::read("server-key.pem").await?;
let ca_cert = tokio::fs::read("ca.pem").await?;
let identity = Identity::from_pem(cert, key);
let ca_certificado = Certificate::from_pem(ca_cert);
let tls_config = ServerTlsConfig::new()
.identity(identity)
.client_ca_root(ca_certificado); // mTLS
let addr = "[::1]:50051".parse()?;
let servico = MeuServicoTarefas::new();
Server::builder()
.tls_config(tls_config)?
.add_service(TarefaServiceServer::new(servico))
.serve(addr)
.await?;
Ok(())
}
Boas Práticas
1. Estruture o projeto corretamente
meu-servico-grpc/
proto/
tarefas.proto
src/
main.rs
server.rs
client.rs
build.rs
Cargo.toml
2. Use erros gRPC apropriados
use tonic::Status;
// Mapeie erros de domínio para Status gRPC
fn mapear_erro(erro: MeuErro) -> Status {
match erro {
MeuErro::NaoEncontrado(msg) => Status::not_found(msg),
MeuErro::Validacao(msg) => Status::invalid_argument(msg),
MeuErro::NaoAutorizado => Status::unauthenticated("Acesso negado"),
MeuErro::Proibido => Status::permission_denied("Sem permissão"),
MeuErro::Conflito(msg) => Status::already_exists(msg),
MeuErro::Interno(msg) => {
eprintln!("Erro interno: {}", msg);
Status::internal("Erro interno do servidor")
}
}
}
3. Implemente health checking
// Use o protocolo padrão de health check do gRPC
syntax = "proto3";
package grpc.health.v1;
service Health {
rpc Check (HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch (HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
4. Configure timeouts e limites
use std::time::Duration;
use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
Server::builder()
.timeout(Duration::from_secs(30))
.concurrency_limit_per_connection(256)
.add_service(TarefaServiceServer::new(MeuServicoTarefas::new()))
.serve(addr)
.await?;
Ok(())
}
5. Use Reflection para depuração
[dependencies]
tonic-reflection = "0.12"
use tonic_reflection::server::Builder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let reflection_service = Builder::configure()
.register_encoded_file_descriptor_set(tarefas::FILE_DESCRIPTOR_SET)
.build_v1()?;
Server::builder()
.add_service(TarefaServiceServer::new(MeuServicoTarefas::new()))
.add_service(reflection_service)
.serve("[::1]:50051".parse()?)
.await?;
Ok(())
}
Exemplos Práticos
Microserviço gRPC completo com health check
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tonic::{transport::Server, Request, Response, Status};
pub mod tarefas {
tonic::include_proto!("tarefas");
}
use tarefas::tarefa_service_server::{TarefaService, TarefaServiceServer};
use tarefas::*;
// ---- Camada de domínio ----
#[derive(Clone, Debug)]
struct TarefaDominio {
id: u64,
titulo: String,
descricao: String,
concluida: bool,
prioridade: i32,
}
impl From<TarefaDominio> for Tarefa {
fn from(t: TarefaDominio) -> Self {
Tarefa {
id: t.id,
titulo: t.titulo,
descricao: t.descricao,
concluida: t.concluida,
prioridade: t.prioridade,
}
}
}
// ---- Repositório ----
#[derive(Clone)]
struct TarefaRepo {
dados: Arc<RwLock<HashMap<u64, TarefaDominio>>>,
proximo_id: Arc<RwLock<u64>>,
}
impl TarefaRepo {
fn new() -> Self {
Self {
dados: Arc::new(RwLock::new(HashMap::new())),
proximo_id: Arc::new(RwLock::new(1)),
}
}
async fn criar(&self, titulo: String, descricao: String, prioridade: i32) -> TarefaDominio {
let mut proximo = self.proximo_id.write().await;
let id = *proximo;
*proximo += 1;
let tarefa = TarefaDominio {
id,
titulo,
descricao,
concluida: false,
prioridade,
};
self.dados.write().await.insert(id, tarefa.clone());
tarefa
}
async fn obter(&self, id: u64) -> Option<TarefaDominio> {
self.dados.read().await.get(&id).cloned()
}
async fn listar(&self, pagina: usize, por_pagina: usize) -> (Vec<TarefaDominio>, usize) {
let dados = self.dados.read().await;
let total = dados.len();
let lista: Vec<_> = dados
.values()
.skip((pagina - 1) * por_pagina)
.take(por_pagina)
.cloned()
.collect();
(lista, total)
}
async fn deletar(&self, id: u64) -> bool {
self.dados.write().await.remove(&id).is_some()
}
}
// ---- Serviço gRPC ----
struct ServicoTarefas {
repo: TarefaRepo,
}
#[tonic::async_trait]
impl TarefaService for ServicoTarefas {
async fn criar_tarefa(
&self,
request: Request<CriarTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let req = request.into_inner();
if req.titulo.is_empty() {
return Err(Status::invalid_argument("Título não pode ser vazio"));
}
if req.titulo.len() > 200 {
return Err(Status::invalid_argument("Título muito longo (máx 200 chars)"));
}
let tarefa = self
.repo
.criar(req.titulo, req.descricao, req.prioridade)
.await;
Ok(Response::new(tarefa.into()))
}
async fn obter_tarefa(
&self,
request: Request<ObterTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let id = request.into_inner().id;
self.repo
.obter(id)
.await
.map(|t| Response::new(t.into()))
.ok_or_else(|| Status::not_found(format!("Tarefa {} não encontrada", id)))
}
async fn listar_tarefas(
&self,
request: Request<ListarTarefasRequest>,
) -> Result<Response<ListarTarefasResponse>, Status> {
let req = request.into_inner();
let pagina = (req.pagina as usize).max(1);
let por_pagina = (req.por_pagina as usize).clamp(1, 100);
let (tarefas, total) = self.repo.listar(pagina, por_pagina).await;
Ok(Response::new(ListarTarefasResponse {
tarefas: tarefas.into_iter().map(Into::into).collect(),
total: total as i64,
}))
}
async fn atualizar_tarefa(
&self,
request: Request<AtualizarTarefaRequest>,
) -> Result<Response<Tarefa>, Status> {
let req = request.into_inner();
let mut dados = self.repo.dados.write().await;
match dados.get_mut(&req.id) {
Some(tarefa) => {
if let Some(titulo) = req.titulo {
tarefa.titulo = titulo;
}
if let Some(descricao) = req.descricao {
tarefa.descricao = descricao;
}
if let Some(concluida) = req.concluida {
tarefa.concluida = concluida;
}
if let Some(prioridade) = req.prioridade {
tarefa.prioridade = prioridade;
}
Ok(Response::new(tarefa.clone().into()))
}
None => Err(Status::not_found(format!(
"Tarefa {} não encontrada", req.id
))),
}
}
async fn deletar_tarefa(
&self,
request: Request<DeletarTarefaRequest>,
) -> Result<Response<DeletarTarefaResponse>, Status> {
let id = request.into_inner().id;
let sucesso = self.repo.deletar(id).await;
if !sucesso {
return Err(Status::not_found(format!(
"Tarefa {} não encontrada", id
)));
}
Ok(Response::new(DeletarTarefaResponse { sucesso }))
}
}
// ---- Ponto de entrada ----
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse()?;
let repo = TarefaRepo::new();
let servico = ServicoTarefas { repo };
println!("Microserviço gRPC de Tarefas");
println!("Endereço: {}", addr);
Server::builder()
.add_service(TarefaServiceServer::new(servico))
.serve(addr)
.await?;
Ok(())
}
Comparação com Alternativas
| Característica | Tonic (gRPC) | Axum (REST) | tarpc (RPC) |
|---|---|---|---|
| Protocolo | HTTP/2 + Protobuf | HTTP/1.1 ou /2 + JSON | TCP customizado |
| Serialização | Protocol Buffers (binário) | JSON (texto) | Bincode/Serde |
| Streaming | Nativo (4 tipos) | Via WebSocket/SSE | Limitado |
| Interoperabilidade | Multi-linguagem | Multi-linguagem | Somente Rust |
| Contrato | Arquivo .proto | OpenAPI/Swagger | Trait Rust |
| Geração de código | Automática (build.rs) | Não necessária | Macro proc |
| Performance | Excelente | Boa | Excelente |
| Ecossistema | Grande (multi-lang) | Enorme (HTTP) | Pequeno |
- Tonic vs REST (Axum): Use gRPC quando precisa de comunicação eficiente entre serviços internos. Use REST para APIs públicas e frontends web.
- Tonic vs tarpc: tarpc é mais simples e ideal para comunicação entre serviços Rust-to-Rust. Tonic é melhor quando precisa de interoperabilidade com outras linguagens.
- Cenário ideal para gRPC: microserviços internos com alto volume de comunicação, streaming de dados, e equipes usando múltiplas linguagens.
Conclusão
O Tonic torna o desenvolvimento de serviços gRPC em Rust acessível e produtivo. Com geração automática de código a partir de arquivos .proto, integração com o ecossistema Tokio/Tower e suporte completo a streaming, ele é a escolha natural para comunicação entre microserviços em Rust.
O gRPC brilha especialmente em cenários de comunicação interna entre serviços, onde a eficiência do Protocol Buffers e a tipagem forte dos contratos .proto proporcionam ganhos significativos de performance e confiabilidade.
Próximos passos
- Estude Tower para adicionar middleware (autenticação, rate limiting, logging) aos seus serviços gRPC
- Explore Protocol Buffers para modelar dados complexos
- Configure Tracing para observabilidade dos seus serviços gRPC
- Aprenda sobre Service Mesh e como o gRPC se integra com ferramentas como Istio e Linkerd