1

gRPC sem mistério: o que é e quando usar no .NET 10

Introdução#

Imagine um plano de controle que precisa conversar continuamente com agentes instalados em vários hosts. A API envia comandos, cada agente publica heartbeats (sinais periódicos de que continua ativo), informa mudanças no inventário e pode abrir um canal interativo para um terminal. Uma sequência de chamadas HTTP independentes ainda funcionaria para operações pontuais, mas não expressaria tão naturalmente esse fluxo persistente nos dois sentidos. É nesse tipo de problema que gRPC no .NET 10 merece ser considerado.

gRPC é uma estrutura de chamadas de procedimentos remotos (RPC, de Remote Procedure Call): o cliente invoca métodos definidos por contrato em um servidor, usando mensagens tipadas que normalmente são serializadas com Protocol Buffers e transportadas por HTTP/2. Eu o escolheria quando os participantes são serviços sob controle da mesma equipe, precisam compartilhar contratos gerados ou manter um fluxo de mensagens contínuo, especialmente bidirecional. Isso não faz de gRPC um substituto universal de REST, nem prova que ele será mais rápido para qualquer aplicação.

No Dokpod, o navegador usa REST/OpenAPI para operações da interface e SignalR para notificações. O gRPC fica no canal interno entre a API e os agentes: o agente inicia uma conexão de saída, autentica-se com certificado de cliente e mantém um stream para trocar mensagens de controle. Essa escolha responde a uma necessidade concreta do sistema, não a uma regra para todos os produtos.

Este é o primeiro de três artigos da série APIs e Comunicação em .NET. A parte 2 será um guia introdutório de GraphQL; a parte 3 explicará SignalR no .NET, do primeiro Hub à produção. Os dois ainda não foram publicados, por isso não incluo links inexistentes.

ℹ️ Informação: Na data desta consulta, o repositório público do Dokpod não possui releases ou tags publicados. Os links de código neste artigo apontam para a branch main e acompanham seu estado atual; por isso, podem mudar e não representam uma release estável.

RPC, REST e a função do HTTP/2#

Uma chamada RPC começa com uma pergunta conhecida: “qual operação quero executar e qual mensagem preciso enviar?”. Um cliente chama um método como GetInventory; o servidor recebe os dados, executa a operação e devolve uma resposta. A rede está escondida atrás de uma interface semelhante a uma chamada local, mas a operação continua sendo remota: pode demorar, falhar, ser cancelada ou perder a conexão. Essa distinção importa porque uma rede não oferece as mesmas garantias de uma chamada dentro do processo.

Para uma RPC existir, cliente e servidor precisam concordar sobre o nome do método, os tipos de entrada e saída e como os dados serão representados. Esse acordo é o contrato. Em gRPC, costuma ser descrito em arquivos .proto; ferramentas geram código para construir mensagens e invocar o serviço. O código reduz diferenças de digitação e ajuda a detectar incompatibilidades durante o build, mas não elimina a necessidade de versionar e implantar os dois lados com cuidado.

REST parte de outra organização mental. Em vez de perguntar “qual método remoto chamo?”, o cliente interage com recursos identificados por URLs e usa a semântica HTTP, como GET, POST, PUT e DELETE. Uma API REST normalmente expõe representações, comumente JSON, que podem ser inspecionadas e consumidas por clientes diversos. gRPC descreve operações e mensagens com mais rigidez e costuma priorizar clientes que conhecem o contrato e podem usar código gerado. Não é uma divisão absoluta: ambos podem transportar dados estruturados e ambos dependem de decisões cuidadosas de design.

O gRPC tradicional usa HTTP/2. Essa versão do HTTP organiza mensagens em frames e permite multiplexar vários streams sobre uma conexão. Multiplexação significa que diferentes trocas podem compartilhar a conexão sem abrir uma conexão TCP para cada chamada. O HTTP/2 também define controle de fluxo para limitar quanto dado um receptor aceita. Esses recursos dão uma base adequada para chamadas persistentes, mas não tornam automaticamente uma aplicação mais rápida: serialização, tamanho das mensagens, rede, balanceamento, concorrência e trabalho no servidor continuam determinando o resultado.

Um stream gRPC é uma chamada lógica que pode conter uma mensagem ou várias. Não se deve confundi-lo com “uma conexão TCP por método” nem supor que uma conexão pode permanecer aberta para sempre em qualquer infraestrutura. Proxies, firewalls, NATs, limites de duração, certificados e reinicializações podem interromper o fluxo. A aplicação precisa decidir como detectar a queda, abrir outra sessão e recuperar o estado que ficou incerto.

Há também uma consequência prática: um cliente gRPC tradicional no navegador não é igual a um cliente C# ou Go. Navegadores não dão acesso irrestrito às primitivas HTTP/2 que o gRPC convencional espera. Para cenários web existem gRPC-Web e gateways que traduzem protocolos, mas eles adicionam componentes e restrições. No Dokpod, a interface usa HTTP e SignalR, enquanto os agentes usam gRPC; separar os consumidores evita obrigar o browser a falar o protocolo interno.

