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:
- Sempre use
deferpara fechar sockets: vazamentos de file descriptors podem esgotar os recursos do sistema. - Trate timeouts: conexões podem ficar penduradas indefinidamente sem timeouts adequados.
- Valide dados de rede: nunca confie em dados recebidos de um socket sem validação.
- Use buffered I/O: para protocolos baseados em texto, buffered readers e writers melhoram significativamente a performance.
- 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.