Networking com Sockets Raw em Zig: TCP, UDP e Protocolos Customizados

Networking e um pilar fundamental da programacao de sistemas moderna. Neste quarto artigo da serie, exploramos como Zig fornece acesso direto a sockets de rede, desde conexoes TCP/UDP simples ate sockets raw para protocolos customizados. A standard library do Zig oferece abstracoees seguras sem sacrificar o controle de baixo nivel que programadores de sistemas precisam.

Para uma introducao mais acessivel a networking, veja Networking e Sockets em Zig. Este artigo foca no nivel mais baixo.

Fundamentos: Sockets TCP

O protocolo TCP (Transmission Control Protocol) fornece comunicacao confiavel e orientada a conexao. E a base de HTTP, SSH, SMTP e muitos outros protocolos.

Servidor TCP Basico

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

pub fn main() !void {
    // Criar socket de escuta
    const endereco = try net.Address.parseIp4("127.0.0.1", 8080);
    var servidor = try net.StreamServer.init(.{
        .reuse_address = true,
    });
    defer servidor.deinit();

    try servidor.listen(endereco);
    std.debug.print("Servidor TCP escutando em 127.0.0.1:8080\n", .{});

    while (true) {
        // Aceitar conexao
        const conexao = try servidor.accept();
        defer conexao.stream.close();

        std.debug.print("Nova conexao de {}\n", .{conexao.address});

        // Ler dados do cliente
        var buffer: [1024]u8 = undefined;
        const bytes = try conexao.stream.read(&buffer);

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

            // Enviar resposta
            try conexao.stream.writeAll("HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nOla, Mundo!");
        }
    }
}

Cliente TCP

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

pub fn main() !void {
    // Conectar ao servidor
    const stream = try net.tcpConnectToHost(
        std.heap.page_allocator,
        "127.0.0.1",
        8080,
    );
    defer stream.close();

    // Enviar requisicao HTTP
    const requisicao =
        "GET / HTTP/1.1\r\n" ++
        "Host: 127.0.0.1\r\n" ++
        "Connection: close\r\n" ++
        "\r\n";
    try stream.writeAll(requisicao);

    // Ler resposta
    var buffer: [4096]u8 = undefined;
    var total: usize = 0;

    while (true) {
        const n = try stream.read(buffer[total..]);
        if (n == 0) break;
        total += n;
    }

    std.debug.print("Resposta:\n{s}\n", .{buffer[0..total]});
}

Sockets UDP

UDP (User Datagram Protocol) e um protocolo sem conexao, ideal para aplicacoes que precisam de baixa latencia e toleram perda de pacotes (jogos, streaming, DNS).

Servidor e Cliente UDP

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

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

    const endereco = net.Address.initIp4(.{ 127, 0, 0, 1 }, 9090);
    try posix.bind(sock, &endereco.any, endereco.getOsSockLen());

    std.debug.print("Servidor UDP escutando em 127.0.0.1:9090\n", .{});

    var buffer: [1024]u8 = undefined;
    while (true) {
        var src_addr: posix.sockaddr = undefined;
        var addr_len: posix.socklen_t = @sizeOf(posix.sockaddr);

        const n = try posix.recvfrom(
            sock,
            &buffer,
            0,
            &src_addr,
            &addr_len,
        );

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

        // Ecoar de volta
        _ = try posix.sendto(
            sock,
            buffer[0..n],
            0,
            &src_addr,
            addr_len,
        );
    }
}

pub fn main() !void {
    try servidorUDP();
}

Servidor TCP Concorrente

Para lidar com multiplos clientes simultaneamente, podemos usar threads:

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

fn handleCliente(conexao: net.StreamServer.Connection) void {
    defer conexao.stream.close();

    var buffer: [4096]u8 = undefined;

    while (true) {
        const bytes = conexao.stream.read(&buffer) catch |err| {
            std.debug.print("Erro de leitura: {}\n", .{err});
            return;
        };

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

        const dados = buffer[0..bytes];
        std.debug.print("[{}] {s}", .{ conexao.address, dados });

        // Ecoar dados de volta (echo server)
        conexao.stream.writeAll(dados) catch |err| {
            std.debug.print("Erro de escrita: {}\n", .{err});
            return;
        };
    }
}

pub fn main() !void {
    const endereco = try net.Address.parseIp4("0.0.0.0", 8080);
    var servidor = try net.StreamServer.init(.{
        .reuse_address = true,
    });
    defer servidor.deinit();

    try servidor.listen(endereco);
    std.debug.print("Echo server rodando em 0.0.0.0:8080\n", .{});

    while (true) {
        const conexao = servidor.accept() catch |err| {
            std.debug.print("Erro ao aceitar: {}\n", .{err});
            continue;
        };

        // Criar thread para cada cliente
        _ = std.Thread.spawn(.{}, handleCliente, .{conexao}) catch |err| {
            std.debug.print("Erro ao criar thread: {}\n", .{err});
            conexao.stream.close();
        };
    }
}

Sockets Raw

Sockets raw permitem enviar e receber pacotes de rede no nivel mais baixo, dando acesso direto aos cabecalhos IP e protocolos customizados. Geralmente requerem privilegios de root.

Construindo um Pacote ICMP (Ping)

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

const IcmpHeader = packed struct {
    tipo: u8,
    codigo: u8,
    checksum: u16,
    identificador: u16,
    sequencia: u16,
};

