Para implementar WebSockets em Zig, trate o canal como dois contratos distintos: um handshake HTTP Upgrade que valida headers e devolve 101 Switching Protocols, e um loop de frames que lê opcode, tamanho, máscara, payload e control frames (ping, pong, close). Zig não precisa de um framework monolítico para isso. O que precisa ficar explícito é validação, limites de memória, backpressure, autenticação no upgrade e encerramento limpo.
Este guia fecha a lacuna entre o artigo de Server-Sent Events em Zig — ideal para push unidirecional — e a operação real descrita em Zig por trás de Nginx. Se a sua base HTTP ainda está nascendo, combine com servidor HTTP em produção, sockets TCP e UDP e rate limiting com token bucket.
Como a API de std.http e o runtime de I/O ainda evoluem antes do Zig 1.0, confirme zig version e a documentação do release instalado. Os nomes de request, writer e peer address podem mudar; as regras do protocolo RFC 6455 e os cuidados de produção permanecem estáveis.
Resposta rápida
| Necessidade | Recomendação |
|---|---|
| Push servidor → cliente sem upload frequente | prefira SSE |
| Chat, colaboração, terminal, jogo | WebSocket |
| Handshake | GET + Upgrade: websocket + Sec-WebSocket-Key → 101 |
Aceite (Sec-WebSocket-Accept) | SHA-1(key + GUID) em Base64 |
| Frames do cliente | payload mascarado; servidor não mascara |
| Heartbeat | ping/pong com prazo curto |
| Payload máximo | limite duro por mensagem e por conexão |
| Autenticação | no HTTP upgrade, antes do 101 |
| Proxy | Nginx com Upgrade/Connection e timeout alto |
| Observabilidade | separe handshake, mensagens/s, closes e timeouts |
O que WebSocket adiciona ao HTTP
HTTP clássico é request/response. SSE mantém uma resposta longa em que o servidor escreve eventos. WebSocket começa em HTTP e depois troca o significado da conexão: os dois lados passam a enviar frames a qualquer momento sobre o mesmo socket TCP (ou TLS).
O fluxo resumido:
- o cliente faz um
GETcom headers de upgrade; - o servidor valida e responde
101; - a partir daí, ambos falam o framing binário do WebSocket;
- qualquer lado pode enviar dados, ping ou close.
Essa flexibilidade tem custo. Você passa a gerenciar estado de conexão, fragmentação, controle de fluxo, autenticação contínua e limpeza de sockets ociosos. Em Zig, esse custo aparece cedo porque grande parte da infraestrutura fica no seu código, não escondida em um framework.
Quando WebSocket é a escolha certa
Escolha WebSocket quando o produto realmente precisa de canal bidirecional de baixa latência:
- chat e presença online;
- edição colaborativa com cursores e locks curtos;
- terminal remoto ou console interativa;
- painéis que também enviam comandos frequentes;
- jogos e simulações com estado contínuo;
- sincronização interativa entre dispositivos.
Prefira SSE quando o cliente só consome progresso, logs ou notificações. Prefira polling curto quando a atualização é rara e manter milhares de sockets abertos não vale a pena. Prefira filas e workers quando a entrega pode ser assíncrona e não precisa de push imediato no navegador.
A pergunta útil não é “WebSocket é moderno?”. É “o cliente precisa falar com frequência, com baixa latência, no mesmo canal?”. Se a resposta for não, SSE ou HTTP comum costumam ser melhores.
Handshake: do HTTP para o canal
O cliente envia algo nesta família:
GET /ws HTTP/1.1
Host: api.exemplo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: https://app.exemplo.com
O servidor só continua se:
- o método for
GET; Upgradecontiverwebsocket(compare de forma case-insensitive);Connectioncontiverupgrade;Sec-WebSocket-Versionfor13;Sec-WebSocket-Keyexistir e tiver tamanho plausível;- a rota, origem e autenticação forem aceitáveis para o produto.
O valor de Sec-WebSocket-Accept é:
Base64( SHA1( Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) )
Para a chave de exemplo acima, o aceite canônico é s3pPLMBiTxaQ9kYGzzhZRbK+xOo=. Use esse vetor em teste unitário do handshake.
Resposta mínima:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Depois do 101, não tente reutilizar a abstração de “responder uma request HTTP” como se ainda houvesse body clássico. Você está em outro protocolo sobre o mesmo socket.
Esboço de validação em Zig
A API exata depende da sua camada HTTP, mas a lógica fica parecida com isto:
const std = @import("std");
const websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
fn headerContainsToken(value: []const u8, token: []const u8) bool {
var it = std.mem.splitScalar(u8, value, ',');
while (it.next()) |part| {
const trimmed = std.mem.trim(u8, part, " \t");
if (std.ascii.eqlIgnoreCase(trimmed, token)) return true;
}
return false;
}
fn computeAccept(allocator: std.mem.Allocator, key: []const u8) ![]u8 {
var digest: [20]u8 = undefined;
var hasher = std.crypto.hash.Sha1.init(.{});
hasher.update(key);
hasher.update(websocket_guid);
hasher.final(&digest);
return std.base64.standard.Encoder.allocEncode(allocator, &digest);
}
fn handshakeLooksValid(
method: []const u8,
upgrade: ?[]const u8,
connection: ?[]const u8,
version: ?[]const u8,
key: ?[]const u8,
) bool {
if (!std.mem.eql(u8, method, "GET")) return false;
const up = upgrade orelse return false;
const conn = connection orelse return false;
const ver = version orelse return false;
const k = key orelse return false;
if (!headerContainsToken(up, "websocket")) return false;
if (!headerContainsToken(conn, "upgrade")) return false;
if (!std.mem.eql(u8, ver, "13")) return false;
if (k.len < 16 or k.len > 64) return false;
return true;
}
Rejeite cedo com 400 ou 426 quando o pedido for incompleto. Não abra half-open connections “para ver no que dá”.
Frames: o contrato depois do 101
Todo frame WebSocket tem:
- byte de flags + opcode;
- byte de máscara + tamanho (7 bits, ou 16/64 bits estendidos);
- máscara de 4 bytes se o bit de máscara estiver ligado;
- payload.
Opcodes essenciais:
| Opcode | Significado |
|---|---|
0x1 | texto UTF-8 |
0x2 | binário |
0x8 | close |
0x9 | ping |
0xA | pong |
0x0 | continuação de mensagem fragmentada |
Regras práticas que evitam incidentes:
- frames do cliente para o servidor chegam mascarados; desemascarar é obrigatório;
- frames do servidor para o cliente não usam máscara;
- controle (
ping/pong/close) não deve ser fragmentado; - texto precisa ser UTF-8 válido antes de entrar na regra de negócio;
- imponha
max_frame_sizeemax_message_sizeseparados.
Leitura mínima de um frame
const Frame = struct {
fin: bool,
opcode: u4,
payload: []u8,
};
fn readFrame(reader: anytype, allocator: std.mem.Allocator, max_payload: usize) !Frame {
const b0: u8 = try reader.readByte();
const b1: u8 = try reader.readByte();
const fin = (b0 & 0x80) != 0;
const opcode: u4 = @truncate(b0 & 0x0f);
const masked = (b1 & 0x80) != 0;
var payload_len: u64 = b1 & 0x7f;
if (payload_len == 126) {
payload_len = try reader.readInt(u16, .big);
} else if (payload_len == 127) {
payload_len = try reader.readInt(u64, .big);
}
if (payload_len > max_payload) return error.PayloadTooLarge;
var mask: [4]u8 = .{ 0, 0, 0, 0 };
if (masked) {
_ = try reader.readAll(&mask);
}
const payload = try allocator.alloc(u8, @intCast(payload_len));
errdefer allocator.free(payload);
_ = try reader.readAll(payload);
if (masked) {
for (payload, 0..) |*byte, i| {
byte.* ^= mask[i % 4];
}
}
return .{ .fin = fin, .opcode = opcode, .payload = payload };
}
Esse esboço é didático. Em produção, trate short reads, cancelamento, fragmentação, RSV bits ilegais e close com código/razão. Também decida se mensagens grandes serão rejeitadas, spooladas em disco ou processadas em streaming.
Ping, pong e close
Conexões WebSocket ociosas são baratas na demo e caras em produção. Defina:
- intervalo de
pingdo servidor (por exemplo, 20–30 s); - prazo máximo sem
pong(por exemplo, 10–15 s); - código e motivo de
closelegíveis para telemetria; - limpeza imediata de mapas de sessão após close local ou remoto.
fn writeServerPing(writer: anytype, payload: []const u8) !void {
// fin=1, opcode=0x9, sem máscara
const len: u8 = @intCast(payload.len);
try writer.writeAll(&.{ 0x89, len });
if (payload.len > 0) try writer.writeAll(payload);
}
fn writeServerClose(writer: anytype, code: u16) !void {
var body: [2]u8 = undefined;
std.mem.writeInt(u16, &body, code, .big);
try writer.writeAll(&.{ 0x88, 0x02 });
try writer.writeAll(&body);
}
Códigos comuns: 1000 (normal), 1001 (going away), 1002 (protocol error), 1009 (message too big), 1011 (internal error). Registre o código no log estruturado junto com a duração da sessão.
Modelo de servidor em Zig
Um desenho simples e operável:
- accept/handshake em um loop HTTP;
- após
101, mova o socket para um connection registry; - cada conexão tem leitor, escritor, last_activity e user_id;
- mensagens de aplicação passam por uma fila interna com limite;
- broadcasts usam fan-out com drop policy explícita.
Evite um único mutex global em torno de todo I/O. Prefira:
- shard por conexão ou por sala;
- canais com capacidade finita;
- política clara para cliente lento: drop da mensagem, desconectar, ou mudar para modo degradado.
Backpressure é o detalhe que separa demo de serviço. Se um cliente não consome, alguém paga: memória do processo, latência das outras conexões ou perda silenciosa de eventos. Escolha e meça.
Autenticação e autorização
Autentique no handshake, não “depois de umas mensagens”:
- cookie de sessão HttpOnly, no mesmo site da app;
- token em header quando a borda e o cliente permitem;
- ticket de curta duração emitido por uma rota HTTP anterior.
Evite colocar access tokens longevos em query string de ws:// se os logs de proxy forem guardar a URL completa. Se precisar de ticket na query, faça-o descartável, amarrado a IP/user-agent quando fizer sentido, e com TTL curto.
Depois do upgrade:
- autorize cada ação por mensagem (
subscribe,send,join-room); - revogue sessões server-side mesmo com o socket ainda aberto;
- revalide permissões em mudanças de papel ou banimento.
Combine com cookies e sessões, JWT em APIs e a política de IP de X-Forwarded-For confiável quando o limitador ou a auditoria dependerem do cliente real.
Segurança que não pode ficar implícita
Checklist mínimo antes de expor /ws:
| Risco | Mitigação |
|---|---|
| Origin aberto demais | valide Origin para browsers; não confie só nisso para clientes nativos |
| Flood de conexões | limite por IP e por conta; combine com token bucket |
| Mensagens gigantes | max_payload duro e close 1009 |
| Slow consumer | fila limitada + política de drop/desconexão |
| Dados sensíveis em logs | nunca logue payload completo por padrão |
| XSS virando WS abuse | cookies HttpOnly/Secure/SameSite e CSRF strategy da app |
| Proxy mal configurado | teste Upgrade ponta a ponta, não só local |
WebSocket não substitui TLS. Em produção use wss:// terminado no Nginx/CDN ou no próprio serviço, com a mesma disciplina de certificados do restante do HTTP.
Nginx, timeouts e borda
O artigo de proxy reverso com Nginx já cobre o bloco essencial. Na prática, um location dedicado evita que timeouts curtos de API REST matem o canal:
location /ws {
proxy_pass http://zig_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Sem Upgrade/Connection, o handshake morre. Sem timeout alto, o proxy encerra salas quietas. Sem heartbeat no serviço Zig, você acumula sockets zumbis até esgotar file descriptors.
SSE vs WebSocket vs polling
| Critério | SSE | WebSocket | Polling |
|---|---|---|---|
| Direção principal | servidor → cliente | bidirecional | cliente → servidor |
| Protocolo | HTTP longo | HTTP upgrade + frames | HTTP curto |
| Reconexão no browser | nativa (EventSource) | manual ou lib | trivial |
| Binário | ruim/não natural | nativo | possível |
| Complexidade em Zig | menor | média | menor |
| Melhor para | progresso, logs, feed | chat, collab, terminal | atualização rara |
Se o seu painel só mostra status, comece por SSE. Se depois o usuário precisa enviar comandos contínuos no mesmo canal, migre esse fluxo para WebSocket sem forçar o restante da aplicação a mudar.
Observabilidade e testes
Meça pelo menos:
websocket_handshake_total{result=ok|reject};websocket_active_connections;websocket_messages_total{opcode,direction};websocket_close_total{code};- latência de ping/pong;
- tamanho médio e máximo de payload;
- conexões desconectadas por backpressure.
Testes que valem ouro:
- vetor canônico do
Sec-WebSocket-Accept; - frame texto mascarado pequeno;
- payload acima do limite;
- ping sem pong até timeout;
- close local e remoto;
- cliente lento com fila cheia;
- upgrade atrás do proxy real de staging.
Para o restante da telemetria HTTP, encaixe no guia de observabilidade com logs e Prometheus.
Biblioteca própria ou dependência?
Implementar o núcleo do protocolo em Zig é viável e educativo. Ainda assim, para produto, avalie bibliotecas do ecossistema com histórico de frames, fragmentação e testes. O registry e a comunidade costumam apontar opções como websocket.zig e frameworks HTTP que já embutem upgrade. Qualquer escolha precisa de:
- política clara de memória/allocator;
- suporte a ping/pong e close;
- limites configuráveis;
- compatibilidade com a sua versão de Zig;
- caminho de auditoria para CVEs e releases.
Se a stdlib ou o framework HTTP que você usa já entregar um helper de upgrade estável na sua versão, prefira-o para o handshake e mantenha o loop de aplicação explícito.
Roteiro de implementação em um dia
- suba um endpoint
/wsque só valida headers e responde101em desenvolvimento; - leia e escreva um frame texto eco;
- adicione ping/pong e close com logs;
- autentique no upgrade com sessão existente;
- coloque limites de payload, conexões e fila;
- configure Nginx/
wssem staging; - só então acrescente rooms, broadcast e regras de negócio.
Esse caminho evita o erro clássico: construir um “chat completo” antes de ter um canal que sobrevive a proxy, timeout e cliente lento.
Conclusão
WebSockets em Zig são um contrato de engenharia, não um recurso mágico de framework. O handshake precisa ser chato e correto. Os frames precisam de limites. O servidor precisa de heartbeat, autenticação no upgrade, backpressure e métricas. Com isso no lugar, Zig brilha: o binário fica pequeno, o comportamento fica explícito e o canal em tempo real deixa de ser uma caixa-preta.
Se o seu próximo passo for só empurrar eventos de progresso, volte ao guia de SSE. Se a operação já exige chat, terminal ou colaboração, implemente o canal WebSocket com os limites acima e coloque a borda Nginx/TLS na frente desde o primeiro deploy sério. O restante da aplicação — health checks, graceful shutdown e workers — continua o mesmo: falhar de forma visível e recuperar sem drama.
Perguntas frequentes
Zig tem WebSocket na biblioteca padrão?
Não como produto completo e congelado. Você implementa handshake/frames, usa biblioteca do ecossistema ou combina os dois. O protocolo é pequeno; o difícil é produção.
Quando escolher WebSocket em vez de SSE?
Quando o cliente também envia mensagens frequentes com baixa latência. Para logs e progresso, SSE costuma ser suficiente e mais simples.
O que o handshake precisa validar?
GET, Upgrade: websocket, Connection: upgrade, versão 13 e Sec-WebSocket-Key válida, além de autenticação/origem da sua aplicação.
Como evitar sockets zumbis?
Ping periódico, timeout de pong, close explícito e limites por IP/usuário, com timeout coerente no proxy.
WebSocket autentica sozinho?
Não. Autentique no upgrade HTTP e continue autorizando por mensagem depois do 101.
Dá para usar Nginx na frente?
Sim, com HTTP/1.1, headers de Upgrade/Connection e timeouts longos na rota do canal.