Protocolo Binário em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir uma implementação de protocolo binário em Zig com serialização e deserialização de mensagens estruturadas, checksums para integridade e suporte a versionamento.
O Que Vamos Construir
Nosso protocolo binário vai:
- Definir um formato de mensagem com header, tipo, payload e checksum
- Serializar structs Zig para bytes e vice-versa
- Incluir CRC32 para verificação de integridade
- Suportar múltiplos tipos de mensagem (login, dados, heartbeat, erro)
- Funcionar sobre TCP ou arquivo
- Incluir versionamento para compatibilidade futura
Por Que Este Projeto?
Protocolos binários são usados em games, sistemas financeiros, IoT e qualquer aplicação que precise de comunicação eficiente. Diferente de JSON/XML, protocolos binários são compactos e rápidos de parsear. Em Zig, o controle sobre layout de memória e endianness é direto e seguro.
Pré-requisitos
- Zig 0.13+ instalado (guia de instalação)
- Conhecimento de I/O binário
- Familiaridade com structs e packed structs
Passo 1: Estrutura do Projeto
mkdir protocolo-binario
cd protocolo-binario
zig init
Passo 2: Definição do Protocolo
const std = @import("std");
const io = std.io;
const mem = std.mem;
const fmt = std.fmt;
const hash = std.hash;
/// Número mágico para identificar mensagens do nosso protocolo.
const MAGIC: u32 = 0x5A494742; // "ZIGB"
const VERSAO_PROTOCOLO: u8 = 1;
/// Tipos de mensagem suportados.
const TipoMensagem = enum(u8) {
handshake = 0x01,
handshake_ack = 0x02,
dados = 0x10,
heartbeat = 0x20,
heartbeat_ack = 0x21,
erro = 0xFE,
desconectar = 0xFF,
pub fn nome(self: TipoMensagem) []const u8 {
return switch (self) {
.handshake => "HANDSHAKE",
.handshake_ack => "HANDSHAKE_ACK",
.dados => "DADOS",
.heartbeat => "HEARTBEAT",
.heartbeat_ack => "HEARTBEAT_ACK",
.erro => "ERRO",
.desconectar => "DESCONECTAR",
};
}
};
/// Header de cada mensagem (12 bytes).
/// Layout fixo para parsing rápido.
const MensagemHeader = struct {
magic: u32, // identificador do protocolo
versao: u8, // versão do protocolo
tipo: TipoMensagem, // tipo da mensagem
seq: u16, // número de sequência
payload_len: u32, // tamanho do payload em bytes
};
const HEADER_SIZE = 12;
/// Mensagem completa do protocolo.
const Mensagem = struct {
header: MensagemHeader,
payload: []const u8,
checksum: u32, // CRC32 do payload
pub fn tipoStr(self: *const Mensagem) []const u8 {
return self.header.tipo.nome();
}
};
Passo 3: Serialização
/// Serializa um header em bytes big-endian.
fn serializarHeader(header: MensagemHeader, buf: *[HEADER_SIZE]u8) void {
// Magic (4 bytes)
buf[0] = @intCast((header.magic >> 24) & 0xFF);
buf[1] = @intCast((header.magic >> 16) & 0xFF);
buf[2] = @intCast((header.magic >> 8) & 0xFF);
buf[3] = @intCast(header.magic & 0xFF);
// Versão (1 byte)
buf[4] = header.versao;
// Tipo (1 byte)
buf[5] = @intFromEnum(header.tipo);
// Seq (2 bytes)
buf[6] = @intCast((header.seq >> 8) & 0xFF);
buf[7] = @intCast(header.seq & 0xFF);
// Payload length (4 bytes)
buf[8] = @intCast((header.payload_len >> 24) & 0xFF);
buf[9] = @intCast((header.payload_len >> 16) & 0xFF);
buf[10] = @intCast((header.payload_len >> 8) & 0xFF);
buf[11] = @intCast(header.payload_len & 0xFF);
}
/// Deserializa um header de bytes big-endian.
fn deserializarHeader(buf: *const [HEADER_SIZE]u8) !MensagemHeader {
const magic = @as(u32, buf[0]) << 24 | @as(u32, buf[1]) << 16 |
@as(u32, buf[2]) << 8 | buf[3];
if (magic != MAGIC) return error.MagicInvalido;
return .{
.magic = magic,
.versao = buf[4],
.tipo = @enumFromInt(buf[5]),
.seq = @as(u16, buf[6]) << 8 | buf[7],
.payload_len = @as(u32, buf[8]) << 24 | @as(u32, buf[9]) << 16 |
@as(u32, buf[10]) << 8 | buf[11],
};
}
/// Calcula o CRC32 de um payload.
fn calcularChecksum(payload: []const u8) u32 {
return hash.Crc32.hash(payload);
}
/// Serializa uma mensagem completa em um buffer.
fn serializarMensagem(msg: Mensagem, buf: []u8) !usize {
if (buf.len < HEADER_SIZE + msg.payload.len + 4) return error.BufferPequeno;
var header_buf: [HEADER_SIZE]u8 = undefined;
serializarHeader(msg.header, &header_buf);
@memcpy(buf[0..HEADER_SIZE], &header_buf);
@memcpy(buf[HEADER_SIZE .. HEADER_SIZE + msg.payload.len], msg.payload);
// Checksum (4 bytes no final)
const checksum = calcularChecksum(msg.payload);
const pos = HEADER_SIZE + msg.payload.len;
buf[pos] = @intCast((checksum >> 24) & 0xFF);
buf[pos + 1] = @intCast((checksum >> 16) & 0xFF);
buf[pos + 2] = @intCast((checksum >> 8) & 0xFF);
buf[pos + 3] = @intCast(checksum & 0xFF);
return HEADER_SIZE + msg.payload.len + 4;
}
/// Deserializa uma mensagem completa de um buffer.
fn deserializarMensagem(buf: []const u8) !Mensagem {
if (buf.len < HEADER_SIZE + 4) return error.BufferPequeno;
const header_buf: *const [HEADER_SIZE]u8 = buf[0..HEADER_SIZE];
const header = try deserializarHeader(header_buf);
const payload_fim = HEADER_SIZE + header.payload_len;
if (buf.len < payload_fim + 4) return error.BufferPequeno;
const payload = buf[HEADER_SIZE..payload_fim];
// Verificar checksum
const checksum_recebido = @as(u32, buf[payload_fim]) << 24 |
@as(u32, buf[payload_fim + 1]) << 16 |
@as(u32, buf[payload_fim + 2]) << 8 |
buf[payload_fim + 3];
const checksum_calculado = calcularChecksum(payload);
if (checksum_recebido != checksum_calculado) return error.ChecksumInvalido;
return .{
.header = header,
.payload = payload,
.checksum = checksum_recebido,
};
}
Passo 4: Tipos de Payload
/// Payload de handshake.
const HandshakePayload = struct {
nome_cliente: [32]u8,
nome_len: u8,
versao_cliente: u16,
pub fn criar(nome: []const u8, versao: u16) HandshakePayload {
var p = HandshakePayload{
.nome_cliente = [_]u8{0} ** 32,
.nome_len = @intCast(@min(nome.len, 32)),
.versao_cliente = versao,
};
@memcpy(p.nome_cliente[0..p.nome_len], nome[0..p.nome_len]);
return p;
}
pub fn serializar(self: *const HandshakePayload, buf: []u8) usize {
buf[0] = self.nome_len;
@memcpy(buf[1 .. 1 + self.nome_len], self.nome_cliente[0..self.nome_len]);
const pos = 1 + self.nome_len;
buf[pos] = @intCast(self.versao_cliente >> 8);
buf[pos + 1] = @intCast(self.versao_cliente & 0xFF);
return pos + 2;
}
pub fn deserializar(buf: []const u8) !HandshakePayload {
if (buf.len < 3) return error.PayloadIncompleto;
var p = HandshakePayload{
.nome_cliente = [_]u8{0} ** 32,
.nome_len = buf[0],
.versao_cliente = 0,
};
if (buf.len < 1 + p.nome_len + 2) return error.PayloadIncompleto;
@memcpy(p.nome_cliente[0..p.nome_len], buf[1 .. 1 + p.nome_len]);
const pos = 1 + p.nome_len;
p.versao_cliente = @as(u16, buf[pos]) << 8 | buf[pos + 1];
return p;
}
};
/// Payload de dados genérico com tipo e valor.
const DadosPayload = struct {
tipo_dado: u8,
dados: [1024]u8,
dados_len: u16,
pub fn criarTexto(texto: []const u8) DadosPayload {
var p = DadosPayload{
.tipo_dado = 1, // texto
.dados = [_]u8{0} ** 1024,
.dados_len = @intCast(@min(texto.len, 1024)),
};
@memcpy(p.dados[0..p.dados_len], texto[0..p.dados_len]);
return p;
}
};
/// Construtor de mensagens com número de sequência automático.
const MensagemBuilder = struct {
seq: u16,
pub fn init() MensagemBuilder {
return .{ .seq = 0 };
}
pub fn criar(self: *MensagemBuilder, tipo: TipoMensagem, payload: []const u8) Mensagem {
self.seq += 1;
return .{
.header = .{
.magic = MAGIC,
.versao = VERSAO_PROTOCOLO,
.tipo = tipo,
.seq = self.seq,
.payload_len = @intCast(payload.len),
},
.payload = payload,
.checksum = calcularChecksum(payload),
};
}
};
Passo 5: Demonstração
pub fn main() !void {
const stdout = io.getStdOut().writer();
try stdout.print(
\\
\\ ==========================================
\\ PROTOCOLO BINARIO - Zig
\\ ==========================================
\\ Magic: 0x{X:0>8} ("ZIGB")
\\ Versao: {d}
\\ Header: {d} bytes
\\ ==========================================
\\
\\
, .{ MAGIC, VERSAO_PROTOCOLO, HEADER_SIZE });
var builder = MensagemBuilder.init();
// Criar e serializar diferentes tipos de mensagem
var buf: [4096]u8 = undefined;
// 1. Handshake
var hs_payload: [64]u8 = undefined;
const hs = HandshakePayload.criar("ZigClient", 100);
const hs_len = hs.serializar(&hs_payload);
const msg_hs = builder.criar(.handshake, hs_payload[0..hs_len]);
const hs_serializado = try serializarMensagem(msg_hs, &buf);
try stdout.print(" --- Handshake ---\n", .{});
try stdout.print(" Tipo: {s}\n", .{msg_hs.tipoStr()});
try stdout.print(" Seq: {d}\n", .{msg_hs.header.seq});
try stdout.print(" Payload: {d} bytes\n", .{msg_hs.header.payload_len});
try stdout.print(" Total serializado: {d} bytes\n", .{hs_serializado});
try stdout.print(" Checksum: 0x{X:0>8}\n\n", .{msg_hs.checksum});
// Deserializar e verificar
const msg_recebida = try deserializarMensagem(buf[0..hs_serializado]);
try stdout.print(" Deserializado com sucesso!\n", .{});
try stdout.print(" Tipo: {s}, Seq: {d}\n", .{msg_recebida.tipoStr(), msg_recebida.header.seq});
const hs_recebido = try HandshakePayload.deserializar(msg_recebida.payload);
try stdout.print(" Cliente: {s}, Versao: {d}\n\n", .{
hs_recebido.nome_cliente[0..hs_recebido.nome_len], hs_recebido.versao_cliente,
});
// 2. Mensagem de dados
const dados_texto = "Hello, protocolo binario!";
const msg_dados = builder.criar(.dados, dados_texto);
const dados_serializado = try serializarMensagem(msg_dados, &buf);
try stdout.print(" --- Dados ---\n", .{});
try stdout.print(" Tipo: {s}\n", .{msg_dados.tipoStr()});
try stdout.print(" Payload: \"{s}\"\n", .{dados_texto});
try stdout.print(" Total serializado: {d} bytes\n\n", .{dados_serializado});
// 3. Heartbeat
const msg_hb = builder.criar(.heartbeat, "");
const hb_serializado = try serializarMensagem(msg_hb, &buf);
try stdout.print(" --- Heartbeat ---\n", .{});
try stdout.print(" Tipo: {s}\n", .{msg_hb.tipoStr()});
try stdout.print(" Total: {d} bytes (somente header + checksum)\n\n", .{hb_serializado});
// Hex dump
try stdout.print(" --- Hex Dump (Handshake) ---\n ", .{});
const hs_data = buf[0..hs_serializado];
for (hs_data, 0..) |b, i| {
try stdout.print("{X:0>2} ", .{b});
if ((i + 1) % 16 == 0) try stdout.print("\n ", .{});
}
try stdout.print("\n", .{});
}
Testes
test "serializar e deserializar header" {
const header = MensagemHeader{
.magic = MAGIC,
.versao = 1,
.tipo = .dados,
.seq = 42,
.payload_len = 100,
};
var buf: [HEADER_SIZE]u8 = undefined;
serializarHeader(header, &buf);
const deserialized = try deserializarHeader(&buf);
try std.testing.expectEqual(MAGIC, deserialized.magic);
try std.testing.expectEqual(@as(u8, 1), deserialized.versao);
try std.testing.expectEqual(TipoMensagem.dados, deserialized.tipo);
try std.testing.expectEqual(@as(u16, 42), deserialized.seq);
try std.testing.expectEqual(@as(u32, 100), deserialized.payload_len);
}
test "mensagem completa - roundtrip" {
var builder = MensagemBuilder.init();
const payload = "teste de payload";
const msg = builder.criar(.dados, payload);
var buf: [1024]u8 = undefined;
const len = try serializarMensagem(msg, &buf);
const msg_back = try deserializarMensagem(buf[0..len]);
try std.testing.expectEqual(TipoMensagem.dados, msg_back.header.tipo);
try std.testing.expectEqualStrings(payload, msg_back.payload);
}
test "checksum invalido" {
var builder = MensagemBuilder.init();
const msg = builder.criar(.dados, "dados");
var buf: [1024]u8 = undefined;
const len = try serializarMensagem(msg, &buf);
// Corromper o checksum
buf[len - 1] ^= 0xFF;
const result = deserializarMensagem(buf[0..len]);
try std.testing.expectError(error.ChecksumInvalido, result);
}
test "magic invalido" {
var buf = [_]u8{0} ** 16;
const result = deserializarHeader(buf[0..HEADER_SIZE]);
try std.testing.expectError(error.MagicInvalido, result);
}
test "handshake payload roundtrip" {
const original = HandshakePayload.criar("TestClient", 200);
var buf: [64]u8 = undefined;
const len = original.serializar(&buf);
const decoded = try HandshakePayload.deserializar(buf[0..len]);
try std.testing.expectEqual(@as(u16, 200), decoded.versao_cliente);
}
Compilando e Executando
zig build run
zig build test
Conceitos Aprendidos
- Serialização binária com controle de endianness
- Checksums (CRC32) para verificação de integridade
- Enums com valores para tipos de mensagem
- Versionamento de protocolo para compatibilidade
- Packed structs e layout de memória
Próximos Passos
- Explore a stdlib de hash para checksums
- Veja o DNS Resolver para outro protocolo binário
- Construa a Máquina Virtual com bytecode
- Consulte I/O binário na stdlib