Networking in C · beginner · ~8 min
- By the end you can explain what endianness is and determine your own machine's byte order at runtime. - By the end you can convert 16- and 32-bit integers between host and network byte order with htons/htonl and ntohs/ntohl. - By the end you can parse a dotted-decimal IPv4 string into an in_addr with inet_pton and turn it back into text with inet_ntop. - By the end you can spot the classic 'I forgot to convert the port' bug and read raw wire bytes to confirm the fix. - By the end you can explain why inet_pton is safer than the legacy inet_addr.
In the sockaddr-in lesson you learned how to fill in a struct sockaddr_in: the sin_family, sin_port, and sin_addr fields that describe an endpoint. This lesson zooms in on a detail that trips up almost every beginner the first time they write a socket: the byte order those numeric fields must be stored in. A port number or an IPv4 address is a multi-byte integer, and different CPUs disagree about which byte to store first. If two machines that disagree exchange a raw integer, they read each other's numbers scrambled.
The fix is a single fixed convention for the wire — network byte order — plus a tiny family of conversion functions. You already know where these values go (into the sockaddr_in you build); here you learn the mandatory conversion step that must happen before you put a port or address on the wire, and the reverse step when you read one back.
Every real TCP/UDP program stores ports and addresses in network byte order — get it wrong and your client silently connects to the wrong port (8080 becomes 36895 on a little-endian box) or your server binds somewhere unexpected. These bugs are invisible in a printf of the host value and only show up on the wire, so they waste hours. On the defensive side, any code that parses length fields, ports, or addresses out of a received packet must convert with ntohs/ntohl before using them; treating an attacker-supplied big-endian length as a native integer, or trusting inet_addr's ambiguous error return, leads to mis-sized buffers and mis-parsed input — exactly the kind of mistake that turns malformed traffic into a crash or worse.
A value like the 32-bit integer 0x01020304 is four bytes: 01, 02, 03, 04. "Endianness" is simply which byte the CPU puts at the lowest memory address.
value = 0x01020304 (MSB = 01, LSB = 04)
Big-endian memory: low addr -> [01][02][03][04] <- high addr
MSB first
Little-endian memory: low addr -> [04][03][02][01] <- high addr
LSB first
Most desktop/server CPUs (x86-64, most ARM) are little-endian. The value in a register is identical on both kinds of machine — endianness is only about how it is laid out in memory (and therefore how it looks when copied byte-for-byte onto a network).
To make packets portable, the internet protocols fix one order for every multi-byte field on the wire: big-endian, most-significant byte first. This is called network byte order. Your machine's native order is host byte order. On a big-endian host the two happen to be identical; on a little-endian host they differ and you must convert.
The rule is mechanical: convert host -> network before you write a number into a packet or sockaddr, and convert network -> host after you read one out.
| Function | Meaning | Width | Typical use |
|---|---|---|---|
htons(x) |
Host TO Network Short | 16-bit | a port number (sin_port) |
htonl(x) |
Host TO Network Long | 32-bit | an IPv4 address / a length field |
ntohs(x) |
Network TO Host Short | 16-bit | read a port back off the wire |
ntohl(x) |
Network TO Host Long | 32-bit | read a 32-bit field back |
Mnemonic: read the name left to right. htons = host to network short. The s suffix is 16 bits, the l suffix is 32 bits (this is a historical name — "long" here means 32 bits regardless of what long is on your platform). On a big-endian machine all four are no-ops that return their argument unchanged; your code stays correct either way, which is the whole point of calling them unconditionally.
Knowledge check: On a little-endian machine you write sin_port = 8080; (no conversion) and connect. What port does the peer actually see, and why?
8080is0x1F90. Stored little-endian in memory the bytes are90 1F. Those bytes go on the wire as-is, and the peer — which interprets them as big-endian network order — reads0x901F= 36895. The fix issin_port = htons(8080);, which stores the bytes1F 90so the peer reads 8080.
You rarely hard-code an address as an integer; you have a string like "127.0.0.1". inet_pton ("presentation to network") parses that dotted-decimal (or IPv6) text and writes the result already in network byte order into an in_addr:
"127.0.0.1" --inet_pton(AF_INET)--> in_addr.s_addr bytes = 7f 00 00 01
(127 . 0 . 0 . 1, big-endian, no htonl needed)
Because inet_pton already produces network order, you do not call htonl on its result. The reverse, inet_ntop, turns the network-order bytes back into a printable string. Prefer these over the legacy inet_addr/inet_ntoa: inet_pton supports IPv6 (AF_INET6) and returns an unambiguous success/failure code.
inet_addr("255.255.255.255") legitimately returns 0xFFFFFFFF. But inet_addr also returns 0xFFFFFFFF (INADDR_NONE) to signal a parse error. So you cannot distinguish the valid broadcast address from a failure. inet_pton fixes this by returning 1 on success, 0 on a malformed string, and -1 (with errno set) on an unsupported family — three distinct, checkable outcomes.
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); // host -> network, 16-bit; never fails
uint32_t htonl(uint32_t hostlong); // host -> network, 32-bit; never fails
uint16_t ntohs(uint16_t netshort); // network -> host, 16-bit; never fails
uint32_t ntohl(uint32_t netlong); // network -> host, 32-bit; never fails
int inet_pton(int af, const char *src, void *dst);
// af = AF_INET (writes 4 bytes) or AF_INET6 (writes 16 bytes)
// src = NUL-terminated address string, e.g. "127.0.0.1"
// dst = pointer to struct in_addr (AF_INET) or struct in6_addr (AF_INET6)
// returns 1 = success, 0 = src not parseable, -1 = af unsupported (errno=EAFNOSUPPORT)
// on success dst already holds the address in NETWORK byte order
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// src = pointer to in_addr / in6_addr (network order)
// dst = caller buffer; size must be >= INET_ADDRSTRLEN (16) or INET6_ADDRSTRLEN (46)
// returns dst on success, NULL on error (errno set); never allocates
Notes: the conversion functions cannot fail and allocate nothing. inet_pton/inet_ntop do not allocate either — you own the destination buffer, so size it with the INET*_ADDRSTRLEN constants. Always check inet_pton's return value against 1; > 0 is fine but comparing exactly to 1 documents intent.
A multi-byte integer can be stored in memory in more than one order. This ordering is called endianness.
Different CPUs make different choices. The same number can sit in memory in different byte orders on different machines.
To keep networking portable across machines, the standard fixes one order for the wire. Every multi-byte field in a network packet is stored in network byte order, which is big-endian (most significant byte first).
Before you send a number, convert it from your machine's order (the host order) to network order. After you receive one, convert it back.
htons(x) — host to network short — for 16-bit values, such as a port.htonl(x) — host to network long — for 32-bit values, such as an IPv4 address.ntohs, ntohl — the reverse direction (network to host).inet_pton(family, "127.0.0.1", &addr) parses a dotted-decimal string into an in_addr structure. It is the modern function and is preferred over the deprecated inet_addr.
#include <arpa/inet.h> /* htons, htonl, ntohs, ntohl, inet_pton, inet_ntop */
#include <stdint.h> /* uint16_t, uint32_t */
#include <stdio.h>
/* Print the raw bytes of an object in memory order (low address first). */
static void dump_bytes(const char *label, const void *p, size_t n) {
const unsigned char *b = (const unsigned char *)p;
printf("%-22s", label);
for (size_t i = 0; i < n; i++) printf(" %02x", b[i]);
printf("\n");
}
int main(void) {
/* 1. Detect this machine's endianness at runtime. */
uint32_t probe = 0x01020304u;
unsigned char first = *(const unsigned char *)&probe;
printf("Host is %s-endian\n\n", first == 0x01 ? "big" : "little");
/* 2. A 16-bit port: host order vs network order. */
uint16_t port_host = 8080; /* 0x1F90 */
uint16_t port_net = htons(port_host); /* wire order: 0x1F 0x90 */
printf("port value = %u (0x%04x)\n", port_host, port_host);
dump_bytes(" host-order bytes:", &port_host, sizeof port_host);
dump_bytes(" network-order bytes:", &port_net, sizeof port_net);
printf(" round-trip ntohs = %u\n\n", ntohs(port_net));
/* 3. A 32-bit value with htonl / ntohl. */
uint32_t v_host = 0xDEADBEEFu;
uint32_t v_net = htonl(v_host);
dump_bytes(" host 32-bit bytes:", &v_host, sizeof v_host);
dump_bytes(" net 32-bit bytes:", &v_net, sizeof v_net);
printf(" round-trip ntohl = 0x%08x\n\n", ntohl(v_net));
/* 4. Parse a dotted-decimal IPv4 string with inet_pton. */
struct in_addr addr;
int rc = inet_pton(AF_INET, "127.0.0.1", &addr);
if (rc == 1) {
dump_bytes(" 127.0.0.1 wire bytes:", &addr.s_addr, sizeof addr.s_addr);
char back[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, back, sizeof back);
printf(" inet_ntop back -> %s\n", back);
} else if (rc == 0) {
printf(" inet_pton: not a valid IPv4 string\n");
} else {
perror(" inet_pton");
}
/* 5. inet_pton reports failure cleanly (unlike inet_addr). */
struct in_addr bad;
int rc2 = inet_pton(AF_INET, "999.1.1.1", &bad);
printf(" inet_pton(\"999.1.1.1\") returned %d (0 = malformed)\n", rc2);
return 0;
}
dump_bytes(...): a helper that casts any object to unsigned char * and prints each byte in memory order. Casting to unsigned char * is the one legal way in C to inspect an object's raw representation, and it is exactly what a network card copies onto the wire — so this shows the actual bytes that travel.uint32_t probe = 0x01020304u; first = *(unsigned char*)&probe;: reads the byte at the lowest address of a known value. If it is 0x01 the MSB came first (big-endian); otherwise little-endian. This is a runtime endianness detector.port_host = 8080; port_net = htons(port_host);: 8080 is 0x1F90. On a little-endian host the dumps show 90 1f for the host value and 1f 90 for the network value — visual proof that htons reorders the bytes.ntohs(port_net): converts back and prints 8080, showing the round trip is lossless.htonl/ntohl block does the same for a 32-bit value; 0xDEADBEEF is a memorable pattern so the reordering (ef be ad de -> de ad be ef) is obvious.inet_pton(AF_INET, "127.0.0.1", &addr): parses the string straight into network order. The dump prints 7f 00 00 01 — that is 127.0.0.1 read left to right, confirming the bytes are already big-endian and no htonl is needed.inet_ntop(AF_INET, &addr, back, sizeof back): converts the network-order bytes back to the string "127.0.0.1", into a caller-owned buffer sized with INET_ADDRSTRLEN.inet_pton("999.1.1.1", ...) returns 0, demonstrating clean malformed-input detection that inet_addr cannot give you.1. Assigning a port without converting.
addr.sin_port = 8080; // WRONG on little-endian: peer sees 36895
Why it breaks: the raw bytes 90 1F go on the wire and are interpreted big-endian as 0x901F.
addr.sin_port = htons(8080); // FIXED
2. Calling htonl on an inet_pton result.
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
addr.sin_addr.s_addr = htonl(addr.sin_addr.s_addr); // WRONG: double conversion scrambles it
Why it breaks: inet_pton already returns network order; converting again reverses the bytes.
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // FIXED: use the result as-is
3. Trusting inet_addr's return value.
addr.sin_addr.s_addr = inet_addr(user_string); // WRONG: -1 means error OR 255.255.255.255
Why it breaks: you cannot tell a failed parse from the valid broadcast address.
if (inet_pton(AF_INET, user_string, &addr.sin_addr) != 1) { /* handle bad input */ } // FIXED
4. Using a received length/port without ntoh.
uint16_t len = *(uint16_t*)pkt; // WRONG: raw wire bytes in host order assumption
read_exactly(fd, buf, len);
Why it breaks: on a little-endian host len is byte-swapped, so you read the wrong (often huge) amount.
uint16_t len; memcpy(&len, pkt, 2); len = ntohs(len); // FIXED: convert before trusting it
dump_bytes helper above: print the raw bytes of your sin_port/sin_addr right before you send. If a port shows 90 1f instead of 1f 90, you skipped htons.printf("%u vs %u\n", x, ntohs(x)); — if the two are wildly different (8080 vs 36895) you are looking at an unconverted value.sudo tcpdump -i lo0 -X port 8080 (macOS) or tcpdump -i lo -X (Linux) shows the actual on-wire bytes; the destination port in the TCP header is authoritative.sin_addr uninitialized; a perror or an explicit != 1 check turns a mysterious 'connection to garbage address' into a clear error.x/4xb &addr.sin_addr examines the four address bytes, and p/x addr.sin_port shows the stored port in hex so you can eyeball the byte order.inet_pton returns 0 or -1 and you ignore it, sin_addr holds whatever garbage was on the stack. Zero the whole sockaddr_in (memset(&addr, 0, sizeof addr);) and check the return value before using it.INET_ADDRSTRLEN (16) for IPv4 or INET6_ADDRSTRLEN (46) for IPv6, and pass its real size — undersizing causes inet_ntop to fail with ENOSPC, and hand-guessing the length invites overflow.unsigned char * (as dump_bytes does) is well-defined; reading a value through an incompatible pointer type (e.g. *(uint16_t*)pkt on a misaligned pointer) is undefined behaviour and can fault on strict-alignment CPUs. Prefer memcpy into a properly typed local, then convert.ntohs/ntohl a length field and then bounds-check it against your buffer before using it. A byte-swapped 16-bit length can appear enormous; using it directly to size a read or copy is a classic overflow.sockaddr_in for connect/bind is the canonical use of htons (port) and inet_pton (address). Servers, clients, proxies, and load balancers all do this.ntohs/ntohl on every such field. Defensive parsers convert then validate before allocating or copying.htonl/htons so the format is portable across client and server architectures.ntoh*, work, hton*, write out.Write a program that prints whether your machine is big- or little-endian, using a one-line union or pointer-cast probe like the example's.
Ask the user for a port number, convert it with htons, and print the two wire bytes in hex. Verify by hand that 80 -> 00 50 and 8080 -> 1f 90.
Take an IPv4 string on the command line, parse it with inet_pton, print its four network-order bytes, then convert back with inet_ntop and confirm you get the original string. Handle the malformed-input case (return value 0).
Write matching pack/unpack functions: one that serializes a struct { uint16_t port; uint32_t ip; } into a 6-byte big-endian buffer, and one that reads it back — using htons/htonl and ntohs/ntohl — and assert the round trip is lossless.
Simulate a received 2-byte length prefix in a fixed buffer, convert it with ntohs, and safely reject it if it exceeds your buffer capacity before you would ever copy that many bytes. Show that an unconverted read would have produced a dangerously wrong length.
htons/ntohs handle 16-bit values (ports); htonl/ntohl handle 32-bit values. They never fail and are no-ops on big-endian hosts, so call them unconditionally.inet_pton parses an address string directly into network order (so don't htonl it) and returns 1/0/-1 for success/malformed/bad-family; inet_ntop reverses it into a caller-owned buffer.inet_pton/inet_ntop over the ambiguous legacy inet_addr/inet_ntoa, and always ntoh* and bounds-check length fields from untrusted packets before using them.