Network Byte Order & Conversion htons, htonl, ntohs, ntohl Explained

 

Chapter 58 โ€“ Network Byte Order & Conversion
Part 6 of 8 ย |ย  htons, htonl, ntohs, ntohl Explained
๐Ÿ”„ Big Endian
๐Ÿ”„ Little Endian
๐ŸŒ Network Byte Order
๐Ÿ“ htons/ntohl

Why Byte Order Matters in Networking

Different CPU architectures store multi-byte integers differently in memory. An Intel x86 CPU stores them in little-endian order (least significant byte first). Some older network hardware and protocols use big-endian (most significant byte first).

If a little-endian machine sends the number 0x0050 (decimal 80 = port 80) directly, a big-endian machine reads 0x5000 (port 20480) โ€” completely wrong! The fix: a universal network byte order standard. All network protocols use big-endian. Before sending, you convert host byte order โ†’ network byte order. After receiving, you convert network byte order โ†’ host byte order.

Key Terms:

Big Endian Little Endian Network Byte Order Host Byte Order htons() htonl() ntohs() ntohl()

๐Ÿ”„ Big Endian vs Little Endian โ€” Visualized

Let’s store the 32-bit integer 0x01020304 in memory. It has 4 bytes: 01, 02, 03, 04. Where does byte 01 go?

Big Endian (= Network Byte Order)
Memory Addr Value (byte) Meaning
addr + 0 0x01 Most significant byte first
addr + 1 0x02
addr + 2 0x03
addr + 3 0x04 Least significant byte last
โœ… Used by: Network protocols, Motorola 68000, SPARC, ARM (BE mode)
Little Endian (x86 / ARM default)
Memory Addr Value (byte) Meaning
addr + 0 0x04 Least significant byte first!
addr + 1 0x03
addr + 2 0x02
addr + 3 0x01 Most significant byte last
โ„น๏ธ Used by: x86, x86-64, ARM (LE mode) โ€” most modern PCs and Linux systems
โš ๏ธ The Problem: If a little-endian machine sends port = 80 (0x0050) as raw bytes, it sends 0x50 then 0x00. A big-endian receiver interprets this as 0x5000 = port 20480. Disaster! That is why we always convert port numbers and IP addresses to network byte order before putting them in structs.

๐Ÿ“ The Four Conversion Functions

POSIX provides four functions to convert between host and network byte order. The names follow a pattern: host to network (or reverse), for short (16-bit) or long (32-bit).

Function Stands For Direction Size Use For
htons() Host To Network Short Host โ†’ Network 16-bit Port numbers before storing in struct
htonl() Host To Network Long Host โ†’ Network 32-bit IPv4 addresses (INADDR_LOOPBACK etc.)
ntohs() Network To Host Short Network โ†’ Host 16-bit Read port from received struct
ntohl() Network To Host Long Network โ†’ Host 32-bit Read IPv4 address from received struct
Header to include: #include <arpa/inet.h> (also available via #include <netinet/in.h>)

On big-endian machines: these functions are no-ops (they do nothing) because host byte order already IS network byte order. The code is still correct โ€” always use these functions for portability.

๐Ÿ’ป Code: Byte Order Conversion in Practice
#include <stdio.h>
#include <arpa/inet.h>
#include <stdint.h>

int main(void) {
    /* -------- Port number example -------- */
    uint16_t host_port    = 8080;
    uint16_t network_port = htons(host_port);    /* convert BEFORE storing */

    printf("Host port:    %u (0x%04X)\n", host_port,    host_port);
    printf("Network port: %u (0x%04X)\n", network_port, network_port);
    /* On little-endian x86:
       Host port:    8080 (0x1F90)
       Network port: 36895 (0x901F)   <-- bytes swapped! */

    /* -------- IP address example -------- */
    uint32_t host_ip    = INADDR_LOOPBACK;    /* 0x7F000001 in host order */
    uint32_t network_ip = htonl(host_ip);

    printf("Host IP:    0x%08X\n", host_ip);
    printf("Network IP: 0x%08X\n", network_ip);

    /* -------- Receiving: network โ†’ host -------- */
    /* Suppose you received a port in network byte order: */
    uint16_t received_port_NBO = 0x901F;          /* came from wire */
    uint16_t readable_port     = ntohs(received_port_NBO);
    printf("Received port (readable): %u\n", readable_port);  /* 8080 */

    return 0;
}
/* Real socket usage โ€” ALWAYS use htons() for port: */

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port   = htons(8080);       /* โ† MUST convert! */
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);   /* inet_pton already stores in NBO */

