Como Criar um Servidor TCP em Zig

Introdução

Um servidor TCP é um programa que escuta em uma porta específica, aceita conexões de clientes e processa suas requisições. Servidores TCP são a base de muitos protocolos de aplicação como HTTP, FTP e SMTP.

Nesta receita, você aprenderá a construir servidores TCP em Zig, desde um servidor simples de conexão única até um servidor que gerencia múltiplos clientes simultaneamente.

Pré-requisitos

Servidor TCP Básico

Este servidor aceita uma conexão por vez, lê a mensagem do cliente e envia uma resposta:

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

pub fn main() !void {
    // Criar o servidor escutando em 0.0.0.0:8080
    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 escutando na porta 8080...\n", .{});

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

        std.debug.print("Cliente conectado: {}\n", .{connection.address});

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

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

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

Como testar

Em um terminal, inicie o servidor:

zig build-exe server.zig && ./server

Em outro terminal, conecte-se com netcat:

echo "Olá, servidor!" | nc localhost 8080

Saída esperada

No servidor:

Servidor escutando na porta 8080...
Cliente conectado: 127.0.0.1:54321
Recebido: Olá, servidor!

No cliente:

Mensagem recebida com sucesso!

Servidor Echo (Eco)

Um servidor echo repete de volta tudo que o cliente envia. É útil para testes de rede:

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

pub fn main() !void {
    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 echo na porta 8080...\n", .{});

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

        // Eco: enviar de volta tudo que receber
        var buffer: [4096]u8 = undefined;
        while (true) {
            const bytes_read = connection.stream.read(&buffer) catch break;
            if (bytes_read == 0) break;

            // Escrever de volta exatamente o que foi recebido
            _ = connection.stream.write(buffer[0..bytes_read]) catch break;
        }

        std.debug.print("Cliente desconectado.\n", .{});
    }
}

Servidor Multi-cliente com Threads

Para atender múltiplos clientes simultaneamente, use threads:

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

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

    std.debug.print("Novo cliente: {}\n", .{connection.address});

    var buffer: [4096]u8 = undefined;
    while (true) {
        const bytes_read = connection.stream.read(&buffer) catch |err| {
            std.debug.print("Erro de leitura: {}\n", .{err});
            break;
        };
        if (bytes_read == 0) break;

        const message = buffer[0..bytes_read];
        std.debug.print("[{}] {s}\n", .{ connection.address, message });

        // Responder ao cliente
        const response = "OK\n";
        _ = connection.stream.write(response) catch break;
    }

    std.debug.print("Cliente desconectado: {}\n", .{connection.address});
}

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

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

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

        // Criar uma thread para cada conexão
        const thread = std.Thread.spawn(.{}, handleClient, .{connection}) catch |err| {
            std.debug.print("Erro ao criar thread: {}\n", .{err});
            connection.stream.close();
            continue;
        };
        thread.detach();
    }
}

Servidor com Protocolo Customizado

Este exemplo implementa um protocolo simples com comprimento prefixado:

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

const Protocol = struct {
    /// Enviar mensagem com prefixo de comprimento (4 bytes big-endian)
    pub fn sendMessage(stream: net.Stream, data: []const u8) !void {
        // Enviar comprimento como 4 bytes big-endian
        const len: u32 = @intCast(data.len);
        const len_bytes = std.mem.toBytes(std.mem.nativeToBig(u32, len));
        _ = try stream.write(&len_bytes);
        _ = try stream.write(data);
    }

    /// Receber mensagem com prefixo de comprimento
    pub fn recvMessage(stream: net.Stream, buffer: []u8) !?[]const u8 {
        // Ler 4 bytes do comprimento
        var len_bytes: [4]u8 = undefined;
        const header_read = try stream.read(&len_bytes);
        if (header_read == 0) return null;
        if (header_read != 4) return error.IncompleteHeader;

        const len = std.mem.bigToNative(u32, std.mem.bytesToValue(u32, &len_bytes));

        if (len > buffer.len) return error.MessageTooLarge;

        // Ler o corpo da mensagem
        var total_read: usize = 0;
        while (total_read < len) {
            const n = try stream.read(buffer[total_read..len]);
            if (n == 0) return error.ConnectionClosed;
            total_read += n;
        }

        return buffer[0..len];
    }
};

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 de protocolo customizado na porta 9000...\n", .{});

    while (true) {
        const conn = try server.accept();
        defer conn.stream.close();

        var buffer: [65536]u8 = undefined;

        while (true) {
            const msg = Protocol.recvMessage(conn.stream, &buffer) catch break;
            if (msg == null) break;

            std.debug.print("Mensagem: {s}\n", .{msg.?});

            // Responder com protocolo
            Protocol.sendMessage(conn.stream, "Recebido!") catch break;
        }
    }
}

Dicas e Boas Práticas

  1. Use reuse_address: A opção .reuse_address = true permite reiniciar o servidor rapidamente sem esperar o timeout do SO.

  2. Defina backlog adequado: O kernel_backlog define quantas conexões pendentes o kernel mantém na fila.

  3. Feche conexões com defer: Use defer connection.stream.close() para garantir limpeza correta.

  4. Limite o número de threads: Em produção, considere usar um pool de threads em vez de criar uma thread por conexão.

  5. Trate erros de rede graciosamente: Clientes podem desconectar a qualquer momento. Use o tratamento de erros para lidar com isso.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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