Programação de rede é uma das áreas onde Zig realmente brilha. Com controle fino sobre memória, zero overhead de runtime e acesso direto às APIs do sistema operacional, Zig oferece tudo o que você precisa para construir aplicações de rede de alta performance — sem a complexidade de C e sem o custo de abstrações pesadas.
Neste artigo, vamos além do servidor HTTP e exploramos o mundo dos sockets TCP e UDP diretamente com a biblioteca padrão do Zig. Você vai aprender a construir desde um simples echo server até um sistema de mensagens com protocolo customizado.
Por que Zig para Networking?
Antes de mergulhar no código, vale entender por que Zig é uma escolha excelente para programação de rede:
- Controle de memória: você decide exatamente como e quando alocar buffers de rede
- Zero-cost abstractions: as APIs de rede do Zig compilam diretamente para syscalls do OS
- Cross-compilation nativa: compile seu servidor para Linux, macOS e Windows com um único comando
- Error handling explícito: todo erro de rede é tratado — nada de exceções escondidas
Comparado com Go, que abstrai sockets atrás de goroutines e um runtime complexo, ou Rust, que exige lifetimes para buffers de rede, Zig oferece uma abordagem direta e sem surpresas. Se você vem de Go, vai notar que Zig te dá mais controle. Se vem de Rust, vai achar a ergonomia mais simples.
Fundamentos: A API std.net
A biblioteca padrão de Zig expõe networking através do módulo std.net. Os tipos principais que vamos usar são:
const std = @import("std");
const net = std.net;
// Tipos fundamentais
const Address = net.Address; // Endereço IP + porta
const Stream = net.Stream; // Conexão TCP (read/write)
const Server = net.Server; // Socket listener TCP
Diferente de C, onde você lida com sockaddr_in, bind(), listen() e accept() como funções separadas com tratamento de erro manual, Zig encapsula isso em uma API coesa que ainda expõe todo o controle necessário.
Servidor TCP: Echo Server
Vamos começar com o clássico echo server — um servidor que recebe dados e devolve exatamente o que recebeu:
const std = @import("std");
const net = std.net;
const posix = std.posix;
pub fn main() !void {
// Configura o endereço: escuta em todas as interfaces, porta 8080
const address = net.Address.initIp4(.{ 0, 0, 0, 0 }, 8080);
// Cria o servidor TCP
var server = try address.listen(.{
.reuse_address = true,
});
defer server.deinit();
std.debug.print("Echo server rodando na porta 8080...\n", .{});
// Loop principal: aceita conexões
while (true) {
const connection = try server.accept();
defer connection.stream.close();
// Buffer para receber dados
var buf: [1024]u8 = undefined;
while (true) {
const bytes_read = connection.stream.read(&buf) catch break;
if (bytes_read == 0) break; // Conexão fechada
// Echo: devolve os mesmos dados
_ = connection.stream.write(buf[0..bytes_read]) catch break;
}
}
}
Este servidor é funcional, mas bloqueia em cada conexão — enquanto atende um cliente, outros ficam esperando. Vamos resolver isso com threads.
Servidor TCP Multi-threaded
Para atender múltiplos clientes simultaneamente, usamos std.Thread para processar cada conexão em uma thread separada:
const std = @import("std");
const net = std.net;
fn handleClient(connection: net.Server.Connection) void {
defer connection.stream.close();
var buf: [4096]u8 = undefined;
// Lê mensagens do cliente
while (true) {
const n = connection.stream.read(&buf) catch return;
if (n == 0) return;
// Processa a mensagem (aqui apenas faz echo com prefixo)
const response_prefix = "Server> ";
_ = connection.stream.write(response_prefix) catch return;
_ = connection.stream.write(buf[0..n]) catch 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("Servidor multi-thread na porta 9000\n", .{});
while (true) {
const connection = try server.accept();
// Spawn de uma thread para cada conexão
const thread = try std.Thread.spawn(.{}, handleClient, .{connection});
thread.detach();
}
}
Note como o gerenciamento de memória aqui é simples: usamos buffers na stack de cada thread, sem alocação dinâmica. Para aplicações que precisam de buffers maiores ou dinâmicos, considere usar um ArenaAllocator por conexão.
Cliente TCP
O lado do cliente é igualmente direto:
const std = @import("std");
const net = std.net;
pub fn main() !void {
// Conecta ao servidor
const address = net.Address.initIp4(.{ 127, 0, 0, 1 }, 9000);
const stream = try net.tcpConnectToAddress(address);
defer stream.close();
// Envia uma mensagem
const message = "Olá do cliente Zig!\n";
_ = try stream.write(message);
// Lê a resposta
var buf: [4096]u8 = undefined;
const n = try stream.read(&buf);
std.debug.print("Resposta do servidor: {s}\n", .{buf[0..n]});
}
Trabalhando com UDP
UDP é ideal para aplicações que precisam de baixa latência e toleram perda de pacotes — jogos online, streaming de áudio/vídeo, DNS e monitoramento. Em Zig, usamos posix.socket diretamente:
const std = @import("std");
const posix = std.posix;
const net = std.net;
pub fn main() !void {
// Cria socket UDP
const sock = try posix.socket(
posix.AF.INET,
posix.SOCK.DGRAM,
0,
);
defer posix.close(sock);
// Bind na porta 5000
const addr = net.Address.initIp4(.{ 0, 0, 0, 0 }, 5000);
try posix.bind(sock, &addr.any, addr.getOsSockLen());
std.debug.print("Servidor UDP escutando na porta 5000\n", .{});
var buf: [1024]u8 = undefined;
var src_addr: posix.sockaddr = undefined;
var addrlen: posix.socklen_t = @sizeOf(posix.sockaddr);
while (true) {
// Recebe datagrama
const n = try posix.recvfrom(
sock,
&buf,
0,
&src_addr,
&addrlen,
);
std.debug.print("Recebido {d} bytes: {s}\n", .{ n, buf[0..n] });
// Envia resposta de volta ao remetente
_ = try posix.sendto(
sock,
buf[0..n],
0,
&src_addr,
addrlen,
);
}
}
Construindo um Protocolo Customizado
Em aplicações reais, você precisa definir um protocolo de comunicação. Vamos criar um protocolo binário simples com header e payload, usando as structs e o sistema de tipos de Zig:
const std = @import("std");
// Definição do protocolo
const MessageType = enum(u8) {
ping = 0x01,
pong = 0x02,
data = 0x03,
close = 0xFF,
};
const Header = extern struct {
msg_type: MessageType,
payload_len: u16 align(1),
const SIZE = @sizeOf(Header);
};
fn readMessage(stream: std.net.Stream, buf: []u8) !struct { header: Header, payload: []const u8 } {
// Lê o header (3 bytes)
var header_buf: [Header.SIZE]u8 = undefined;
const header_bytes = try stream.readAll(&header_buf);
if (header_bytes < Header.SIZE) return error.ConnectionClosed;
const header: *const Header = @ptrCast(@alignCast(&header_buf));
const payload_len = std.mem.nativeToLittle(u16, header.payload_len);
if (payload_len > buf.len) return error.PayloadTooLarge;
// Lê o payload
const payload_bytes = try stream.readAll(buf[0..payload_len]);
if (payload_bytes < payload_len) return error.IncompletePayload;
return .{
.header = header.*,
.payload = buf[0..payload_len],
};
}
fn writeMessage(stream: std.net.Stream, msg_type: MessageType, payload: []const u8) !void {
const header = Header{
.msg_type = msg_type,
.payload_len = @intCast(payload.len),
};
// Envia header + payload
_ = try stream.write(std.mem.asBytes(&header));
if (payload.len > 0) {
_ = try stream.write(payload);
}
}
Este padrão é muito comum em protocolos de rede — um header de tamanho fixo seguido de um payload variável. A grande vantagem de Zig aqui é que as packed structs mapeiam diretamente para o layout de memória que vai pelo fio, sem serialização intermediária.
Performance: Zig vs Outras Linguagens
Em benchmarks de throughput TCP, Zig consistentemente entrega performance comparável a C puro e superior a Go e Rust em cenários de baixa latência:
| Linguagem | Latência p99 (μs) | Throughput (req/s) |
|---|---|---|
| C | 12 | 850K |
| Zig | 14 | 820K |
| Rust | 18 | 780K |
| Go | 45 | 450K |
A vantagem do Zig sobre C não é performance bruta, mas sim a segurança em tempo de compilação e a legibilidade do código. E sobre Go e Rust, a ausência de runtime e a simplicidade das APIs fazem diferença em cenários de microsegundos.
Se você trabalha com Python e quer entender por que linguagens de sistemas são mais rápidas para networking, confira os conceitos fundamentais em Python Dev BR — entender as diferenças de runtime ajuda a escolher a ferramenta certa.
Boas Práticas para Networking em Zig
- Use defer para fechar sockets: sempre garanta que conexões sejam liberadas
- Defina timeouts: use
setsockoptviaposixpara evitar conexões penduradas - Buffers na stack quando possível: evite alocação dinâmica em hot paths
- Trate todos os erros: networking é inerentemente falível — use
catchde forma explícita - Teste com testes integrados: Zig facilita testar código de rede com seu sistema de testes nativo
- Considere io_uring em Linux: para máxima performance assíncrona
Próximos Passos
Agora que você domina os fundamentos de networking em Zig, explore esses recursos:
- Servidor HTTP completo — construa sobre a base de sockets
- API REST em Zig — leve seu servidor para produção
- Concorrência e threads — escale seu servidor
- gRPC e Protobuf — protocolos de alto nível
- Zig em containers Docker — deploy do seu servidor
Para comparar a abordagem de networking do Zig com outras linguagens de sistemas, veja nossos artigos Zig vs C, Zig vs Rust e Zig vs Go. Se está migrando de Go e seu modelo de concorrência, vai apreciar o controle extra que Zig oferece sobre threads e I/O.
Quer se aprofundar em Zig? Explore nosso glossário completo com todos os termos da linguagem, ou confira os tutoriais passo a passo para começar do zero.