⚠️ Atenção: HTTP/2 multiplexa streams, mas a conexão continua sujeita a falhas e limites da rede. Trate toda desconexão como parte normal do desenho, não como um evento impossível.

Protocol Buffers, arquivos .proto e código gerado#

Protocol Buffers, ou Protobuf, é um formato de serialização e uma linguagem para descrever mensagens. Serializar é transformar uma estrutura em bytes para transmiti-la ou armazená-la; desserializar é reconstruir a estrutura do outro lado. Um arquivo .proto declara mensagens, campos, números de campo, serviços e métodos. A partir dele, o compilador e os plugins da linguagem geram tipos para o cliente e o servidor.

Um campo tem tipo, nome e um número. O nome ajuda as pessoas que leem o contrato; o número identifica o campo no formato binário. Essa diferença é fundamental: depois de publicado e usado, um número não deve ser reaproveitado para outro significado. Se um campo for removido, reserve seu número e, quando apropriado, seu nome. Acrescentar campos opcionais costuma ser compatível no formato binário, mas isso não garante compatibilidade de comportamento. Um agente antigo pode ignorar um campo novo; já um código de negócio pode depender de uma capability (capacidade anunciada) que o agente não oferece.

Também é importante separar compatibilidade de wire format (formato transmitido) de compatibilidade de aplicação. Uma mudança pode continuar sendo decodificada e ainda quebrar um switch, uma validação ou uma regra de negócio. A implantação pode envolver várias versões de API e agentes ativas ao mesmo tempo. Por isso, mudanças de contrato precisam de uma estratégia de rollout, testes com versões consecutivas e observabilidade suficiente para identificar quem ainda usa o formato antigo.

No Dokpod, AgentControl declara dois métodos de streaming. Connect recebe mensagens do agente e devolve mensagens do plano de controle. Exec usa outro stream, separado, para quadros de terminal. Mensagens de envelope carregam metadados como versão do protocolo, identificador da sessão, fencing token e sequência. Um oneof agrupa tipos de payload alternativos, como AgentHello, Heartbeat, uma página de inventário ou um comando. Isso dá ao leitor uma visão explícita das mensagens possíveis sem transformar o canal em um proxy genérico para a API do engine.

O contrato também registra campos reservados e comentários de evolução. Por exemplo, mensagens de inventário têm revisões e páginas, e capabilities novas são aceitas somente quando o servidor as reconhece. Essa disciplina é parte do custo de um protocolo próprio: a equipe passa a ser responsável por documentar, testar e evoluir os contratos usados pelo produto.

Os quatro modos de chamada gRPC#

O tipo de uma RPC é definido pelo número de mensagens que cada lado pode enviar durante a chamada. O método unary recebe uma mensagem e devolve uma mensagem. É o modelo mais simples: buscar detalhes de uma configuração, validar um token ou consultar uma informação pontual. Ele se parece com uma chamada HTTP de request/response, embora use método e mensagens tipados no contrato.

No streaming do servidor, o cliente envia uma requisição e o servidor responde com zero ou mais mensagens ao longo do tempo. Uma exportação paginada, uma sequência de progresso ou atualizações contínuas que não precisam de resposta do cliente são exemplos possíveis. O cliente pode cancelar o stream, e o servidor deve propagar o token de cancelamento para o trabalho assíncrono. Se o servidor tentar enviar uma resposta infinita sem limites, o custo de recursos também pode ser contínuo.

No streaming do cliente, o cliente envia várias mensagens e recebe uma resposta final. Isso pode fazer sentido para enviar lotes, fragmentos de uma tarefa ou amostras que o servidor consolida. O servidor lê até o cliente encerrar o stream e então responde. Esse padrão não é apenas “muitas chamadas unary”: mantém uma chamada aberta e uma sequência ordenada de mensagens.

No streaming bidirecional, ou duplex, os dois lados podem enviar várias mensagens durante a mesma chamada. Eles não precisam alternar uma mensagem de cada lado: cliente e servidor podem ler e escrever independentemente. É útil quando há troca contínua de comandos, respostas, estados e confirmações. O protocolo da aplicação ainda deve definir correlação, ordenação, controle de fluxo, prioridade e o que fazer quando a chamada termina.

ModoMensagens do clienteMensagens do servidorExemplo de uso
UnaryUmaUmaConsulta pontual
Streaming do servidorUmaMuitasProgresso ou feed de leitura
Streaming do clienteMuitasUmaEnvio de lotes para consolidação
BidirecionalMuitasMuitasCanal interativo de controle

O Connect do Dokpod é bidirecional porque os agentes e a API precisam enviar mensagens enquanto a sessão está ativa. Isso não significa que todo agente precise transmitir dados a cada instante. Heartbeats, inventário e comandos são mensagens de aplicação carregadas no stream; a aplicação ainda define quando e por que cada uma deve circular.

Por que o Dokpod escolheu gRPC para os agentes#

O contexto do Dokpod é diferente de uma API pública voltada a qualquer navegador. O produto precisa alcançar engines de containers instalados em hosts que podem estar em redes privadas, atrás de NAT ou protegidos por regras que não permitem conexões iniciadas de fora. Fazer cada API central iniciar uma conexão até o host exigiria endereços acessíveis e regras de rede por ambiente.

