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

Programação de rede é uma das áreas onde linguagens de sistemas realmente brilham, e Zig lang não é exceção. Com acesso direto a sockets BSD através do módulo std.net, a linguagem Zig permite criar servidores, clientes e ferramentas de rede com controle total sobre o protocolo, sem as camadas de abstração que escondem detalhes importantes em linguagens de mais alto nível. E ao contrário de C, Zig faz tudo isso com segurança de memória e tratamento de erros explícito.

Neste tutorial, vamos construir aplicações de rede reais em Zig: desde um servidor TCP básico até um chat simples com múltiplas conexões, passando por comunicação UDP e resolução DNS. Cada exemplo é funcional e pode ser usado como base para seus próprios projetos.

Fundamentos de Networking em Zig com std.net

O módulo std.net é a interface principal para programação de rede em Zig. Ele abstrai as diferenças entre plataformas (Linux, macOS, Windows) enquanto mantém acesso aos conceitos fundamentais de sockets.

Os componentes principais são:

  • std.net.StreamServer: servidor TCP que aceita conexões.
  • std.net.Stream: conexão TCP bidirecional (leitura e escrita).
  • std.net.Address: endereço IP + porta.
const std = @import("std");
const net = std.net;

pub fn main() !void {
    // Criar um endereço para IPv4
    const addr = net.Address.initIp4(.{ 127, 0, 0, 1 }, 8080);
    std.debug.print("Endereço: {}\n", .{addr});

    // Resolver um hostname
    const resolved = try net.Address.resolveIp("0.0.0.0", 3000);
    std.debug.print("Resolvido: {}\n", .{resolved});
}

Criando um Servidor TCP

Um servidor TCP segue o fluxo clássico: bind, listen, accept. Em Zig, o StreamServer encapsula essa lógica de forma limpa.

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

pub fn main() !void {
    // Criar o servidor
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Servidor ouvindo na porta 8080...\n", .{});

    // Loop principal: aceitar conexões
    while (true) {
        // accept() bloqueia até uma nova conexão chegar
        const connection = try server.accept();
        defer connection.stream.close();

        std.debug.print("Nova conexão de: {}\n", .{connection.address});

        // Ler dados do cliente
        var buf: [1024]u8 = undefined;
        const bytes_read = connection.stream.read(&buf) catch |err| {
            std.debug.print("Erro de leitura: {}\n", .{err});
            continue;
        };

        if (bytes_read == 0) continue;

        const mensagem = buf[0..bytes_read];
        std.debug.print("Recebido: {s}\n", .{mensagem});

        // Enviar resposta
        const resposta = "Mensagem recebida com sucesso!\n";
        _ = try connection.stream.write(resposta);
    }
}

Servidor com Echo (Eco)

Um servidor echo é o “Hello World” da programação de rede: ele simplesmente devolve tudo que recebe.

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