/* When you get back a peer address after accept() or recvfrom(): */
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int conn = accept(listenfd, (struct sockaddr *)&peer, &peer_len);

/* Read the peer's port and IP in human-readable form: */
uint16_t peer_port = ntohs(peer.sin_port);    /* โ† convert back! */
char peer_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer.sin_addr, peer_ip, sizeof(peer_ip));
printf("Client: %s:%u\n", peer_ip, peer_port);

๐Ÿ” How to Detect Your System’s Byte Order
#include <stdio.h>
#include <stdint.h>

int is_little_endian(void) {
    uint16_t n = 0x0001;
    /* If little-endian: lowest address holds 0x01 (least significant byte) */
    return *(uint8_t *)&n == 0x01;
}

int main(void) {
    if (is_little_endian()) {
        printf("This system is LITTLE-ENDIAN (e.g., x86)\n");
        printf("โ†’ htons/htonl will swap bytes\n");
    } else {
        printf("This system is BIG-ENDIAN\n");
        printf("โ†’ htons/htonl are no-ops (already network byte order)\n");
    }
    return 0;
}
/* You can also use compiler macros: */
#include <endian.h>

#if __BYTE_ORDER == __LITTLE_ENDIAN
    printf("Little endian\n");
#elif __BYTE_ORDER == __BIG_ENDIAN
    printf("Big endian\n");
#endif

๐ŸŽฏ Interview Questions โ€” Byte Order
Q1: What is “network byte order” and why is it needed?

A: Network byte order is big-endian (most significant byte first). It was standardized so that machines with different endianness can communicate correctly. Without this standard, a little-endian Intel machine sending port 80 (0x0050) would transmit bytes 0x50 0x00, which a big-endian machine would read as 0x5000 = port 20480. By converting to network byte order before sending and from network byte order after receiving, all machines communicate correctly regardless of hardware.

Q2: What is the difference between htons() and htonl()?

A: Both convert host byte order to network byte order (big-endian). htons() works on 16-bit values (short) โ€” used for port numbers. htonl() works on 32-bit values (long) โ€” used for IPv4 addresses when setting them as integers (e.g., INADDR_LOOPBACK). Note: inet_pton() already stores the address in network byte order, so you don’t call htonl() when using inet_pton().

Q3: What happens if you forget to call htons() when setting a port number?

A: On a little-endian system (x86), the port bytes will be swapped. For example, if you set port 80 without htons(), the kernel interprets it as port 20480 (0x5000 instead of 0x0050). Your server will listen on the wrong port and connections to port 80 will fail. The bug is silent and confusing because no error is reported.

Q4: Is it necessary to call htons() on a big-endian machine?

A: Technically no โ€” on a big-endian machine, host byte order equals network byte order, so htons() is a no-op. However, you should ALWAYS call htons() anyway. The code is portable โ€” it works correctly on any architecture without modification. Skipping it for “optimization” on big-endian breaks portability.

Q5: When should you use ntohs() vs ntohl()?

A: ntohs() when reading 16-bit values from network structures โ€” port numbers from sin_port. ntohl() when reading 32-bit values from network structures โ€” IPv4 addresses as integers from sin_addr.s_addr. If you use inet_ntop() to convert the address to a string, you don’t need ntohl() because inet_ntop handles it.

๐Ÿ“– Chapter 58 Navigation

โ† Part 5: Ports Next: Socket API โ†’

Leave a Reply

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