Sockets – Internet Domains Data Representation, Marshalling & Text Encoding

 

Chapter 59: Sockets – Internet Domains
Part 3 of 4  |  Data Representation, Marshalling & Text Encoding
Marshalling
Standard Encoding
XDR / ASN.1
Binary Standards
Text Protocol
Simple Approach

The Problem: Different Machines, Different Representations

Byte order (endianness) is just one of the differences between computer architectures. There are more issues when you try to send structured data (like a C struct) across a network between two different machines:

  • A long might be 32-bit on one system and 64-bit on another.
  • A struct might have different padding/alignment bytes on different compilers.
  • Floating-point formats can differ.
  • Character encodings can differ (ASCII vs EBCDIC on old IBM systems).

This chapter covers two strategies to handle this: formal marshalling standards and a simpler newline-delimited text encoding.

Key Terms

Marshalling Serialization XDR ASN.1-BER CORBA XML Text Protocol Newline Delimiter Heterogeneous Systems Struct Padding telnet debugging

59.3a – Why Sending Raw Structs Across Networks Fails

Consider this simple struct:

struct Message {
    short  type;   /* 2 bytes */
    int    value;  /* 4 bytes */
    char   name[8];
};

On System A (32-bit compiler, 4-byte aligned):

type
2 bytes
padding
2 bytes
value
4 bytes
name
8 bytes
Total: 16 bytes (2+2 padding+4+8)

On System B (packed struct, no alignment padding):

type
2 bytes
value
4 bytes
name
8 bytes
Total: 14 bytes (no padding)

If System A sends 16 bytes and System B reads a 14-byte struct, the field offsets don’t match — the value field on System B reads into the padding bytes. The data is corrupted even though the network delivered it perfectly.

Also: if one machine is little-endian and the other is big-endian, the int value will be interpreted backwards (the byte order problem from Part 2).

59.3b – Marshalling: The Formal Solution

Marshalling (also called serialization) means encoding data into a platform-independent standard format before sending, and decoding it on the receiver side. Both sides agree on the format — so byte order, field sizes, and padding are all handled by the standard.

Sender
C struct in host memory
Marshal / Encode
Convert to standard format
(XDR, JSON, protobuf…)
Network
Standard bytes transmitted
Receiver
Unmarshal → C struct

Common marshalling standards:

Standard Full Name Notes
XDR External Data Representation (RFC 1014) Used in NFS; big-endian, fixed-size types
ASN.1-BER Abstract Syntax Notation 1 Used in SNMP, X.509 certificates; self-describing with type+length+value tags
CORBA Common Object Request Broker Architecture Object-oriented RPC; uses CDR (Common Data Representation)
XML eXtensible Markup Language Human-readable text; used in SOAP, REST APIs
JSON JavaScript Object Notation Widely used in REST APIs; human-readable text
Protocol Buffers Google’s protobuf Binary, compact, fast; needs .proto schema file

Each standard defines a fixed format for every data type: byte order, number of bits, and how to tag each field with its type and length.

59.3c – The Simple Approach: Newline-Delimited Text

Instead of binary marshalling, many protocols simply encode all data as ASCII text, with fields separated by newlines (or spaces/commas).

Famous examples of text-based protocols include: HTTP/1.1, SMTP, FTP, POP3, and IMAP. Their human-readable format makes them easy to debug.

Sender
Formats data as text
"42\n"
Network
ASCII bytes transmitted
0x34 0x32 0x0A
Receiver
Reads line, calls atoi()
int val = 42;

Advantages of text encoding:

  • Works the same on any architecture — ASCII is portable
  • Easy to debug using telnet or nc (netcat)
  • Human-readable — you can inspect traffic easily

Disadvantages:

  • Less efficient than binary (a 32-bit integer takes up to 10 bytes as text vs 4 bytes binary)
  • Requires parsing (converting strings to numbers)
  • Not suitable for binary data (images, audio) without encoding (e.g., base64)

Debugging with telnet example (testing an HTTP server):

$ telnet www.example.com 80
Trying 93.184.216.34...
Connected to www.example.com.
GET / HTTP/1.0
Host: www.example.com

HTTP/1.0 200 OK
Content-Type: text/html
...

You type text lines, the server responds. This works because HTTP is a text protocol. You can do the same with SMTP (port 25) or POP3 (port 110).

59.3d – Sending Integers as Text Over a Socket

Here is a practical example: a client sends an integer to a server as a text line:

/* ===== SERVER SIDE ===== */
/* Reads an integer sent as a text line */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

#define BUF_SIZE 128
#define PORT     9000

/* Read a '\n'-terminated line from fd into buf (max n-1 chars + null) */
ssize_t read_line(int fd, char *buf, size_t n) {
    ssize_t total = 0;
    char c;

    while (total < (ssize_t)(n - 1)) {
        ssize_t rc = read(fd, &c, 1);
        if (rc == -1) return -1;
        if (rc == 0) break;   /* EOF */
        buf[total++] = c;
        if (c == '\n') break;
    }
    buf[total] = '\0';
    return total;
}

