Chat TCP em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um servidor de chat TCP que permite múltiplos clientes se conectarem e trocarem mensagens em tempo real. Este é um projeto fundamental para entender programação de redes em Zig, incluindo sockets, polling de I/O e gerenciamento de conexões.
O Que Vamos Construir
Nosso chat vai:
- Aceitar múltiplas conexões TCP simultâneas
- Fazer broadcast de mensagens para todos os clientes conectados
- Suportar nomes de usuário e mensagens formatadas
- Comandos especiais (/nome, /lista, /sair)
- Usar
pollpara I/O não-bloqueante sem threads
Por Que Este Projeto?
Programação de rede é onde Zig realmente brilha. A stdlib fornece wrappers seguros sobre sockets POSIX, e o sistema de erros garante que nunca ignoremos falhas de rede silenciosamente. O modelo de I/O com poll é eficiente e não requer o overhead de threads.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Conhecimentos básicos de redes TCP/IP
- Familiaridade com I/O em Zig
Passo 1: Estrutura do Projeto
mkdir chat-tcp
cd chat-tcp
zig init
Passo 2: Gerenciamento de Clientes
const std = @import("std");
const net = std.net;
const posix = std.posix;
const io = std.io;
const mem = std.mem;
const MAX_CLIENTES = 64;
const TAMANHO_BUF = 1024;
/// Representa um cliente conectado ao chat.
/// Cada cliente tem um socket, um nome e um buffer de leitura.
const Cliente = struct {
socket: posix.socket_t,
nome: [32]u8,
nome_len: usize,
ativo: bool,
buf: [TAMANHO_BUF]u8,
pub fn nomeStr(self: *const Cliente) []const u8 {
return self.nome[0..self.nome_len];
}
pub fn enviar(self: *const Cliente, msg: []const u8) void {
_ = posix.write(self.socket, msg) catch {};
}
};
/// Gerenciador de todos os clientes conectados.
/// Usa um array fixo para evitar alocação dinâmica.
/// A posição no array é o "ID" do cliente.
const GerenciadorClientes = struct {
clientes: [MAX_CLIENTES]Cliente,
quantidade: usize,
const Self = @This();
pub fn init() Self {
var g = Self{
.clientes = undefined,
.quantidade = 0,
};
for (&g.clientes) |*c| {
c.ativo = false;
}
return g;
}
/// Adiciona um novo cliente. Retorna o índice ou null se lotado.
pub fn adicionar(self: *Self, socket: posix.socket_t) ?usize {
for (self.clientes, 0..) |*c, i| {
if (!c.ativo) {
c.* = .{
.socket = socket,
.nome = undefined,
.nome_len = 0,
.ativo = true,
.buf = undefined,
};
// Nome padrão: "Anon_X"
const nome_padrao = std.fmt.bufPrint(&c.nome, "Anon_{d}", .{i}) catch "Anon";
c.nome_len = nome_padrao.len;
self.quantidade += 1;
return i;
}
}
return null;
}
/// Remove um cliente pelo índice.
pub fn remover(self: *Self, idx: usize) void {
if (idx < MAX_CLIENTES and self.clientes[idx].ativo) {
posix.close(self.clientes[idx].socket);
self.clientes[idx].ativo = false;
self.quantidade -= 1;
}
}
/// Envia uma mensagem para todos os clientes exceto o remetente.
pub fn broadcast(self: *Self, msg: []const u8, exceto: ?usize) void {
for (self.clientes, 0..) |*c, i| {
if (c.ativo) {
if (exceto) |ex| {
if (i == ex) continue;
}
c.enviar(msg);
}
}
}
/// Retorna lista de nomes dos clientes ativos.
pub fn listarNomes(self: *const Self, buf: []u8) []const u8 {
var pos: usize = 0;
for (self.clientes) |c| {
if (c.ativo) {
const nome = c.nomeStr();
if (pos + nome.len + 2 < buf.len) {
@memcpy(buf[pos .. pos + nome.len], nome);
pos += nome.len;
buf[pos] = ',';
buf[pos + 1] = ' ';
pos += 2;
}
}
}
if (pos >= 2) pos -= 2; // remove última vírgula
return buf[0..pos];
}
};
Decisão de design: Usamos um array fixo de clientes em vez de ArrayList. Isso elimina alocação dinâmica e torna o gerenciamento de memória trivial. O limite de 64 clientes é razoável para um chat simples.
Passo 3: Processamento de Comandos
/// Processa um comando especial (começa com /).
fn processarComando(
gerenciador: *GerenciadorClientes,
idx: usize,
msg: []const u8,
) void {
const trimmed = mem.trim(u8, msg, " \t\r\n");
if (mem.startsWith(u8, trimmed, "/nome ")) {
const novo_nome = mem.trim(u8, trimmed[6..], " ");
if (novo_nome.len > 0 and novo_nome.len <= 31) {
var buf_msg: [128]u8 = undefined;
const aviso = std.fmt.bufPrint(&buf_msg, "* {s} agora se chama {s}\n", .{
gerenciador.clientes[idx].nomeStr(), novo_nome,
}) catch return;
@memcpy(gerenciador.clientes[idx].nome[0..novo_nome.len], novo_nome);
gerenciador.clientes[idx].nome_len = novo_nome.len;
gerenciador.broadcast(aviso, null);
} else {
gerenciador.clientes[idx].enviar("* Nome invalido (1-31 caracteres)\n");
}
} else if (mem.eql(u8, trimmed, "/lista")) {
var buf_lista: [1024]u8 = undefined;
const lista = gerenciador.listarNomes(&buf_lista);
var buf_msg: [1100]u8 = undefined;
const msg_lista = std.fmt.bufPrint(&buf_msg, "* Usuarios online: {s}\n", .{lista}) catch return;
gerenciador.clientes[idx].enviar(msg_lista);
} else if (mem.eql(u8, trimmed, "/sair")) {
var buf_msg: [128]u8 = undefined;
const aviso = std.fmt.bufPrint(&buf_msg, "* {s} saiu do chat\n", .{
gerenciador.clientes[idx].nomeStr(),
}) catch return;
gerenciador.broadcast(aviso, idx);
gerenciador.remover(idx);
} else if (mem.eql(u8, trimmed, "/ajuda")) {
gerenciador.clientes[idx].enviar(
\\* Comandos disponiveis:
\\* /nome <novo_nome> - Alterar nome
\\* /lista - Ver usuarios online
\\* /sair - Sair do chat
\\* /ajuda - Esta mensagem
\\
);
} else {
gerenciador.clientes[idx].enviar("* Comando desconhecido. Use /ajuda\n");
}
}
Passo 4: Servidor Principal com Poll
/// Configura o servidor TCP.
fn criarServidor(porta: u16) !posix.socket_t {
const endereco = net.Address.initIp4(.{ 0, 0, 0, 0 }, porta);
const socket = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
errdefer posix.close(socket);
// Permite reusar o endereço (útil durante desenvolvimento)
const optval: i32 = 1;
try posix.setsockopt(socket, posix.SOL.SOCKET, posix.SO.REUSEADDR, std.mem.asBytes(&optval));
try posix.bind(socket, &endereco.any, endereco.getOsSockLen());
try posix.listen(socket, 10);
return socket;
}
pub fn main() !void {
const stdout = io.getStdOut().writer();
const porta: u16 = 8080;
const servidor_socket = try criarServidor(porta);
defer posix.close(servidor_socket);
try stdout.print(
\\
\\ ==========================================
\\ CHAT TCP - Servidor Zig
\\ ==========================================
\\ Escutando na porta {d}
\\ Conecte com: nc localhost {d}
\\ ou: telnet localhost {d}
\\
, .{ porta, porta, porta });
var gerenciador = GerenciadorClientes.init();
// Array de poll file descriptors
// Posição 0 = socket do servidor (para aceitar conexões)
// Posições 1+ = sockets dos clientes (para receber dados)
var pollfds: [MAX_CLIENTES + 1]posix.pollfd = undefined;
pollfds[0] = .{
.fd = servidor_socket,
.events = posix.POLL.IN,
.revents = 0,
};
// Inicializa o resto como inválido
for (pollfds[1..]) |*pfd| {
pfd.fd = -1;
pfd.events = 0;
pfd.revents = 0;
}
while (true) {
// Atualiza pollfds com os sockets ativos
for (gerenciador.clientes, 0..) |c, i| {
if (c.ativo) {
pollfds[i + 1] = .{
.fd = c.socket,
.events = posix.POLL.IN,
.revents = 0,
};
} else {
pollfds[i + 1].fd = -1;
}
}
// Espera por atividade em qualquer socket
_ = posix.poll(&pollfds, -1) catch continue;
// Verificar novas conexões
if (pollfds[0].revents & posix.POLL.IN != 0) {
const cliente_socket = posix.accept(servidor_socket, null, null) catch continue;
if (gerenciador.adicionar(cliente_socket)) |idx| {
const nome = gerenciador.clientes[idx].nomeStr();
try stdout.print(" [+] {s} conectou\n", .{nome});
// Boas-vindas
var buf_msg: [256]u8 = undefined;
const boas_vindas = std.fmt.bufPrint(&buf_msg,
\\* Bem-vindo ao Chat Zig! Voce e {s}
\\* Use /nome para mudar, /ajuda para comandos
\\
, .{nome}) catch continue;
gerenciador.clientes[idx].enviar(boas_vindas);
// Avisa outros
const aviso = std.fmt.bufPrint(&buf_msg, "* {s} entrou no chat\n", .{nome}) catch continue;
gerenciador.broadcast(aviso, idx);
} else {
// Servidor lotado
_ = posix.write(cliente_socket, "* Servidor lotado. Tente mais tarde.\n") catch {};
posix.close(cliente_socket);
}
}
// Verificar mensagens dos clientes
for (gerenciador.clientes, 0..) |*c, i| {
if (!c.ativo) continue;
if (pollfds[i + 1].revents & posix.POLL.IN != 0) {
const bytes_lidos = posix.read(c.socket, &c.buf) catch 0;
if (bytes_lidos == 0) {
// Cliente desconectou
var buf_msg: [128]u8 = undefined;
const aviso = std.fmt.bufPrint(&buf_msg, "* {s} desconectou\n", .{c.nomeStr()}) catch continue;
try stdout.print(" [-] {s} desconectou\n", .{c.nomeStr()});
gerenciador.remover(i);
gerenciador.broadcast(aviso, null);
} else {
const msg = c.buf[0..bytes_lidos];
const trimmed = mem.trim(u8, msg, " \t\r\n");
if (trimmed.len > 0 and trimmed[0] == '/') {
processarComando(&gerenciador, i, trimmed);
} else if (trimmed.len > 0) {
// Broadcast mensagem normal
var buf_msg: [TAMANHO_BUF + 64]u8 = undefined;
const formatada = std.fmt.bufPrint(&buf_msg, "[{s}] {s}\n", .{
c.nomeStr(), trimmed,
}) catch continue;
gerenciador.broadcast(formatada, i);
try stdout.print(" {s}", .{formatada});
}
}
}
}
}
}
Passo 5: Cliente Simples
Para testar, você pode usar nc (netcat) ou telnet, mas aqui está um cliente básico em Zig:
// cliente.zig - compilar separadamente
const std = @import("std");
const net = std.net;
const posix = std.posix;
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
const stdin = std.io.getStdIn();
const endereco = net.Address.initIp4(.{ 127, 0, 0, 1 }, 8080);
const socket = try posix.socket(posix.AF.INET, posix.SOCK.STREAM, 0);
defer posix.close(socket);
try posix.connect(socket, &endereco.any, endereco.getOsSockLen());
try stdout.print("Conectado! Digite mensagens (CTRL+C para sair):\n", .{});
var pollfds = [_]posix.pollfd{
.{ .fd = socket, .events = posix.POLL.IN, .revents = 0 },
.{ .fd = stdin.handle, .events = posix.POLL.IN, .revents = 0 },
};
var buf: [1024]u8 = undefined;
while (true) {
_ = posix.poll(&pollfds, -1) catch continue;
// Dados do servidor
if (pollfds[0].revents & posix.POLL.IN != 0) {
const n = posix.read(socket, &buf) catch break;
if (n == 0) break;
try stdout.print("{s}", .{buf[0..n]});
}
// Entrada do teclado
if (pollfds[1].revents & posix.POLL.IN != 0) {
const n = posix.read(stdin.handle, &buf) catch break;
if (n == 0) break;
_ = posix.write(socket, buf[0..n]) catch break;
}
}
try stdout.print("Desconectado.\n", .{});
}
Testes
test "gerenciador - adicionar e remover" {
var g = GerenciadorClientes.init();
// Não podemos testar com sockets reais em testes unitários,
// mas podemos verificar a lógica do gerenciador
try std.testing.expectEqual(@as(usize, 0), g.quantidade);
}
test "listar nomes vazio" {
var g = GerenciadorClientes.init();
var buf: [1024]u8 = undefined;
const lista = g.listarNomes(&buf);
try std.testing.expectEqual(@as(usize, 0), lista.len);
}
Compilando e Executando
# Terminal 1: Servidor
zig build run
# Terminal 2: Cliente
nc localhost 8080
# Terminal 3: Outro cliente
nc localhost 8080
Conceitos Aprendidos
- Programação com sockets TCP em Zig
- I/O multiplexado com
poll - Gerenciamento de múltiplas conexões simultâneas
- Broadcast de mensagens
- Processamento de comandos com prefixo
Próximos Passos
- Explore a documentação de redes da stdlib
- Aprenda sobre threads para I/O paralelo
- Construa o próximo projeto: Servidor HTTP de Arquivos