---
title: "WebSockets em Zig: Handshake, Frames e Servidor em Tempo Real"
url: "https://ziglang.com.br/artigos/zig-websockets-servidor-tempo-real/"
markdown_url: "https://ziglang.com.br/artigos/zig-websockets-servidor-tempo-real.MD"
description: "Guia prático de WebSockets em Zig: handshake HTTP Upgrade, frames texto e binários, ping/pong, backpressure, autenticação, proxy e produção com SSE e Nginx."
date: "2026-08-25"
author: ""
---

# WebSockets em Zig: Handshake, Frames e Servidor em Tempo Real

Guia prático de WebSockets em Zig: handshake HTTP Upgrade, frames texto e binários, ping/pong, backpressure, autenticação, proxy e produção com SSE e Nginx.


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](/artigos/zig-server-sent-events-sse/) — ideal para push unidirecional — e a operação real descrita em [Zig por trás de Nginx](/artigos/zig-nginx-proxy-reverso-load-balancing/). Se a sua base HTTP ainda está nascendo, combine com [servidor HTTP em produção](/artigos/zig-http-server-producao/), [sockets TCP e UDP](/artigos/zig-networking-sockets-tcp-udp/) e [rate limiting com token bucket](/artigos/zig-rate-limiting-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](/artigos/zig-server-sent-events-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:

1. o cliente faz um `GET` com headers de upgrade;
2. o servidor valida e responde `101`;
3. a partir daí, ambos falam o framing binário do WebSocket;
4. 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:

```http
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`;
- `Upgrade` contiver `websocket` (compare de forma case-insensitive);
- `Connection` contiver `upgrade`;
- `Sec-WebSocket-Version` for `13`;
- `Sec-WebSocket-Key` existir e tiver tamanho plausível;
- a rota, origem e autenticação forem aceitáveis para o produto.

O valor de `Sec-WebSocket-Accept` é:

```text
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
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:

```zig
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:

1. byte de flags + opcode;
2. byte de máscara + tamanho (7 bits, ou 16/64 bits estendidos);
3. máscara de 4 bytes se o bit de máscara estiver ligado;
4. 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_size` e `max_message_size` separados.

### Leitura mínima de um frame

```zig
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 `ping` do servidor (por exemplo, 20–30 s);
- prazo máximo sem `pong` (por exemplo, 10–15 s);
- código e motivo de `close` legíveis para telemetria;
- limpeza imediata de mapas de sessão após close local ou remoto.

```zig
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:

1. **accept/handshake** em um loop HTTP;
2. após `101`, mova o socket para um **connection registry**;
3. cada conexão tem leitor, escritor, last_activity e user_id;
4. mensagens de aplicação passam por uma fila interna com limite;
5. 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](/artigos/zig-cookies-sessao-http/), [JWT em APIs](/artigos/zig-jwt-autenticacao-api/) e a política de IP de [X-Forwarded-For confiável](/artigos/zig-http-x-forwarded-for-proxy-confiavel/) 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](/artigos/zig-rate-limiting-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](/artigos/zig-nginx-proxy-reverso-load-balancing/) já cobre o bloco essencial. Na prática, um `location` dedicado evita que timeouts curtos de API REST matem o canal:

```nginx
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:

1. vetor canônico do `Sec-WebSocket-Accept`;
2. frame texto mascarado pequeno;
3. payload acima do limite;
4. ping sem pong até timeout;
5. close local e remoto;
6. cliente lento com fila cheia;
7. upgrade atrás do proxy real de staging.

Para o restante da telemetria HTTP, encaixe no guia de [observabilidade com logs e Prometheus](/artigos/zig-observabilidade-logs-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

1. suba um endpoint `/ws` que só valida headers e responde `101` em desenvolvimento;
2. leia e escreva um frame texto eco;
3. adicione ping/pong e close com logs;
4. autentique no upgrade com sessão existente;
5. coloque limites de payload, conexões e fila;
6. configure Nginx/`wss` em staging;
7. 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](/artigos/zig-server-sent-events-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](/artigos/zig-health-check-readiness-liveness/), [graceful shutdown](/artigos/zig-graceful-shutdown-sigterm-sigint/) 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.