fn handleClient(stream: net.Stream) void {
    defer stream.close();

    var buf: [4096]u8 = undefined;

    while (true) {
        const bytes_read = stream.read(&buf) catch {
            std.debug.print("Cliente desconectou (erro de leitura).\n", .{});
            return;
        };

        if (bytes_read == 0) {
            std.debug.print("Cliente desconectou.\n", .{});
            return;
        }

        // Eco: enviar de volta os mesmos dados
        stream.writeAll(buf[0..bytes_read]) catch {
            std.debug.print("Erro ao enviar resposta.\n", .{});
            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("Echo server na porta 9000. Teste com: nc localhost 9000\n", .{});

    while (true) {
        const connection = try server.accept();
        std.debug.print("Cliente conectado: {}\n", .{connection.address});
        handleClient(connection.stream);
    }
}

Criando um Cliente TCP

O lado do cliente é mais simples: conecte, envie dados e receba a resposta.

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

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

    std.debug.print("Conectado ao servidor!\n", .{});

    // Enviar dados
    const mensagem = "Olá do cliente Zig!";
    _ = try stream.write(mensagem);
    std.debug.print("Enviado: {s}\n", .{mensagem});

    // Receber resposta
    var buf: [1024]u8 = undefined;
    const bytes_read = try stream.read(&buf);

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

Cliente HTTP Simples

Vamos construir um cliente que faz uma requisição HTTP GET básica:

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

pub fn httpGet(allocator: std.mem.Allocator, host: []const u8, path: []const u8) ![]u8 {
    // Resolver o hostname e conectar
    const address_list = try net.getAddressList(allocator, host, 80);
    defer address_list.deinit();

    if (address_list.addrs.len == 0) return error.CouldNotResolve;

    const stream = try net.tcpConnectToAddress(address_list.addrs[0]);
    defer stream.close();

    // Montar e enviar a requisição HTTP
    var buf: [4096]u8 = undefined;
    const request = try std.fmt.bufPrint(&buf,
        "GET {s} HTTP/1.1\r\nHost: {s}\r\nConnection: close\r\n\r\n",
        .{ path, host },
    );
    _ = try stream.write(request);

    // Ler a resposta completa
    var response = std.ArrayList(u8).init(allocator);
    errdefer response.deinit();

    var read_buf: [4096]u8 = undefined;
    while (true) {
        const n = stream.read(&read_buf) catch break;
        if (n == 0) break;
        try response.appendSlice(read_buf[0..n]);
    }

    return try response.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const response = try httpGet(allocator, "example.com", "/");
    defer allocator.free(response);

    // Mostrar apenas os primeiros 500 bytes
    const preview_len = @min(response.len, 500);
    std.debug.print("{s}\n", .{response[0..preview_len]});
}

Sockets UDP: Comunicação por Datagramas

UDP é um protocolo sem conexão, mais simples e rápido que TCP, mas sem garantia de entrega. Em Zig, usamos a API POSIX de baixo nível para UDP.

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

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

    const addr = posix.sockaddr.in{
        .family = posix.AF.INET,
        .port = std.mem.nativeToBig(u16, 9999),
        .addr = 0, // 0.0.0.0
    };

    try posix.bind(sock, @ptrCast(&addr), @sizeOf(@TypeOf(addr)));
    std.debug.print("Servidor UDP ouvindo na porta 9999...\n", .{});

    var buf: [1024]u8 = undefined;
    var client_addr: posix.sockaddr.in = undefined;
    var addr_len: posix.socklen_t = @sizeOf(posix.sockaddr.in);

    while (true) {
        const bytes_read = try posix.recvfrom(
            sock,
            &buf,
            0,
            @ptrCast(&client_addr),
            &addr_len,
        );

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

        // Responder ao cliente
        const resposta = "ACK";
        _ = try posix.sendto(
            sock,
            resposta,
            0,
            @ptrCast(&client_addr),
            addr_len,
        );
    }
}

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

    const server_addr = posix.sockaddr.in{
        .family = posix.AF.INET,
        .port = std.mem.nativeToBig(u16, 9999),
        .addr = std.mem.nativeToBig(u32, 0x7F000001), // 127.0.0.1
    };

    const mensagem = "Olá via UDP!";
    _ = try posix.sendto(
        sock,
        mensagem,
        0,
        @ptrCast(&server_addr),
        @sizeOf(@TypeOf(server_addr)),
    );

    std.debug.print("Enviado: {s}\n", .{mensagem});
}

As diferenças fundamentais entre TCP e UDP em Zig são as mesmas de qualquer linguagem: TCP garante ordem e entrega, UDP é mais rápido mas sem garantias. A API de sockets em Zig reflete isso diretamente, usando SOCK.STREAM para TCP e SOCK.DGRAM para UDP.

Construindo um Chat Server Simples

Vamos combinar tudo em um chat server que aceita múltiplas conexões usando threads:

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

const MAX_CLIENTS = 10;

const ChatServer = struct {
    clients: [MAX_CLIENTS]?net.Stream = [_]?net.Stream{null} ** MAX_CLIENTS,
    mutex: std.Thread.Mutex = .{},

    pub fn broadcast(self: *ChatServer, mensagem: []const u8, remetente: usize) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        for (&self.clients, 0..) |*slot, i| {
            if (i == remetente) continue; // Não enviar para quem mandou
            if (slot.*) |stream| {
                stream.writeAll(mensagem) catch {
                    slot.* = null; // Remover cliente desconectado
                };
            }
        }
    }

    pub fn addClient(self: *ChatServer, stream: net.Stream) ?usize {
        self.mutex.lock();
        defer self.mutex.unlock();

        for (&self.clients, 0..) |*slot, i| {
            if (slot.* == null) {
                slot.* = stream;
                return i;
            }
        }
        return null; // Servidor lotado
    }

    pub fn removeClient(self: *ChatServer, index: usize) void {
        self.mutex.lock();
        defer self.mutex.unlock();

        if (self.clients[index]) |stream| {
            stream.close();
        }
        self.clients[index] = null;
    }
};

