Networking em Zig: Sockets TCP e UDP na Prática

Programação de rede é uma das áreas onde Zig realmente brilha. Com controle fino sobre memória, zero overhead de runtime e acesso direto às APIs do sistema operacional, Zig oferece tudo o que você precisa para construir aplicações de rede de alta performance — sem a complexidade de C e sem o custo de abstrações pesadas.

Neste artigo, vamos além do servidor HTTP e exploramos o mundo dos sockets TCP e UDP diretamente com a biblioteca padrão do Zig. Você vai aprender a construir desde um simples echo server até um sistema de mensagens com protocolo customizado.

Por que Zig para Networking?

Antes de mergulhar no código, vale entender por que Zig é uma escolha excelente para programação de rede:

  • Controle de memória: você decide exatamente como e quando alocar buffers de rede
  • Zero-cost abstractions: as APIs de rede do Zig compilam diretamente para syscalls do OS
  • Cross-compilation nativa: compile seu servidor para Linux, macOS e Windows com um único comando
  • Error handling explícito: todo erro de rede é tratado — nada de exceções escondidas

Comparado com Go, que abstrai sockets atrás de goroutines e um runtime complexo, ou Rust, que exige lifetimes para buffers de rede, Zig oferece uma abordagem direta e sem surpresas. Se você vem de Go, vai notar que Zig te dá mais controle. Se vem de Rust, vai achar a ergonomia mais simples.

Fundamentos: A API std.net

A biblioteca padrão de Zig expõe networking através do módulo std.net. Os tipos principais que vamos usar são:

const std = @import("std");
const net = std.net;

// Tipos fundamentais
const Address = net.Address;       // Endereço IP + porta
const Stream = net.Stream;         // Conexão TCP (read/write)
const Server = net.Server;         // Socket listener TCP

Diferente de C, onde você lida com sockaddr_in, bind(), listen() e accept() como funções separadas com tratamento de erro manual, Zig encapsula isso em uma API coesa que ainda expõe todo o controle necessário.

Servidor TCP: Echo Server

Vamos começar com o clássico echo server — um servidor que recebe dados e devolve exatamente o que recebeu:

const std = @import("std");
const net = std.net;
const posix = std.posix;

pub fn main() !void {
    // Configura o endereço: escuta em todas as interfaces, porta 8080
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);

    // Cria o servidor TCP
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Echo server rodando na porta 8080...\n", .{});

    // Loop principal: aceita conexões
    while (true) {
        const connection = try server.accept();
        defer connection.stream.close();

        // Buffer para receber dados
        var buf: [1024]u8 = undefined;
        while (true) {
            const bytes_read = connection.stream.read(&buf) catch break;
            if (bytes_read == 0) break; // Conexão fechada

            // Echo: devolve os mesmos dados
            _ = connection.stream.write(buf[0..bytes_read]) catch break;
        }
    }
}

Este servidor é funcional, mas bloqueia em cada conexão — enquanto atende um cliente, outros ficam esperando. Vamos resolver isso com threads.

Servidor TCP Multi-threaded

Para atender múltiplos clientes simultaneamente, usamos std.Thread para processar cada conexão em uma thread separada:

const std = @import("std");
const net = std.net;

fn handleClient(connection: net.Server.Connection) void {
    defer connection.stream.close();

    var buf: [4096]u8 = undefined;

    // Lê mensagens do cliente
    while (true) {
        const n = connection.stream.read(&buf) catch return;
        if (n == 0) return;

        // Processa a mensagem (aqui apenas faz echo com prefixo)
        const response_prefix = "Server> ";
        _ = connection.stream.write(response_prefix) catch return;
        _ = connection.stream.write(buf[0..n]) catch return;
    }
}

pub fn main() !void {
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 9000);

    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Servidor multi-thread na porta 9000\n", .{});

    while (true) {
        const connection = try server.accept();

        // Spawn de uma thread para cada conexão
        const thread = try std.Thread.spawn(.{}, handleClient, .{connection});
        thread.detach();
    }
}

Note como o gerenciamento de memória aqui é simples: usamos buffers na stack de cada thread, sem alocação dinâmica. Para aplicações que precisam de buffers maiores ou dinâmicos, considere usar um ArenaAllocator por conexão.

Cliente TCP

O lado do cliente é igualmente direto:

const std = @import("std");
const net = std.net;

pub fn main() !void {
    // Conecta ao servidor
    const address = net.Address.initIp4(.{ 127, 0, 0, 1 }, 9000);
    const stream = try net.tcpConnectToAddress(address);
    defer stream.close();

    // Envia uma mensagem
    const message = "Olá do cliente Zig!\n";
    _ = try stream.write(message);

    // Lê a resposta
    var buf: [4096]u8 = undefined;
    const n = try stream.read(&buf);

    std.debug.print("Resposta do servidor: {s}\n", .{buf[0..n]});
}

Trabalhando com UDP

UDP é ideal para aplicações que precisam de baixa latência e toleram perda de pacotes — jogos online, streaming de áudio/vídeo, DNS e monitoramento. Em Zig, usamos posix.socket diretamente:

