---
title: "Zig e WebSockets: Servidor Real-Time com Handshake, Frames e Limites"
url: "https://ziglang.com.br/tutoriais/zig-websockets/"
markdown_url: "https://ziglang.com.br/tutoriais/zig-websockets.MD"
description: "Aprenda a criar um servidor WebSocket em Zig: handshake HTTP Upgrade, frames texto, ping/pong, limites de payload, arquitetura e cuidados de produção."
date: "2026-05-26"
author: ""
---

# Zig e WebSockets: Servidor Real-Time com Handshake, Frames e Limites

Aprenda a criar um servidor WebSocket em Zig: handshake HTTP Upgrade, frames texto, ping/pong, limites de payload, arquitetura e cuidados de produção.


WebSockets em Zig entram quando HTTP tradicional começa a ficar desconfortável: chat interno, painel de métricas, jogo simples, stream de eventos, sincronização entre clientes, logs ao vivo ou uma ferramenta de DevOps que precisa mostrar progresso sem ficar fazendo polling. A ideia é manter uma conexão TCP aberta depois de um `HTTP Upgrade` e trocar frames bidirecionais enquanto cliente e servidor estiverem vivos.

Este tutorial mostra como pensar em **WebSocket server em Zig** sem esconder os detalhes importantes. Vamos passar pelo handshake, pelo formato de frames, por um servidor mínimo, por limites de payload, por ping/pong e por decisões de produção. Se você ainda não montou um servidor HTTP, comece pelo guia de [Zig Server HTTP com `std.http.Server`](/tutoriais/zig-http-server/). Se seu objetivo é consumir um WebSocket externo, veja também a receita de [cliente WebSocket em Zig](/receitas/zig-websocket-client/).

## Quando usar WebSocket em Zig

WebSocket não é substituto universal para REST. Ele vale a pena quando o servidor precisa enviar dados sem esperar uma nova requisição do cliente, ou quando a latência de abrir uma requisição HTTP repetida atrapalha a experiência.

| Caso de uso | WebSocket ajuda? | Observação prática |
|---|---:|---|
| Chat, presença e colaboração | Sim | Conexão persistente simplifica broadcast |
| Dashboard de logs ou métricas | Sim | Envie apenas deltas, não snapshots gigantes |
| API CRUD comum | Não | REST ou HTTP JSON continua mais simples |
| Notificação eventual | Talvez | Server-Sent Events pode bastar |
| Jogo ou simulação leve | Sim | Controle limites, frequência e backpressure |

Zig combina bem com esse tipo de servidor porque deixa custos explícitos: cada conexão tem buffer, cada mensagem tem limite, cada alocação precisa de dono. Isso é ótimo para sistemas de tempo real, mas cobra disciplina. Um WebSocket sem limite de payload, sem timeout e sem política de fechamento vira uma forma eficiente de derrubar seu processo.

## O protocolo em duas fases

Um WebSocket começa como HTTP. O cliente envia um `GET` com headers especiais:

```http
GET /ws HTTP/1.1
Host: exemplo.local
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
```

O servidor valida os headers e responde com `101 Switching Protocols`. O valor de `Sec-WebSocket-Accept` é calculado concatenando a chave recebida com o GUID fixo do protocolo, fazendo SHA-1 e codificando em Base64.

Depois disso, a conexão deixa de ser HTTP comum e passa a trocar frames WebSocket. Cada frame tem opcode, flag de finalização, tamanho e payload. Clientes de navegador sempre enviam frames mascarados; servidores normalmente enviam frames sem máscara. Esse detalhe é obrigatório pelo protocolo e não deve ser ignorado.

## Handshake em Zig

O trecho abaixo foca na parte que costuma gerar dúvida: calcular `Sec-WebSocket-Accept`. Em um servidor real, você também validaria método, path, versão, `Upgrade`, `Connection` e limite de tamanho dos headers.