var server_state = ChatServer{};

fn handleChatClient(stream: net.Stream) void {
    const slot = server_state.addClient(stream) orelse {
        stream.writeAll("Servidor lotado!\n") catch {};
        stream.close();
        return;
    };
    defer server_state.removeClient(slot);

    // Anunciar entrada
    var msg_buf: [256]u8 = undefined;
    const entrada = std.fmt.bufPrint(&msg_buf, "[Sistema] Usuário {} entrou no chat.\n", .{slot}) catch return;
    server_state.broadcast(entrada, slot);

    stream.writeAll("Bem-vindo ao chat! Digite suas mensagens:\n") catch return;

    // Loop de mensagens
    var buf: [1024]u8 = undefined;
    while (true) {
        const n = stream.read(&buf) catch break;
        if (n == 0) break;

        const texto = std.mem.trimRight(u8, buf[0..n], "\r\n");
        if (texto.len == 0) continue;

        var broadcast_buf: [1280]u8 = undefined;
        const formatted = std.fmt.bufPrint(&broadcast_buf, "[Usuário {}]: {s}\n", .{ slot, texto }) catch continue;
        server_state.broadcast(formatted, slot);
    }

    // Anunciar saída
    const saida = std.fmt.bufPrint(&msg_buf, "[Sistema] Usuário {} saiu do chat.\n", .{slot}) catch return;
    server_state.broadcast(saida, slot);
}

pub fn main() !void {
    const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 7777);
    var server = try address.listen(.{
        .reuse_address = true,
    });
    defer server.deinit();

    std.debug.print("Chat server rodando na porta 7777.\n", .{});
    std.debug.print("Conecte com: nc localhost 7777\n", .{});

    while (true) {
        const connection = try server.accept();
        std.debug.print("Novo cliente: {}\n", .{connection.address});

        // Cada cliente em sua própria thread
        _ = std.Thread.spawn(.{}, handleChatClient, .{connection.stream}) catch |err| {
            std.debug.print("Erro ao criar thread: {}\n", .{err});
            connection.stream.close();
            continue;
        };
    }
}

Para testar, abra múltiplos terminais e conecte-se com nc localhost 7777. Cada mensagem digitada aparecerá nos outros terminais conectados.

Lidando com Múltiplas Conexões usando Threads

O exemplo do chat já demonstra o padrão básico de uma thread por conexão. Para cenários mais avançados, considere um pool de threads:

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

const WorkerPool = struct {
    pool: std.Thread.Pool,

    pub fn init(allocator: std.mem.Allocator) !WorkerPool {
        var pool: std.Thread.Pool = undefined;
        try pool.init(.{
            .allocator = allocator,
            .n_jobs = 8, // 8 worker threads
        });
        return .{ .pool = pool };
    }

    pub fn deinit(self: *WorkerPool) void {
        self.pool.deinit();
    }

    pub fn dispatch(self: *WorkerPool, stream: net.Stream) void {
        self.pool.spawn(processConnection, .{stream}) catch {
            stream.close();
        };
    }
};

fn processConnection(stream: net.Stream) void {
    defer stream.close();

    var buf: [4096]u8 = undefined;
    while (true) {
        const n = stream.read(&buf) catch break;
        if (n == 0) break;

        // Processar a requisição
        stream.writeAll(buf[0..n]) catch break;
    }
}

O Thread.Pool reutiliza threads ao invés de criar uma nova para cada conexão, reduzindo o overhead em servidores com muitas conexões simultâneas. Para entender melhor os modelos de concorrência disponíveis, veja o tutorial de concorrência em Zig.

Resolução DNS em Zig

Resolver nomes de domínio para endereços IP é essencial para qualquer aplicação de rede:

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

