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
Tonic Rust em 2026: onde ele encaixa
Quem busca Tonic Rust normalmente está em uma de três situações: já sabe o básico de Rust e quer criar um serviço gRPC real, trabalha com backend em Go/Java/Node/Python e está avaliando Rust para microserviços, ou viu vagas citando Protocol Buffers, streaming, sistemas distribuídos e baixa latência. Em todos os casos, Tonic é a peça que conecta Rust ao mundo gRPC sem abandonar o ecossistema assíncrono moderno.
Na prática, Tonic não compete com Axum em todos os cenários. Axum é excelente para APIs HTTP públicas, BFFs e JSON. Tonic é mais forte quando o consumidor também é um serviço controlado pela empresa: antifraude chamando score, plataforma chamando billing, serviço de dados enviando streaming, worker consultando catálogo interno ou um gateway conversando com backends em várias linguagens.
Para estudar com foco profissional, combine Tonic com Prost para Protocol Buffers, Tokio para runtime assíncrono, Tower para middleware e Tracing para observabilidade. Essa stack aparece em projetos de backend, infraestrutura, fintech, cripto, dados e developer tools. O guia complementar Rust com gRPC e Tonic: Microserviços em 2026 aprofunda arquitetura, versionamento de contrato, operação e portfólio.
Se o objetivo é carreira, procure nas vagas Rust por sinais como gRPC, Protobuf, microservices, streaming, Kubernetes, observability, platform engineering e distributed systems. Muitas empresas que usam Rust não anunciam “Tonic” no título da vaga, mas descrevem exatamente o tipo de serviço interno onde Tonic faz sentido.
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