int main() {
    int sfd, cfd;
    struct sockaddr_in srv_addr;
    char buf[BUF_SIZE];

    sfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family      = AF_INET;
    srv_addr.sin_port        = htons(PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(sfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    listen(sfd, 5);

    printf("Server waiting on port %d\n", PORT);

    cfd = accept(sfd, NULL, NULL);

    /* Read a text line */
    read_line(cfd, buf, BUF_SIZE);
    printf("Received text: %s", buf);

    /* Convert text to integer — no byte order issues! */
    int value = atoi(buf);
    printf("Parsed integer: %d\n", value);

    close(cfd);
    close(sfd);
    return 0;
}
/* ===== CLIENT SIDE ===== */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 9000

int main() {
    int sfd;
    struct sockaddr_in srv_addr;
    char buf[64];
    int value = 12345;

    sfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port   = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &srv_addr.sin_addr);

    connect(sfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

    /* Send integer as text — portable across any architecture */
    snprintf(buf, sizeof(buf), "%d\n", value);
    write(sfd, buf, strlen(buf));
    printf("Sent: %s", buf);

    close(sfd);
    return 0;
}

Notice the client uses snprintf(buf, sizeof(buf), "%d\n", value) — it converts the integer to a decimal text string. The server uses atoi() to convert it back. No byte-order conversion needed at all!

59.3e – Sending Binary Structs Safely (Packed Structs)

If you must send binary structs (for performance), you can minimize the padding issue with compiler-specific packing directives, combined with explicit fixed-width types and manual byte-order conversion:

#include <stdint.h>
#include <arpa/inet.h>

/* Use fixed-width types — no ambiguity about sizes */
/* Use __attribute__((packed)) to remove compiler padding */

struct __attribute__((packed)) NetMessage {
    uint8_t  type;     /* 1 byte — no byte order issue */
    uint16_t port;     /* 2 bytes — must use htons/ntohs */
    uint32_t value;    /* 4 bytes — must use htonl/ntohl */
};

/* Sender: marshal the struct */
void send_message(int fd, uint8_t type, uint16_t port, uint32_t value) {
    struct NetMessage msg;
    msg.type  = type;
    msg.port  = htons(port);    /* Convert to network order */
    msg.value = htonl(value);   /* Convert to network order */
    write(fd, &msg, sizeof(msg));
}

/* Receiver: unmarshal the struct */
void recv_message(int fd) {
    struct NetMessage msg;
    read(fd, &msg, sizeof(msg));
    printf("type  = %u\n", msg.type);
    printf("port  = %u\n", ntohs(msg.port));   /* Convert back */
    printf("value = %u\n", ntohl(msg.value));  /* Convert back */
}

Important: __attribute__((packed)) is a GCC extension. Packed structs can cause unaligned memory accesses that hurt performance or crash on some architectures. This is why formal marshalling or text encoding is usually preferred for real-world protocols.

Interview Questions – Part 3

Q1. What is marshalling in network programming?

Marshalling is the process of encoding data into a platform-independent standard format before sending it over a network. The receiver decodes (unmarshals) it using the same standard. It solves problems like different endianness, different data type sizes, and struct padding across different systems.

Q2. Why can’t you just send a C struct directly over a socket to another machine?

Several problems: (1) Different endianness means multi-byte integers are stored in different byte orders. (2) Different compilers and architectures insert different amounts of padding between struct fields. (3) Type sizes like long may be 32-bit on one machine and 64-bit on another. The receiver would misinterpret the field values.

Q3. Name two formal marshalling standards used in network protocols.

XDR (External Data Representation, RFC 1014) — used in NFS. ASN.1-BER — used in SNMP and SSL/TLS certificate encoding. Others include CORBA’s CDR, XML, JSON, and Protocol Buffers.

Q4. What is the advantage of using newline-delimited text encoding for network protocols?

Text encoding is portable — ASCII numbers look the same on any architecture. It is easy to debug using tools like telnet or nc because you can read and type the protocol manually. This is why HTTP, SMTP, and FTP were designed as text protocols.

Q5. How would you send the integer 12345 from a client to a server in a portable way using a text protocol?

Use snprintf(buf, sizeof(buf), "%d\n", 12345) on the sender to convert the integer to a decimal text string, then write() that string to the socket. On the receiver, use read_line() to get the string and atoi() to convert it back to an integer. No byte-order conversion is needed.

Q6. What is struct padding and why does it cause problems in network communication?

Compilers insert unused padding bytes between struct fields to align them to memory boundaries (e.g., a 4-byte int starts at a multiple of 4 bytes). Different compilers and architectures may insert different amounts of padding. If you send the raw struct bytes, the receiver may read fields at wrong offsets because their compiler placed padding differently.

Next: The readLine() Utility Function

Learn how to implement and use the readLine() helper function for reading newline-terminated messages from sockets.

Part 4 → readLine() Function ← Part 2: Byte Order

Leave a Reply

Your email address will not be published. Required fields are marked *