Com uma conexão de saída, o agente inicia a sessão até a API. Isso costuma se encaixar melhor em redes restritas porque o host precisa permitir uma saída controlada, não publicar o socket Docker, um endpoint Podman ou SSH para a rede. O padrão não elimina a configuração de rede nem todas as ameaças, mas reduz a necessidade de abrir a interface administrativa do engine para conexões remotas.

O segundo motivo é o contrato de domínio. Um proxy genérico que encaminhasse qualquer chamada para o Docker Engine ampliaria o impacto de uma falha de autorização: a API poderia se tornar uma ponte para operações arbitrárias do host. O Dokpod envia comandos próprios e versionados, como iniciar ou parar um container, e o agente valida o que recebeu antes de usar o adapter do engine. Isso limita a superfície em comparação com um proxy irrestrito. Ainda assim, o agente é um componente privilegiado; o protocolo precisa autorizar cada operação, validar entradas e evitar exposição de dados sensíveis.

O streaming bidirecional permite que o servidor envie comandos e que o agente informe aceitação, resultado, heartbeat e diferenças de inventário no mesmo canal de controle. A escolha também permite que o servidor negocie sessão, versão e capabilities. Isso é valioso para um produto que controla as duas pontas, mas traz o custo de manter uma especificação e garantir compatibilidade entre elas.

Há limites importantes. O plano atual começa com uma instância da API. Escalar horizontalmente requer decidir qual instância é dona de cada stream, como rotear comandos e como recuperar ownership após falhas. O Dokpod adia essa escala distribuída para uma decisão futura. Os testes executados confirmaram o funcionamento do adapter com Podman Linux; isso não substitui a validação de cada combinação de versões e ambiente. O agente Windows ainda depende de gates de qualificação. A escolha do protocolo não certifica automaticamente todos os engines ou sistemas operacionais.

Como o fluxo gRPC do Dokpod funciona#

O agente abre AgentControl.Connect e envia AgentHello como primeira mensagem. O hello informa versão do agente, engine e versão, sistema operacional, arquitetura, hostname e capabilities. O servidor obtém o certificado de cliente da conexão TLS e entrega certificado e hello ao negociador de sessão. Se a identidade ou a negociação forem rejeitadas, a chamada termina com um erro gRPC; não existe uma sessão válida apenas porque o TCP conectou.

Quando aceita a sessão, o plano de controle devolve SessionEstablished: versão de protocolo selecionada, identificador da sessão, fencing token, intervalo de heartbeat, limite de mensagem e capabilities aceitas. O agente valida esses metadados antes de iniciar o trabalho da sessão. O intervalo anunciado é de 15 segundos e o serviço usa timeout de heartbeat de 90 segundos. Esses números descrevem o código consultado, não uma recomendação universal para qualquer rede.

Depois da negociação, o agente pode publicar snapshots paginados e deltas do inventário. O snapshot reconstrói o estado completo em partes; o delta representa mudanças a partir de uma revisão-base. Heartbeats incluem a última sequência de comando aplicada e revisões de inventário. A API também pode enviar requisições de reconciliação, consultas e comandos de domínio. Separar “aceito” de “resultado concluído” permite representar que receber a ordem não significa que o engine já terminou a operação.

O trecho abaixo resume a forma do contrato, sem tentar reproduzir todas as mensagens:

// Contrato resumido: os tipos reais são definidos no mesmo arquivo .proto.
service AgentControl {
  rpc Connect(stream AgentMessage) returns (stream ControlPlaneMessage);
  rpc Exec(stream ExecFrame) returns (stream ExecFrame);
}

📂 Código Fonte: O contrato completo está na branch main do Dokpod: agent.proto.

No cliente, o agente escreve o hello, aguarda a confirmação inicial e só então considera a sessão estabelecida. O código gerado fornece os tipos e o proxy do serviço; a lógica abaixo mostra o ponto de validação, não um cliente completo:

// Abre Connect e apresenta o hello antes de enviar outros eventos.
using var call = connect(stoppingToken);
await call.RequestStream.WriteAsync(hello, stoppingToken);

if (!await call.ResponseStream.MoveNext(stoppingToken) ||
    call.ResponseStream.Current.PayloadCase !=
        ControlPlaneMessage.PayloadOneofCase.SessionEstablished)
{
    throw new RpcException(new Status(
        StatusCode.FailedPrecondition, "agent_session_not_established"));
}

📂 Código Fonte: O fluxo de sessão e reconexão está em AgentControlWorker.cs; o processamento no servidor está em AgentControlService.cs.

O terminal usa AgentControl.Exec, um stream separado que começa com handshake vinculado à identidade do certificado, sessão ativa, fencing token e sessão de terminal pendente. Ele transporta frames de entrada, saída, redimensionamento e status. Um canal separado evita misturar o fluxo interativo com heartbeats e inventário, mas exige limites, expiração e encerramento coordenado.


📖 Artigo completo com exemplos de código: gRPC sem mistério: o que é e quando usar no .NET 10

Carregando publicação patrocinada...
1