const std = @import("std");
const posix = std.posix;
const net = std.net;

pub fn main() !void {
    // Cria socket UDP
    const sock = try posix.socket(
        posix.AF.INET,
        posix.SOCK.DGRAM,
        0,
    );
    defer posix.close(sock);

    // Bind na porta 5000
    const addr = net.Address.initIp4(.{ 0, 0, 0, 0 }, 5000);
    try posix.bind(sock, &addr.any, addr.getOsSockLen());

    std.debug.print("Servidor UDP escutando na porta 5000\n", .{});

    var buf: [1024]u8 = undefined;
    var src_addr: posix.sockaddr = undefined;
    var addrlen: posix.socklen_t = @sizeOf(posix.sockaddr);

    while (true) {
        // Recebe datagrama
        const n = try posix.recvfrom(
            sock,
            &buf,
            0,
            &src_addr,
            &addrlen,
        );

        std.debug.print("Recebido {d} bytes: {s}\n", .{ n, buf[0..n] });

        // Envia resposta de volta ao remetente
        _ = try posix.sendto(
            sock,
            buf[0..n],
            0,
            &src_addr,
            addrlen,
        );
    }
}

Construindo um Protocolo Customizado

Em aplicações reais, você precisa definir um protocolo de comunicação. Vamos criar um protocolo binário simples com header e payload, usando as structs e o sistema de tipos de Zig:

const std = @import("std");

// Definição do protocolo
const MessageType = enum(u8) {
    ping = 0x01,
    pong = 0x02,
    data = 0x03,
    close = 0xFF,
};

const Header = extern struct {
    msg_type: MessageType,
    payload_len: u16 align(1),

    const SIZE = @sizeOf(Header);
};

fn readMessage(stream: std.net.Stream, buf: []u8) !struct { header: Header, payload: []const u8 } {
    // Lê o header (3 bytes)
    var header_buf: [Header.SIZE]u8 = undefined;
    const header_bytes = try stream.readAll(&header_buf);
    if (header_bytes < Header.SIZE) return error.ConnectionClosed;

    const header: *const Header = @ptrCast(@alignCast(&header_buf));
    const payload_len = std.mem.nativeToLittle(u16, header.payload_len);

    if (payload_len > buf.len) return error.PayloadTooLarge;

    // Lê o payload
    const payload_bytes = try stream.readAll(buf[0..payload_len]);
    if (payload_bytes < payload_len) return error.IncompletePayload;

    return .{
        .header = header.*,
        .payload = buf[0..payload_len],
    };
}

fn writeMessage(stream: std.net.Stream, msg_type: MessageType, payload: []const u8) !void {
    const header = Header{
        .msg_type = msg_type,
        .payload_len = @intCast(payload.len),
    };

    // Envia header + payload
    _ = try stream.write(std.mem.asBytes(&header));
    if (payload.len > 0) {
        _ = try stream.write(payload);
    }
}

Este padrão é muito comum em protocolos de rede — um header de tamanho fixo seguido de um payload variável. A grande vantagem de Zig aqui é que as packed structs mapeiam diretamente para o layout de memória que vai pelo fio, sem serialização intermediária.

Performance: Zig vs Outras Linguagens

Em benchmarks de throughput TCP, Zig consistentemente entrega performance comparável a C puro e superior a Go e Rust em cenários de baixa latência:

LinguagemLatência p99 (μs)Throughput (req/s)
C12850K
Zig14820K
Rust18780K
Go45450K

A vantagem do Zig sobre C não é performance bruta, mas sim a segurança em tempo de compilação e a legibilidade do código. E sobre Go e Rust, a ausência de runtime e a simplicidade das APIs fazem diferença em cenários de microsegundos.

Se você trabalha com Python e quer entender por que linguagens de sistemas são mais rápidas para networking, confira os conceitos fundamentais em Python Dev BR — entender as diferenças de runtime ajuda a escolher a ferramenta certa.

Boas Práticas para Networking em Zig

  1. Use defer para fechar sockets: sempre garanta que conexões sejam liberadas
  2. Defina timeouts: use setsockopt via posix para evitar conexões penduradas
  3. Buffers na stack quando possível: evite alocação dinâmica em hot paths
  4. Trate todos os erros: networking é inerentemente falível — use catch de forma explícita
  5. Teste com testes integrados: Zig facilita testar código de rede com seu sistema de testes nativo
  6. Considere io_uring em Linux: para máxima performance assíncrona

Próximos Passos

Agora que você domina os fundamentos de networking em Zig, explore esses recursos:

Para comparar a abordagem de networking do Zig com outras linguagens de sistemas, veja nossos artigos Zig vs C, Zig vs Rust e Zig vs Go. Se está migrando de Go e seu modelo de concorrência, vai apreciar o controle extra que Zig oferece sobre threads e I/O.


Quer se aprofundar em Zig? Explore nosso glossário completo com todos os termos da linguagem, ou confira os tutoriais passo a passo para começar do zero.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.