Para criar Unix domain sockets em Zig, construa um std.net.Address com Address.initUnix, abra o listener nesse endereço e trate cada conexão como um fluxo local. A principal diferença em relação ao TCP não está no read e no write: está no ciclo de vida do caminho do socket, nas permissões do diretório, na portabilidade e no protocolo de mensagens que você precisa definir sobre o fluxo.
Unix domain socket (UDS) é uma ótima escolha quando dois processos rodam no mesmo host: uma API Zig atrás de Nginx, um daemon com uma CLI administrativa, um agente local, um worker conversando com seu supervisor ou um serviço que não deve expor porta na rede. Ele não substitui TCP entre máquinas e não elimina a necessidade de autenticação quando usuários locais não confiáveis podem acessar o endpoint.
Este guia complementa a referência de std.net.Address, o tutorial de sockets TCP e UDP em Zig e a operação de um serviço Zig por trás de Nginx. Como as APIs da stdlib ainda podem mudar antes do Zig 1.0, confirme os nomes na documentação da versão fixada pelo projeto; o desenho operacional permanece o mesmo.
Resposta rápida
| Necessidade | Recomendação |
|---|---|
| IPC entre processos no mesmo Linux/macOS | Unix domain socket |
| Comunicação entre hosts | TCP |
| Endpoint | caminho como /run/minha-api/api.sock |
| API local atrás de Nginx | UDS com protocolo HTTP |
| Controle de acesso | diretório privado, owner/group e modo do socket |
| Reinício após crash | remover socket antigo com validação antes do bind |
Mensagens em SOCK_STREAM | framing explícito; um read não equivale a uma mensagem |
| Payload | limite duro antes da alocação |
| Shutdown | parar accept, drenar conexões e remover o caminho |
| Multiplataforma | abstrair UDS e oferecer TCP loopback ou named pipe |
O que é um Unix domain socket
Um Unix domain socket é um endpoint de comunicação gerenciado pelo kernel para processos locais. Na forma mais comum, ele aparece no sistema de arquivos:
/run/minha-api/api.sock
O arquivo não contém as requisições. Ele funciona como nome e ponto de acesso para o socket. Depois que cliente e servidor se conectam, os bytes passam pelo kernel sem percorrer a pilha IP como uma conexão TCP convencional.
Existem três famílias relevantes:
SOCK_STREAM: fluxo confiável e ordenado, parecido com TCP;SOCK_DGRAM: datagramas locais, com fronteiras de mensagem;SOCK_SEQPACKET: mensagens ordenadas e preservadas, mas com suporte menos uniforme.
As abstrações de alto nível de std.net normalmente levam ao caso de stream. Ele é suficiente para HTTP local, RPC simples e protocolos com prefixo de tamanho. Quando precisar de opções POSIX específicas, credenciais do peer ou tipos diferentes, desça para std.posix atrás de uma camada pequena e testável.
Quando UDS é melhor que TCP loopback
127.0.0.1:8080 também conecta processos locais. Unix socket costuma ser melhor quando:
- o serviço jamais deve aceitar tráfego da rede;
- Nginx e aplicação rodam no mesmo host;
- permissões de arquivo e grupos Unix são úteis para autorização;
- você quer evitar colisão e gestão de portas;
- o endpoint pertence ao ciclo de vida de um serviço systemd;
- uma CLI administrativa conversa com um daemon local.
TCP loopback continua sendo a opção mais simples quando:
- o mesmo binário precisa funcionar no Windows sem backend separado;
- cliente e servidor podem migrar para hosts diferentes;
- health checks e ferramentas existentes esperam host e porta;
- cada processo roda em container isolado sem volume ou namespace compartilhado;
- a equipe domina melhor observabilidade e troubleshooting TCP.
Não escolha UDS apenas porque “deve ser mais rápido”. Para payloads pequenos, a diferença pode ser irrelevante perto do custo do parsing, banco de dados e lógica de negócio. Escolha pelo contrato operacional e meça a latência no seu ambiente.
Servidor Unix socket em Zig
O fluxo conceitual é semelhante ao servidor TCP:
- preparar o diretório;
- verificar e remover um endpoint antigo;
- criar
Address.initUnix(path); - chamar
listen; - aceitar conexões;
- ler e escrever com limites;
- fechar o listener e remover o caminho no encerramento.
Um echo server didático fica assim:
const std = @import("std");
const socket_path = "/tmp/zig-uds-demo.sock";
fn handleClient(stream: std.net.Stream) void {
defer stream.close();
var buffer: [4096]u8 = undefined;
while (true) {
const n = stream.read(&buffer) catch return;
if (n == 0) return;
stream.writeAll(buffer[0..n]) catch return;
}
}
pub fn main() !void {
// Em produção, valide tipo, owner e diretório antes de remover.
std.fs.deleteFileAbsolute(socket_path) catch |err| switch (err) {
error.FileNotFound => {},
else => return err,
};
defer std.fs.deleteFileAbsolute(socket_path) catch {};
const address = try std.net.Address.initUnix(socket_path);
var server = try address.listen(.{});
defer server.deinit();
std.log.info("uds_listen path={s}", .{socket_path});
while (true) {
const connection = try server.accept();
const thread = std.Thread.spawn(.{}, handleClient, .{connection.stream}) catch |err| {
connection.stream.close();
std.log.err("uds_spawn_failed error={}", .{err});
continue;
};
thread.detach();
}
}
O exemplo usa /tmp para ser fácil de testar, mas essa não é a melhor localização para produção. /tmp é compartilhado, pode sofrer limpeza automática e permite mais interferência entre usuários. Prefira um diretório controlado sob /run, criado pelo systemd, ou um diretório privado da aplicação.
A API de listen, Stream e filesystem pode variar entre releases. Se o seu Zig mudou esses nomes, preserve a sequência: remoção segura, bind, accept, limites e cleanup.
Cliente local
O cliente cria o mesmo endereço e abre o stream:
const std = @import("std");
pub fn main() !void {
const address = try std.net.Address.initUnix("/tmp/zig-uds-demo.sock");
const stream = try std.net.tcpConnectToAddress(address);
defer stream.close();
try stream.writeAll("ping\n");
var buffer: [128]u8 = undefined;
const n = try stream.read(&buffer);
std.debug.print("resposta: {s}", .{buffer[0..n]});
}
O nome tcpConnectToAddress pode parecer estranho para um endereço Unix. Em versões em que essa função aceita a variante .un de Address, ela cria o stream correspondente; em outras versões, pode existir um caminho de API diferente. Confirme a assinatura no release usado e esconda essa diferença numa função do projeto:
fn connectLocal(path: []const u8) !std.net.Stream {
const address = try std.net.Address.initUnix(path);
return std.net.tcpConnectToAddress(address);
}
Isso evita espalhar detalhes de compatibilidade por handlers e regras de negócio.
O erro clássico: um read não é uma mensagem
Um Unix socket de stream entrega bytes, não mensagens. Se o cliente fizer duas escritas:
write("PING")
write("STATUS")
O servidor pode observar:
read -> "PINGSTATUS"
ou:
read -> "PI"
read -> "NGSTAT"
read -> "US"
O mesmo vale para TCP. Para um protocolo confiável, escolha framing.
Prefixo de tamanho
Um formato simples usa quatro bytes em big-endian antes do payload:
[ tamanho u32 ][ payload ]
Leitura conceitual:
fn readMessage(
reader: anytype,
allocator: std.mem.Allocator,
max_size: usize,
) ![]u8 {
const size_u32 = try reader.readInt(u32, .big);
const size: usize = @intCast(size_u32);
if (size > max_size) return error.MessageTooLarge;
const payload = try allocator.alloc(u8, size);
errdefer allocator.free(payload);
try reader.readNoEof(payload);
return payload;
}
Defina max_size antes de alocar. Um peer local comprometido também consegue enviar 0xffffffff e tentar esgotar a memória do daemon.
Delimitador
Para comandos humanos pequenos, uma linha terminada por \n pode bastar:
STATUS\n
RELOAD\n
Mesmo assim, imponha tamanho máximo de linha e decida como tratar UTF-8 inválido. Não use readUntilDelimiterAlloc sem limite generoso, porém finito.
HTTP
Quando Nginx é o cliente, HTTP já define framing, headers, status e semântica de conexão. Nesse caso, o serviço Zig pode servir HTTP sobre UDS sem inventar RPC próprio. O protocolo é HTTP; apenas o transporte local mudou.
Caminho antigo, crash e AddressInUse
Se o processo termina normalmente, ele pode remover o socket. Se recebe SIGKILL, sofre crash ou a máquina reinicia de forma incompleta, o caminho pode permanecer. No próximo bind, o serviço encontra AddressInUse.
A correção ingênua é apagar o caminho incondicionalmente. Isso cria riscos:
- apagar um arquivo que não é socket;
- remover o endpoint de outra instância ainda ativa;
- seguir symlink plantado por outro usuário;
- aceitar um caminho configurável sem validação.
Uma política melhor:
- use diretório privado, não gravável por usuários não confiáveis;
- mantenha o caminho fixo ou validado;
- antes de apagar, inspecione o tipo do arquivo quando a API/plataforma permitir;
- opcionalmente, tente conectar: sucesso indica que outra instância está viva;
- só remova um socket stale conhecido;
- após bind, aplique owner, group e modo esperados;
- no shutdown normal, remova o caminho.
Para uma instância única, o systemd também pode controlar o diretório e reduzir a chance de colisão.
Permissões e segurança local
O endpoint não fica seguro apenas por estar “local”. Em uma máquina multiusuário, qualquer processo com permissão pode tentar conectar. A proteção começa pelo diretório:
/run/minha-api/ root:minha-api 0750
/run/minha-api/api.sock app:minha-api 0660
O processo do Nginx pode pertencer ao grupo minha-api, enquanto usuários comuns não conseguem abrir o socket. Além do modo do arquivo, o cliente precisa de permissão de travessia (x) nos diretórios do caminho.
Cuidados adicionais:
- não coloque tokens ou dados sensíveis no próprio nome do socket;
- valide todas as mensagens como faria numa conexão de rede;
- limite conexões simultâneas, payload e tempo ocioso;
- não confie no conteúdo apenas porque veio de UDS;
- quando necessário, consulte credenciais do peer (
SO_PEERCREDno Linux ou mecanismo equivalente), isolando código por plataforma; - mantenha autorização na camada da aplicação para operações destrutivas.
Credenciais do peer são úteis, mas não portáveis de forma idêntica entre Linux, BSD e macOS. Encapsule a leitura numa interface como PeerIdentity e teste o comportamento em cada target suportado.
Nginx falando com um serviço Zig por UDS
Nginx consegue encaminhar HTTP para um Unix socket. Uma configuração comum é:
upstream zig_backend {
server unix:/run/minha-api/api.sock;
keepalive 32;
}
server {
listen 443 ssl;
server_name api.exemplo.com.br;
location / {
proxy_pass http://zig_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 2s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
}
O erro mais comum é 502 Bad Gateway com Permission denied no log do Nginx. Verifique:
namei -l /run/minha-api/api.sock
ls -l /run/minha-api/api.sock
sudo -u www-data test -r /run/minha-api/api.sock
O usuário pode variar (www-data, nginx ou outro). Não resolva com chmod 777: ajuste grupo, RuntimeDirectoryMode e modo do socket. Preserve também as regras de headers de proxy confiáveis e timeouts coerentes.
Diretório gerenciado pelo systemd
Em vez de criar /run/minha-api manualmente em cada boot, deixe o systemd gerenciar o diretório volátil:
[Unit]
Description=Minha API em Zig
After=network.target
[Service]
Type=simple
User=minha-api
Group=minha-api
RuntimeDirectory=minha-api
RuntimeDirectoryMode=0750
ExecStart=/opt/minha-api/bin/minha-api --socket /run/minha-api/api.sock
Restart=on-failure
RestartSec=2s
TimeoutStopSec=20s
[Install]
WantedBy=multi-user.target
RuntimeDirectory=minha-api cria /run/minha-api com owner do serviço e remove o diretório conforme o ciclo de vida da unit. Se Nginx precisa conectar, coloque-o num grupo autorizado ou use uma estratégia explícita de ownership depois do bind.
O serviço ainda precisa tratar graceful shutdown com SIGTERM: parar accept, drenar conexões em andamento, fechar o listener e remover o endpoint. TimeoutStopSec deve ser maior que o prazo interno de drain.
Socket activation
O systemd também oferece socket activation: ele cria e escuta o socket antes de iniciar o processo, entregando o file descriptor ao serviço. Isso evita a janela em que o endpoint não existe durante o startup e centraliza permissões.
Exemplo de unit .socket:
[Socket]
ListenStream=/run/minha-api/api.sock
SocketUser=minha-api
SocketGroup=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target
A aplicação Zig precisa detectar e adotar o file descriptor recebido, em vez de fazer bind novamente. Essa integração é mais avançada e depende da estratégia do projeto para LISTEN_PID e LISTEN_FDS. Implemente primeiro o listener próprio; adote socket activation quando deploy sem janela e supervisão centralizada forem necessidades reais.
Concorrência, backpressure e limites
Trocar TCP por UDS não resolve saturação. Um servidor que cria uma thread ilimitada por conexão pode cair diante de um loop local tão facilmente quanto diante da rede.
Defina:
- máximo de conexões ativas;
- máximo de requests simultâneas;
- tamanho máximo por frame ou body HTTP;
- timeout para receber header e payload;
- tamanho máximo da fila de espera;
- política de rejeição rápida quando saturado;
- orçamento de memória por conexão.
Uma arquitetura prática usa um conjunto limitado de workers. O accept loop entrega conexões para a fila; se ela estiver cheia, fecha a conexão ou devolve erro de serviço ocupado. Para protocolos request/response, uma conexão persistente não deve monopolizar um worker enquanto está ociosa sem timeout.
Em observabilidade, registre pelo menos:
uds_accept_total
uds_active_connections
uds_rejected_connections_total
uds_request_duration_ms
uds_request_bytes
uds_response_bytes
uds_protocol_errors_total
uds_idle_timeout_total
Não coloque conteúdo sensível das mensagens nos logs. Registre operação, tamanho, duração, resultado e identidade autorizada quando disponível.
Containers e namespaces
Dois containers só compartilham um Unix socket quando compartilham um volume e conseguem enxergar o mesmo caminho. Isso adiciona decisões de UID/GID, montagem e ciclo de vida do arquivo. Em Kubernetes, um emptyDir pode ser montado no sidecar e na aplicação; ainda assim, TCP em localhost dentro do mesmo Pod costuma ser mais simples de operar.
Perguntas antes de escolher UDS em containers:
- os processos estão no mesmo namespace de filesystem?
- o volume preserva o tipo socket?
- os UIDs e grupos batem nas duas imagens?
- quem remove o endpoint no restart?
- readiness consegue distinguir socket existente de serviço pronto?
- a plataforma e o runtime aceitam o padrão usado?
Não transforme uma otimização local em acoplamento difícil de depurar. Se sidecar e aplicação já compartilham a rede do Pod, TCP loopback pode ser a decisão mais previsível.
Testes essenciais
O teste feliz de “cliente manda ping” é insuficiente. Cubra:
- bind limpo — diretório existe e o socket é criado;
- reinício após shutdown normal — caminho é removido;
- socket stale — política remove endpoint antigo conhecido;
- segunda instância viva — não remove socket em uso;
- mensagem fragmentada — header e payload chegam em vários reads;
- duas mensagens num read — parser preserva a segunda;
- payload acima do limite — rejeita antes da alocação grande;
- cliente ocioso — timeout fecha a conexão;
- permissão negada — usuário sem grupo não conecta;
- SIGTERM — listener para e requests em andamento drenam;
- Nginx — request HTTP atravessa o upstream Unix;
- backpressure — fila cheia rejeita de forma previsível.
Um teste de integração pode criar o socket dentro de um diretório temporário exclusivo, iniciar servidor e cliente, validar resposta e garantir cleanup com defer. Evite caminho global fixo durante testes paralelos.
Checklist de produção
Antes do deploy:
- UDS é realmente local e mais simples que TCP loopback;
- caminho fica em diretório privado e previsível;
- política de socket stale não apaga arquivo arbitrário;
- owner, group e modo foram definidos;
- protocolo tem framing e tamanho máximo;
- conexões e workers têm limites;
- reads ociosos têm timeout;
- SIGTERM executa drain e cleanup;
- Nginx consegue atravessar todos os diretórios do caminho;
-
502,Permission deniedeAddressInUsetêm runbook; - métricas distinguem accept, protocolo, timeout e saturação;
- backend alternativo existe se o produto é multiplataforma.
Conclusão
Unix domain sockets em Zig são uma fronteira pequena e eficiente para IPC local. O código de transporte pode lembrar TCP, mas a qualidade de produção depende de decisões fora do loop de read: diretório, permissões, remoção segura, framing, limites, shutdown e integração com o supervisor.
Use UDS quando os processos pertencem ao mesmo host e o endpoint de filesystem melhora isolamento e operação. Use TCP loopback quando portabilidade, containers ou ferramentas da equipe tornam host e porta mais simples. Nos dois casos, mantenha o protocolo explícito, limite tudo que vem do peer e teste reinício e falha — não apenas o primeiro ping.
Como próximo passo, conecte este desenho ao guia de servidor HTTP em produção e ao checklist de health checks de readiness e liveness. Assim, o socket deixa de ser um detalhe isolado e passa a fazer parte de um serviço Zig observável e operável.