pub fn resolverDNS(allocator: std.mem.Allocator, hostname: []const u8) !void {
    std.debug.print("Resolvendo '{s}'...\n", .{hostname});

    const list = try net.getAddressList(allocator, hostname, 80);
    defer list.deinit();

    if (list.addrs.len == 0) {
        std.debug.print("  Nenhum endereço encontrado.\n", .{});
        return;
    }

    std.debug.print("  Endereços encontrados: {}\n", .{list.addrs.len});
    for (list.addrs, 0..) |addr, i| {
        std.debug.print("  [{}] {}\n", .{ i, addr });
    }

    // O canon_name pode conter o nome canônico
    if (list.canon_name) |canon| {
        std.debug.print("  Nome canônico: {s}\n", .{canon});
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    try resolverDNS(allocator, "google.com");
    try resolverDNS(allocator, "zig-lang.org");
    try resolverDNS(allocator, "localhost");
}

Exemplo Prático: Um Port Scanner

Vamos construir uma ferramenta prática: um scanner de portas que verifica quais portas estão abertas em um host.

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

const ScanResult = struct {
    port: u16,
    open: bool,
    service: []const u8,
};

fn scanPort(ip: [4]u8, port: u16, timeout_ms: u32) bool {
    const address = net.Address.initIp4(ip, port);
    const stream = net.tcpConnectToAddress(address) catch return false;
    _ = timeout_ms;
    stream.close();
    return true;
}

fn serviceName(port: u16) []const u8 {
    return switch (port) {
        21 => "FTP",
        22 => "SSH",
        23 => "Telnet",
        25 => "SMTP",
        53 => "DNS",
        80 => "HTTP",
        110 => "POP3",
        143 => "IMAP",
        443 => "HTTPS",
        3306 => "MySQL",
        5432 => "PostgreSQL",
        6379 => "Redis",
        8080 => "HTTP-Alt",
        8443 => "HTTPS-Alt",
        27017 => "MongoDB",
        else => "Desconhecido",
    };
}

pub fn main() !void {
    const target_ip = [4]u8{ 127, 0, 0, 1 };
    const portas_comuns = [_]u16{
        21, 22, 23, 25, 53, 80, 110, 143, 443,
        3000, 3306, 5432, 6379, 8080, 8443, 27017,
    };

    std.debug.print("=== Port Scanner Zig ===\n", .{});
    std.debug.print("Alvo: {}.{}.{}.{}\n", .{
        target_ip[0], target_ip[1], target_ip[2], target_ip[3],
    });
    std.debug.print("Escaneando {} portas...\n\n", .{portas_comuns.len});

    var abertas: usize = 0;

    for (portas_comuns) |port| {
        if (scanPort(target_ip, port, 1000)) {
            abertas += 1;
            std.debug.print("  ABERTA  {: >5}  {s}\n", .{ port, serviceName(port) });
        }
    }

    std.debug.print("\nResultado: {} porta(s) aberta(s) de {} escaneadas.\n", .{
        abertas,
        portas_comuns.len,
    });
}

Para um scanner mais eficiente, você pode paralelizar as verificações usando threads ou até mesmo I/O assíncrono, escaneando centenas de portas simultaneamente. Se o seu objetivo e construir um servidor web completo, veja o tutorial Servidor HTTP em Zig. A versão sequencial acima é ideal para aprendizado e já funciona bem para verificações rápidas em redes locais.

Boas Práticas de Networking em Zig

Ao trabalhar com sockets em Zig, algumas práticas são essenciais:

  1. Sempre use defer para fechar sockets: vazamentos de file descriptors podem esgotar os recursos do sistema.
  2. Trate timeouts: conexões podem ficar penduradas indefinidamente sem timeouts adequados.
  3. Valide dados de rede: nunca confie em dados recebidos de um socket sem validação.
  4. Use buffered I/O: para protocolos baseados em texto, buffered readers e writers melhoram significativamente a performance.
  5. Limite conexões: sem limite, um servidor pode esgotar memória e file descriptors.

Conclusão

Zig oferece uma base sólida para programação de rede, com acesso direto ao sistema de sockets sem sacrificar a segurança. O módulo std.net cobre os casos de uso mais comuns de forma idiomática, enquanto std.posix permite acesso de baixo nível quando necessário. Desde servidores TCP simples até aplicações multi-threaded como nosso chat server, Zig combina a performance de C com a segurança e ergonomia que desenvolvedores modernos esperam.

Leia Também

Continue aprendendo Zig

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