fn calcularChecksum(data: []const u8) u16 {
    var soma: u32 = 0;
    var i: usize = 0;

    while (i + 1 < data.len) : (i += 2) {
        soma += @as(u32, data[i]) << 8 | @as(u32, data[i + 1]);
    }

    if (i < data.len) {
        soma += @as(u32, data[i]) << 8;
    }

    while (soma >> 16 != 0) {
        soma = (soma & 0xFFFF) + (soma >> 16);
    }

    return @truncate(~soma);
}

pub fn main() !void {
    // Requer CAP_NET_RAW ou root
    const sock = try posix.socket(
        posix.AF.INET,
        posix.SOCK.RAW,
        posix.IPPROTO.ICMP,
    );
    defer posix.close(sock);

    // Construir pacote ICMP Echo Request
    var pacote: [64]u8 = std.mem.zeroes([64]u8);
    const header: *IcmpHeader = @ptrCast(@alignCast(&pacote));
    header.tipo = 8; // Echo Request
    header.codigo = 0;
    header.identificador = @byteSwap(@as(u16, @truncate(std.os.linux.getpid())));
    header.sequencia = @byteSwap(@as(u16, 1));

    // Preencher dados
    for (pacote[@sizeOf(IcmpHeader)..]) |*byte| {
        byte.* = 0x42;
    }

    // Calcular checksum
    header.checksum = 0;
    header.checksum = calcularChecksum(&pacote);

    // Enviar para 8.8.8.8 (Google DNS)
    const destino = std.net.Address.initIp4(.{ 8, 8, 8, 8 }, 0);
    _ = try posix.sendto(
        sock,
        &pacote,
        0,
        &destino.any,
        destino.getOsSockLen(),
    );

    std.debug.print("Ping enviado para 8.8.8.8\n", .{});

    // Aguardar resposta
    var resposta: [1024]u8 = undefined;
    const n = try posix.read(sock, &resposta);

    if (n > 20) { // Cabecalho IP tem pelo menos 20 bytes
        const icmp_resp: *const IcmpHeader = @ptrCast(@alignCast(&resposta[20]));
        if (icmp_resp.tipo == 0) {
            std.debug.print("Pong recebido! ({d} bytes)\n", .{n});
        }
    }
}

Resolucao DNS Manual

Entender como DNS funciona no nivel do socket:

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

pub fn resolverDNS(allocator: std.mem.Allocator, hostname: []const u8) ![]net.Address {
    var enderecos = std.ArrayList(net.Address).init(allocator);
    defer enderecos.deinit();

    // Usar a resolucao DNS da standard library
    const lista = try net.getAddressList(allocator, hostname, 80);
    defer lista.deinit();

    for (lista.addrs) |addr| {
        try enderecos.append(addr);
    }

    return try enderecos.toOwnedSlice();
}

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

    const hosts = [_][]const u8{
        "ziglang.org",
        "github.com",
        "google.com",
    };

    for (hosts) |host| {
        std.debug.print("\nResolvendo {s}:\n", .{host});

        const enderecos = resolverDNS(allocator, host) catch |err| {
            std.debug.print("  Erro: {}\n", .{err});
            continue;
        };
        defer allocator.free(enderecos);

        for (enderecos) |addr| {
            std.debug.print("  -> {}\n", .{addr});
        }
    }
}

Opcoes de Socket Avancadas

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

pub fn configurarSocket(sock: posix.socket_t) !void {
    // Reutilizar endereco (evita "address already in use")
    try posix.setsockopt(sock, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));

    // Timeout de leitura (5 segundos)
    const timeout = posix.timeval{
        .tv_sec = 5,
        .tv_usec = 0,
    };
    try posix.setsockopt(sock, posix.SOL.SOCKET, posix.SO.RCVTIMEO, std.mem.asBytes(&timeout));

    // TCP Keep-Alive
    try posix.setsockopt(sock, posix.SOL.SOCKET, posix.SO.KEEPALIVE, &std.mem.toBytes(@as(c_int, 1)));

    // Desabilitar Nagle algorithm (util para baixa latencia)
    try posix.setsockopt(sock, posix.IPPROTO.TCP, std.os.linux.TCP.NODELAY, &std.mem.toBytes(@as(c_int, 1)));
}

Comparacao: TCP vs UDP vs Raw

AspectoTCPUDPRaw
ConfiabilidadeGarantidaSem garantiaDepende de voce
OrdemMantidaNao garantidaDepende de voce
OverheadAlto (handshake, ACK)BaixoMinimo
Uso tipicoHTTP, SSH, DBDNS, games, VoIPPing, sniffing
PrivilegiosNenhum especialNenhum especialRoot/CAP_NET_RAW

Exercicios

  1. Chat server: Implemente um servidor de chat TCP que aceite multiplos clientes e retransmita mensagens entre todos os conectados.

  2. Port scanner: Crie um scanner que verifique quais portas TCP estao abertas em um endereco IP.

  3. DNS lookup: Implemente um cliente DNS simples que envie queries UDP para um servidor DNS e parse as respostas.


Proximo Artigo

No artigo final da serie, exploramos io_uring e I/O assincrono, a interface mais moderna e performatica para I/O no Linux.

Conteudo Relacionado


Duvidas sobre networking em Zig? Participe da comunidade Zig Brasil e troque experiencias!

Continue aprendendo Zig

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