Neste primeiro artigo da série Desenvolvimento Web com Zig, vamos construir um servidor HTTP funcional do zero. Sem frameworks, sem dependências externas — apenas Zig puro e sua biblioteca padrão.
Entendendo o Modelo de Rede do Zig
Zig oferece acesso direto a sockets POSIX através de std.posix e uma abstração de nível mais alto com std.net e std.http. Vamos começar pelo nível mais baixo e subir progressivamente.
Passo 1: Um Servidor TCP Simples
Antes de HTTP, precisamos de TCP. Vamos criar um servidor que aceita conexões:
const std = @import("std");
const net = std.net;
pub fn main() !void {
// Criar um socket do servidor
const endereco = net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
var servidor = try endereco.listen(.{
.reuse_address = true,
});
defer servidor.deinit();
std.debug.print("Servidor TCP ouvindo em 127.0.0.1:8080\n", .{});
// Loop de aceitação de conexões
while (true) {
var conexao = try servidor.accept();
defer conexao.stream.close();
std.debug.print("Nova conexão de {}\n", .{conexao.address});
// Ler dados do cliente
var buffer: [1024]u8 = undefined;
const bytes_lidos = try conexao.stream.read(&buffer);
if (bytes_lidos > 0) {
std.debug.print("Recebido: {s}\n", .{buffer[0..bytes_lidos]});
// Enviar resposta
const resposta = "Olá do servidor Zig!\n";
_ = try conexao.stream.write(resposta);
}
}
}
Teste com: echo "Olá" | nc localhost 8080
Passo 2: Servidor HTTP com std.http.Server
Agora vamos usar a abstração HTTP de Zig para criar um servidor HTTP real:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Configurar endereço
const endereco = std.net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
// Criar servidor HTTP
var servidor = try endereco.listen(.{
.reuse_address = true,
});
defer servidor.deinit();
std.debug.print("Servidor HTTP rodando em http://127.0.0.1:8080\n", .{});
// Loop principal
while (true) {
// Aceitar conexão
var conexao = try servidor.accept();
defer conexao.stream.close();
// Criar servidor HTTP para esta conexão
var buf: [8192]u8 = undefined;
var http_server = std.http.Server.init(conexao, &buf);
// Ler a requisição
var request = http_server.receiveHead() catch |err| {
std.debug.print("Erro ao ler requisição: {}\n", .{err});
continue;
};
// Processar a requisição
tratarRequisicao(allocator, &request) catch |err| {
std.debug.print("Erro ao processar requisição: {}\n", .{err});
};
}
}
fn tratarRequisicao(allocator: std.mem.Allocator, request: *std.http.Server.Request) !void {
_ = allocator;
const metodo = request.head.method;
const caminho = request.head.target;
std.debug.print("{s} {s}\n", .{ @tagName(metodo), caminho });
// Corpo da resposta
const corpo =
\\<!DOCTYPE html>
\\<html lang="pt-BR">
\\<head>
\\ <meta charset="UTF-8">
\\ <title>Servidor Zig</title>
\\</head>
\\<body>
\\ <h1>Olá do Zig!</h1>
\\ <p>Este servidor foi construído com Zig puro.</p>
\\</body>
\\</html>
;
// Enviar resposta
try request.respond(corpo, .{
.extra_headers = &.{
.{ .name = "content-type", .value = "text/html; charset=utf-8" },
},
});
}
Passo 3: Servindo Diferentes Tipos de Conteúdo
Vamos expandir o servidor para servir diferentes formatos:
const std = @import("std");
const ContentType = enum {
html,
json,
text,
not_found,
fn header(self: ContentType) []const u8 {
return switch (self) {
.html => "text/html; charset=utf-8",
.json => "application/json",
.text => "text/plain; charset=utf-8",
.not_found => "text/plain; charset=utf-8",
};
}
};
fn rotear(caminho: []const u8) struct { corpo: []const u8, tipo: ContentType, status: std.http.Status } {
if (std.mem.eql(u8, caminho, "/")) {
return .{
.corpo =
\\<!DOCTYPE html>
\\<html><body>
\\<h1>Bem-vindo ao Zig Web Server</h1>
\\<ul>
\\ <li><a href="/api/status">Status da API</a></li>
\\ <li><a href="/sobre">Sobre</a></li>
\\</ul>
\\</body></html>
,
.tipo = .html,
.status = .ok,
};
}
if (std.mem.eql(u8, caminho, "/api/status")) {
return .{
.corpo =
\\{"status": "online", "versao": "1.0.0", "linguagem": "Zig"}
,
.tipo = .json,
.status = .ok,
};
}
if (std.mem.eql(u8, caminho, "/sobre")) {
return .{
.corpo =
\\<!DOCTYPE html>
\\<html><body>
\\<h1>Sobre</h1>
\\<p>Servidor HTTP construído com Zig - Sem dependências externas!</p>
\\<p><a href="/">Voltar</a></p>
\\</body></html>
,
.tipo = .html,
.status = .ok,
};
}
return .{
.corpo = "404 - Página não encontrada",
.tipo = .not_found,
.status = .not_found,
};
}
fn tratarRequisicao(request: *std.http.Server.Request) !void {
const metodo = request.head.method;
const caminho = request.head.target;
std.debug.print("[{s}] {s}\n", .{ @tagName(metodo), caminho });
const rota = rotear(caminho);
try request.respond(rota.corpo, .{
.status = rota.status,
.extra_headers = &.{
.{ .name = "content-type", .value = rota.tipo.header() },
.{ .name = "server", .value = "Zig/1.0" },
},
});
}
pub fn main() !void {
const endereco = std.net.Address.parseIp("127.0.0.1", 8080) catch unreachable;
var servidor = try endereco.listen(.{ .reuse_address = true });
defer servidor.deinit();
std.debug.print("Servidor rodando em http://127.0.0.1:8080\n", .{});
std.debug.print("Rotas disponíveis:\n", .{});
std.debug.print(" GET / - Página inicial\n", .{});
std.debug.print(" GET /api/status - Status da API (JSON)\n", .{});
std.debug.print(" GET /sobre - Página sobre\n", .{});
while (true) {
var conexao = try servidor.accept();
defer conexao.stream.close();
var buf: [8192]u8 = undefined;
var http = std.http.Server.init(conexao, &buf);
var request = http.receiveHead() catch continue;
tratarRequisicao(&request) catch |err| {
std.debug.print("Erro: {}\n", .{err});
};
}
}
Passo 4: Lendo o Corpo da Requisição
Para POST e PUT, precisamos ler o corpo da requisição:
const std = @import("std");
fn lerCorpo(allocator: std.mem.Allocator, request: *std.http.Server.Request) ![]u8 {
var reader = try request.reader();
const corpo = try reader.readAllAlloc(allocator, 1024 * 1024); // Máx 1MB
return corpo;
}
fn tratarPost(allocator: std.mem.Allocator, request: *std.http.Server.Request) !void {
const corpo = try lerCorpo(allocator, request);
defer allocator.free(corpo);
std.debug.print("Corpo recebido ({d} bytes): {s}\n", .{ corpo.len, corpo });
const resposta = try std.fmt.allocPrint(
allocator,
"{{\"recebido\": true, \"tamanho\": {d}}}",
.{corpo.len},
);
defer allocator.free(resposta);
try request.respond(resposta, .{
.extra_headers = &.{
.{ .name = "content-type", .value = "application/json" },
},
});
}
Teste com: curl -X POST -d '{"nome": "Zig"}' http://localhost:8080/api/dados
Entendendo o Modelo de Concorrência
Nosso servidor atual é single-threaded — processa uma requisição por vez. Nos próximos artigos, vamos explorar como torná-lo concorrente. Por enquanto, este modelo é suficiente para entender os fundamentos.
Exercício Prático
Tente expandir o servidor com:
- Uma rota
/api/horaque retorna a hora atual em JSON - Uma rota
/api/echoque retorna o corpo da requisição POST - Headers customizados como
X-Powered-By: Zig
Próximos Passos
No próximo artigo, vamos implementar um sistema de roteamento robusto com handlers modulares, parâmetros de URL e separação de responsabilidades.
Referências
- Programação de Sistemas com Zig — Networking em nível mais baixo
- Masterclass de Memória — Gerenciamento de memória para servidores
- Referência std.net — Documentação da biblioteca de networking