Como Criar um Cliente TCP em Zig

Introdução

Um cliente TCP é um programa que inicia uma conexão com um servidor remoto usando o protocolo TCP (Transmission Control Protocol). TCP garante a entrega ordenada e confiável de dados, sendo essencial para aplicações como navegadores web, clientes de e-mail e ferramentas de linha de comando.

Nesta receita, você aprenderá a criar um cliente TCP funcional em Zig usando a biblioteca padrão std.net. Veremos como estabelecer conexões, enviar requisições e processar respostas.

Pré-requisitos

Cliente TCP Básico

O exemplo a seguir conecta-se a um servidor, envia uma mensagem e lê a resposta:

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

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

    // Enviar dados ao servidor
    const message = "Olá, servidor!";
    _ = try stream.write(message);

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

    if (bytes_read > 0) {
        const response = buffer[0..bytes_read];
        std.debug.print("Resposta do servidor: {s}\n", .{response});
    } else {
        std.debug.print("Servidor fechou a conexão sem responder.\n", .{});
    }
}

Saída esperada

Resposta do servidor: Mensagem recebida com sucesso!

Conectando por Nome de Host

Na maioria dos casos reais, você se conecta por nome de host em vez de endereço IP direto. Use net.tcpConnectToHost para resolução DNS automática:

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

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

    // Conectar por nome de host (resolução DNS automática)
    const stream = try net.tcpConnectToHost(allocator, "example.com", 80);
    defer stream.close();

    // Enviar uma requisição HTTP simples
    const request =
        "GET / HTTP/1.1\r\n" ++
        "Host: example.com\r\n" ++
        "Connection: close\r\n" ++
        "\r\n";

    _ = try stream.write(request);

    // Ler toda a resposta
    var response_buf = std.ArrayList(u8).init(allocator);
    defer response_buf.deinit();

    var buffer: [4096]u8 = undefined;
    while (true) {
        const bytes_read = try stream.read(&buffer);
        if (bytes_read == 0) break;
        try response_buf.appendSlice(buffer[0..bytes_read]);
    }

    std.debug.print("Resposta ({d} bytes):\n{s}\n", .{
        response_buf.items.len,
        response_buf.items,
    });
}

Cliente TCP com Timeout

Em aplicações de produção, é importante configurar timeouts para evitar que o programa fique bloqueado indefinidamente:

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

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

    // Configurar timeout de leitura (5 segundos)
    const timeout = posix.timeval{
        .sec = 5,
        .usec = 0,
    };
    try posix.setsockopt(
        stream.handle,
        posix.SOL.SOCKET,
        posix.SO.RCVTIMEO,
        &std.mem.toBytes(timeout),
    );

    // Configurar timeout de escrita (5 segundos)
    try posix.setsockopt(
        stream.handle,
        posix.SOL.SOCKET,
        posix.SO.SNDTIMEO,
        &std.mem.toBytes(timeout),
    );

    _ = try stream.write("Dados com timeout");

    var buffer: [1024]u8 = undefined;
    const bytes_read = stream.read(&buffer) catch |err| {
        if (err == error.WouldBlock) {
            std.debug.print("Timeout: servidor não respondeu em 5 segundos.\n", .{});
            return;
        }
        return err;
    };

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

Cliente TCP com Reconexão Automática

Para aplicações que precisam manter conexões persistentes, implemente um mecanismo de reconexão:

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

const TcpClient = struct {
    address: net.Address,
    stream: ?net.Stream,
    max_retries: u32,

    pub fn init(ip: [4]u8, port: u16, max_retries: u32) TcpClient {
        return .{
            .address = net.Address.initIp4(ip, port),
            .stream = null,
            .max_retries = max_retries,
        };
    }

    pub fn connect(self: *TcpClient) !void {
        var attempt: u32 = 0;
        while (attempt < self.max_retries) : (attempt += 1) {
            self.stream = net.tcpConnectToAddress(self.address) catch |err| {
                std.debug.print(
                    "Tentativa {d}/{d} falhou: {}\n",
                    .{ attempt + 1, self.max_retries, err },
                );
                std.time.sleep(std.time.ns_per_s * (attempt + 1));
                continue;
            };
            std.debug.print("Conectado com sucesso na tentativa {d}!\n", .{attempt + 1});
            return;
        }
        return error.ConnectionFailed;
    }

    pub fn send(self: *TcpClient, data: []const u8) !usize {
        if (self.stream) |stream| {
            return stream.write(data);
        }
        return error.NotConnected;
    }

    pub fn receive(self: *TcpClient, buffer: []u8) !usize {
        if (self.stream) |stream| {
            return stream.read(buffer);
        }
        return error.NotConnected;
    }

    pub fn close(self: *TcpClient) void {
        if (self.stream) |stream| {
            stream.close();
            self.stream = null;
        }
    }
};

pub fn main() !void {
    var client = TcpClient.init(.{ 127, 0, 0, 1 }, 8080, 3);
    defer client.close();

    try client.connect();

    _ = try client.send("Olá com reconexão!");

    var buffer: [1024]u8 = undefined;
    const n = try client.receive(&buffer);
    std.debug.print("Recebido: {s}\n", .{buffer[0..n]});
}

Dicas e Boas Práticas

  1. Sempre feche conexões: Use defer stream.close() para garantir que a conexão seja fechada mesmo em caso de erro.

  2. Trate erros de rede: Conexões podem falhar por diversos motivos. Utilize o tratamento de erros do Zig adequadamente.

  3. Use buffers adequados: Dimensione seus buffers de acordo com o tamanho esperado dos dados. Para dados grandes, leia em loop.

  4. Configure timeouts: Em produção, sempre configure timeouts para evitar bloqueios indefinidos.

  5. Gerencie memória corretamente: Use o gerenciamento de memória do Zig com allocators apropriados.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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