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.
Let’s store the 32-bit integer 0x01020304 in memory. It has 4 bytes: 01, 02, 03, 04. Where does byte 01 go?
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.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 |
#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.
#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);
#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
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.
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().
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.
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.
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.
