Protocolo Binário em Zig — Tutorial Passo a Passo

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

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

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.