```zig
const std = @import("std");

const websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

fn websocketAcceptKey(key: []const u8, out: *[28]u8) ![]const u8 {
    var sha1 = std.crypto.hash.Sha1.init(.{});
    sha1.update(key);
    sha1.update(websocket_guid);

    var digest: [20]u8 = undefined;
    sha1.final(&digest);

    return std.base64.standard.Encoder.encode(out, &digest);
}
```

Com a chave calculada, a resposta fica direta:

```zig
fn writeUpgradeResponse(writer: anytype, accept_key: []const u8) !void {
    try writer.print(
        "HTTP/1.1 101 Switching Protocols\r\n" ++
        "Upgrade: websocket\r\n" ++
        "Connection: Upgrade\r\n" ++
        "Sec-WebSocket-Accept: {s}\r\n" ++
        "\r\n",
        .{accept_key},
    );
}
```

Não use parsing ingênuo em produção para headers HTTP se você aceita tráfego público. O exemplo serve para entender o mecanismo. Em um serviço sério, reaproveite a camada de servidor HTTP, limite headers, normalize nomes e rejeite upgrades incompletos com status claro.

## Lendo frames com limite de payload

O frame WebSocket começa com dois bytes. O primeiro indica `FIN` e opcode. O segundo indica se há máscara e o tamanho inicial. Tamanhos `126` e `127` significam que o tamanho real vem nos próximos bytes.

Para tutorial, vamos limitar mensagens a 64 KiB e tratar apenas frames texto, ping, pong e close. Isso já cobre chat, logs pequenos e comandos de controle.

```zig
const Opcode = enum(u4) {
    continuation = 0x0,
    text = 0x1,
    binary = 0x2,
    close = 0x8,
    ping = 0x9,
    pong = 0xA,
};

const Frame = struct {
    opcode: Opcode,
    payload: []u8,
};

fn readFrame(allocator: std.mem.Allocator, reader: anytype) !Frame {
    var header: [2]u8 = undefined;
    try reader.readNoEof(&header);

    const opcode: Opcode = @enumFromInt(header[0] & 0x0F);
    const masked = (header[1] & 0x80) != 0;
    var len: u64 = header[1] & 0x7F;

    if (len == 126) {
        var ext: [2]u8 = undefined;
        try reader.readNoEof(&ext);
        len = std.mem.readInt(u16, &ext, .big);
    } else if (len == 127) {
        var ext: [8]u8 = undefined;
        try reader.readNoEof(&ext);
        len = std.mem.readInt(u64, &ext, .big);
    }

    if (len > 64 * 1024) return error.PayloadTooLarge;

    var mask: [4]u8 = .{ 0, 0, 0, 0 };
    if (masked) try reader.readNoEof(&mask);

    const payload = try allocator.alloc(u8, @intCast(len));
    errdefer allocator.free(payload);
    try reader.readNoEof(payload);

    if (masked) {
        for (payload, 0..) |*byte, i| {
            byte.* ^= mask[i % 4];
        }
    }

    return .{ .opcode = opcode, .payload = payload };
}
```

O ponto mais importante não é decorar bits. É notar as defesas: limite antes de alocar, `errdefer` para não vazar, leitura exata e tratamento explícito de máscara. Em WebSocket público, você também deve validar UTF-8 em mensagens texto, recusar frames fragmentados se não implementou continuação e fechar a conexão com código apropriado quando algo violar o protocolo.

## Escrevendo frames do servidor

Servidores não precisam mascarar frames. Um envio simples de texto pode ser assim:

```zig
fn writeTextFrame(writer: anytype, payload: []const u8) !void {
    if (payload.len > 64 * 1024) return error.PayloadTooLarge;

    try writer.writeByte(0x80 | @intFromEnum(Opcode.text));

    if (payload.len < 126) {
        try writer.writeByte(@intCast(payload.len));
    } else {
        try writer.writeByte(126);
        var len_buf: [2]u8 = undefined;
        std.mem.writeInt(u16, &len_buf, @intCast(payload.len), .big);
        try writer.writeAll(&len_buf);
    }

    try writer.writeAll(payload);
}
```

