Standard Encoding
Binary Standards
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
longmight be 32-bit on one system and 64-bit on another. - A
structmight 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.
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).
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.
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
telnetornc(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).
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!
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.
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.
