Setelah socket tersambung, kita perlu mengirim data terstruktur—struct dengan field int, float, string—dari satu mesin ke mesin lain. Masalahnya, representasi memori suatu struct tidak portable: arsitektur yang berbeda punya endianness berbeda, padding berbeda, dan ukuran tipe berbeda. Bab ini membahas bagaimana serialisasi mengkonversi struct ke urutan byte yang portable, lalu bagaimana deserialisasi mengembalikannya. Kita akan menggunakan memcpy untuk menulis field ke buffer dan htonl/htons untuk konversi ke network byte order (big-endian), sehingga data dapat dipertukarkan antara mesin little-endian (x86) dan big-endian.

Mengapa Tidak Bisa Kirim Struct Langsung #

Pertanyaan wajar: mengapa tidak send(fd, &my_struct, sizeof(my_struct), 0)? Jawabannya tiga:

  1. Endianness: arsitektur x86 menyimpan int 4-byte dalam little-endian (byte terendah dulu), sedangkan ARM (big-endian mode), SPARC, dan beberapa mikrokontroler menyimpan big-endian. Pesan biner yang sama akan dibaca sebagai angka berbeda di kedua sisi.
  2. Padding: compiler sering menambahkan byte kosong di antara field untuk alignment. Padding tergantung arsitektur dan opsi compiler—tidak portable.
  3. Ukuran tipe: int bisa 2, 4, atau 8 byte tergantung platform. long berbeda di Linux 64-bit (8 byte) vs Windows 64-bit (4 byte). Pengiriman int mentah bisa menghasilkan overflow atau underflow.

Solusinya: definisikan format wire yang eksplisit (mis. “4 byte unsigned int, big-endian, lalu 4 byte IEEE-754 float, lalu 2 byte unsigned short”), dan gunakan tipe fixed-width (uint32_t, uint16_t) dari <stdint.h>.

Endianness: Host vs Network Order #

Konvensi TCP/IP: semua field 16-bit dan 32-bit di header protokol ditulis dalam network byte order (big-endian). Kernel mengeksekusi kode ini di tingkat hardware, sehingga aplikasi kita juga harus mengikuti konvensi yang sama. Tiga fungsi konversi yang disediakan POSIX:

  • htons(h): host-to-network short (16-bit) — untuk port.
  • htonl(h): host-to-network long (32-bit) — untuk IPv4 address dan uint32.
  • ntohs(n), ntohl(n): kebalikannya.

Pada arsitektur little-endian (x86, ARM little-endian), fungsi-fungsi ini menukar byte. Pada arsitektur big-endian (mis. sambungan SPARC), mereka adalah no-op (tetap sama).

Berikut program yang membandingkan representasi host dan network untuk uint32 dan uint16:

c
/* File: serial2.c - Bandingkan little-endian (host) vs big-endian (network) */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

static void print_hex(const char *label, const unsigned char *buf, size_t n) {
    printf("%-20s: ", label);
    for (size_t i = 0; i < n; i++) printf("%02x ", buf[i]);
    printf("\n");
}