Para `ping`, `pong` e `close`, a estrutura é parecida, mudando opcode e payload. Responda `ping` com `pong` rapidamente. Se o cliente mandar `close`, envie `close` de volta e encerre o stream. Não mantenha conexão zumbi.

## Loop de conexão

Um loop mínimo por conexão pode usar uma arena por mensagem. Isso evita acumular buffers quando o servidor fica horas aberto:

```zig
fn handleWebSocket(allocator: std.mem.Allocator, stream: std.net.Stream) !void {
    defer stream.close();

    const reader = stream.reader();
    const writer = stream.writer();

    while (true) {
        var arena = std.heap.ArenaAllocator.init(allocator);
        defer arena.deinit();

        const frame = readFrame(arena.allocator(), reader) catch |err| {
            std.log.warn("websocket read failed: {s}", .{@errorName(err)});
            break;
        };

        switch (frame.opcode) {
            .text => try writeTextFrame(writer, frame.payload),
            .ping => try writePongFrame(writer, frame.payload),
            .pong => {},
            .close => break,
            else => return error.UnsupportedFrame,
        }
    }
}
```

Esse exemplo é um eco controlado. Em um chat real, você teria um registro de conexões, uma fila de saída por cliente e uma política para desconectar consumidores lentos. Sem isso, um cliente que não lê pode travar broadcast para todos.

## Arquitetura para produção

Antes de publicar um WebSocket em Zig, defina estes limites:

1. Tamanho máximo de header e payload.
2. Número máximo de conexões simultâneas.
3. Timeout de handshake e leitura.
4. Frequência máxima de mensagens por cliente.
5. Tamanho da fila de saída por conexão.
6. Política para ping/pong e conexões ociosas.
7. Logs estruturados sem payload sensível.
8. Métricas de conexões abertas, mensagens, bytes, erros e desconexões.

Esses pontos conectam diretamente com o guia de [HTTP server em produção](/artigos/zig-http-server-producao/) e com [observabilidade em Zig](/artigos/zig-observabilidade/). Um WebSocket parece simples em desenvolvimento local, mas em produção ele é um recurso reservado por cliente. Cada aba aberta mantém file descriptor, memória e estado.

Se você está comparando stacks para serviços real-time, Go ainda tem bibliotecas e exemplos mais maduros. O cluster de programação do portfólio tem um bom ponto de partida em <a href="https://golang.com.br/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'golang.com.br' })">Go para backend e serviços concorrentes</a>. A escolha pragmática não é transformar Zig em framework web pesado; é usar Zig quando controle de binário, latência previsível e integração com sistemas importam mais que velocidade de prototipagem.

## Próximos passos

Para evoluir este tutorial, siga esta ordem:

1. Revise [sockets TCP e UDP em Zig](/artigos/zig-networking-sockets-tcp-udp/) para entender a base da conexão.
2. Leia [Zig Server HTTP com `std.http.Server`](/tutoriais/zig-http-server/) para tratar o `HTTP Upgrade` dentro da pilha web.
3. Use a receita de [cliente WebSocket em Zig](/receitas/zig-websocket-client/) para testar interoperabilidade.
4. Combine com [io_uring em Zig](/tutoriais/zig-async-iouring/) se o serviço precisa de muitas conexões no Linux.
5. Feche com [observabilidade em Zig](/artigos/zig-observabilidade/) antes de colocar tráfego real.

WebSocket em Zig é viável, mas o valor está menos no exemplo curto e mais na clareza operacional. Limite entrada antes de alocar, trate fechamento como parte do protocolo, monitore conexões abertas e evite prometer broadcast infinito. Com esses cuidados, Zig vira uma boa opção para servidores real-time pequenos, agentes locais, dashboards técnicos e ferramentas internas que precisam ser rápidas, previsíveis e fáceis de distribuir.