int main(void) {
    uint32_t nilai = 0x12345678;

    /* Little-endian (host order di x86) */
    unsigned char buf_host[4];
    memcpy(buf_host, &nilai, 4);
    print_hex("Host (little-endian)", buf_host, 4);

    /* Big-endian (network order) */
    uint32_t net = htonl(nilai);
    unsigned char buf_net[4];
    memcpy(buf_net, &net, 4);
    print_hex("Network (big-endian)", buf_net, 4);

    /* uint16_t port 8080 = 0x1F90 */
    uint16_t port = 8080;
    unsigned char buf_port_h[2], buf_port_n[2];
    memcpy(buf_port_h, &port, 2);
    uint16_t port_n = htons(port);
    memcpy(buf_port_n, &port_n, 2);
    print_hex("Port host order    ", buf_port_h, 2);
    print_hex("Port network order ", buf_port_n, 2);

    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o serial2 serial2.c
./serial2

Output yang Dihasilkan:

text
Host (little-endian): 78 56 34 12 
Network (big-endian): 12 34 56 78 
Port host order     : 90 1f 
Port network order  : 1f 90 

Pada output di atas, kita melihat bahwa 0x12345678 di host little-endian disimpan sebagai byte 78 56 34 12 (byte terendah dulu), sedangkan di network order menjadi 12 34 56 78 (byte tertinggi dulu). Demikian juga port 8080 (0x1F90) tersimpan sebagai 90 1f di host, 1f 90 di network order.

Serialisasi Struct Pesan #

Sekarang mari kita terapkan ke struct dengan beberapa field berbeda tipe. Struct Pesan memiliki uint32_t id, float suhu, dan uint16_t panjang. Serialisasi menulis setiap field ke buffer secara berurutan dalam format wire yang sudah ditentukan.

c
/* File: serial1.c - Serialisasi struct Pesan ke byte buffer */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

typedef struct {
    uint32_t id;
    float    suhu;       /* suhu dalam Celsius */
    uint16_t panjang;    /* panjang data */
} Pesan;

/* Serialisasi: tulis setiap field ke buffer dalam network byte order */
static size_t serialisasi_pesan(const Pesan *p, unsigned char *buf) {
    size_t off = 0;
    uint32_t id_net = htonl(p->id);
    memcpy(buf + off, &id_net, 4);   off += 4;

    /* Float: encode ke 4 byte IEEE-754 lewat memcpy ke uint32, lalu htonl */
    uint32_t suhu_raw;
    memcpy(&suhu_raw, &p->suhu, 4);
    uint32_t suhu_net = htonl(suhu_raw);
    memcpy(buf + off, &suhu_net, 4); off += 4;

    uint16_t pan_net = htons(p->panjang);
    memcpy(buf + off, &pan_net, 2);   off += 2;

    return off;  /* total byte tertulis */
}

/* Deserialisasi: balik dari buffer ke struct Pesan */
static void deserialisasi_pesan(const unsigned char *buf, Pesan *p) {
    size_t off = 0;
    uint32_t id_net, suhu_net;
    uint16_t pan_net;

    memcpy(&id_net, buf + off, 4);   off += 4;
    memcpy(&suhu_net, buf + off, 4); off += 4;
    memcpy(&pan_net, buf + off, 2);  off += 2;

    p->id       = ntohl(id_net);
    uint32_t suhu_host = ntohl(suhu_net);
    memcpy(&p->suhu, &suhu_host, 4);
    p->panjang  = ntohs(pan_net);
}

int main(void) {
    Pesan asli = { .id = 12345, .suhu = 36.625f, .panjang = 256 };
    printf("Pesan asli    : id=%u suhu=%.3f panjang=%u\n",
           asli.id, asli.suhu, asli.panjang);

    unsigned char buf[64];
    size_t len = serialisasi_pesan(&asli, buf);
    printf("Bytes terkirim: %zu bytes, hex: ", len);
    for (size_t i = 0; i < len; i++) printf("%02x ", buf[i]);
    printf("\n");

    Pesan hasil = {0};
    deserialisasi_pesan(buf, &hasil);
    printf("Pesan decode  : id=%u suhu=%.3f panjang=%u\n",
           hasil.id, hasil.suhu, hasil.panjang);

    /* Verifikasi */
    if (asli.id == hasil.id && asli.suhu == hasil.suhu && asli.panjang == hasil.panjang)
        printf("VERIFIED: data sama persis\n");
    else
        printf("ERROR: data berbeda\n");
    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -o serial1 serial1.c
./serial1

Output yang Dihasilkan:

text
Pesan asli    : id=12345 suhu=36.625 panjang=256
Bytes terkirim: 10 bytes, hex: 00 00 30 39 42 12 80 00 01 00 
Pesan decode  : id=12345 suhu=36.625 panjang=256
VERIFIED: data sama persis

Mari kita telaah baris-baris kode di atas:

  1. Baris 16-29 (serialisasi_pesan): Menulis tiga field ke buffer. id (uint32) dikonversi dengan htonl lalu memcpy 4 byte. suhu (float) tidak bisa langsung dipassing ke htonl karena type-punning lewat pointer bisa melanggar strict aliasing—kita memcpy dulu ke uint32_t suhu_raw, lalu htonl(suhu_raw), lalu memcpy ke buffer.
  2. Baris 31-43 (deserialisasi_pesan): Kebalikan serialisasi—baca raw bytes dari buffer, ntohl/ntohs untuk kembali ke host order, lalu memcpy ke field struct. Float ditangani dengan memcpy dari uint32 host-order ke float.
  3. Baris 47 (Pesan asli = { .id = 12345, ... }): Inisialisasi designated initializer—lebih aman daripada { 12345, 36.625f, 256 } yang bisa salah urutan.
  4. Baris 52 (size_t len = serialisasi_pesan(...)): Buffer tidak punya header panjang; pemanggil bertanggung jawab mengetahui panjang total. Pada produksi, biasanya panjang dikirim sebagai prefix 4-byte di awal buffer (lihat bab Parsing Protokol).
  5. Hex output 00 00 30 39 42 12 80 00 01 00: 4 byte pertama 00 00 30 39 adalah 12345 dalam big-endian (0x3039 = 12345). 4 byte berikutnya 42 12 80 00 adalah IEEE-754 float 36.625 dalam big-endian. 2 byte terakhir 01 00 adalah 256 dalam big-endian (0x0100 = 256).

Float IEEE-754 dan Endianness #

Float di C mengikuti IEEE-754 (single precision = 4 byte, double = 8 byte). Bit-layout float (sign, exponent, mantissa) tidak bergantung pada endianness—yang berubah hanya urutan byte di memori. htonl/ntohl menukar urutan byte, sehingga float bisa dipertukarkan lintas arsitektur dengan aman.