Networking in C · beginner · ~8 min

htons(), htonl(), inet_pton() — network byte order

- 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.

Overview

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.

Why it matters

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.

Core concepts

Endianness: the same number, two memory layouts

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).

Network byte order = big-endian

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.

The four conversion functions

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?

8080 is 0x1F90. Stored little-endian in memory the bytes are 90 1F. Those bytes go on the wire as-is, and the peer — which interprets them as big-endian network order — reads 0x901F = 36895. The fix is sin_port = htons(8080);, which stores the bytes 1F 90 so the peer reads 8080.

Parsing addresses: inet_pton and inet_ntop

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.

Why inet_addr is a trap

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.

Syntax notes

#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.

Lesson

Endianness: why byte order matters

A multi-byte integer can be stored in memory in more than one order. This ordering is called endianness.

  • Big-endian: the most significant byte comes first.
  • Little-endian: the least significant byte comes first.

Different CPUs make different choices. The same number can sit in memory in different byte orders on different machines.

Network byte order

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.

The conversion functions

  • 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).

Parsing an IP address

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.

Code examples

#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;
}

Line by line

  • 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.
  • The 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.
  • The final inet_pton("999.1.1.1", ...) returns 0, demonstrating clean malformed-input detection that inet_addr cannot give you.

Common mistakes

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

Debugging tips

  • Dump the bytes. The single most useful trick is the 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.
  • Print both interpretations. printf("%u vs %u\n", x, ntohs(x)); — if the two are wildly different (8080 vs 36895) you are looking at an unconverted value.
  • Use a packet sniffer on loopback. 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.
  • Check inet_pton's return, always. A silent failure leaves sin_addr uninitialized; a perror or an explicit != 1 check turns a mysterious 'connection to garbage address' into a clear error.
  • In gdb: 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.

Memory safety

  • Uninitialized address on failure. If 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.
  • Buffer sizing for inet_ntop. Pass a buffer at least 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.
  • Type-punning correctly. Inspecting bytes via 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.
  • Attacker-controlled lengths. When parsing received packets, always 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.

Real-world uses

  • Every socket program. Filling 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.
  • Binary protocol parsing. Protocols like DNS, TLS records, and custom TCP framing put 16/32-bit length and type fields on the wire in network order; parsers call ntohs/ntohl on every such field. Defensive parsers convert then validate before allocating or copying.
  • Serialization. When designing your own wire format, standardize on big-endian and run every integer through htonl/htons so the format is portable across client and server architectures.
  • Best practice: call the conversion functions unconditionally (they are no-ops on big-endian hosts), keep numbers in host order everywhere in your program logic, and convert only at the exact I/O boundary — read in, ntoh*, work, hton*, write out.

Practice tasks

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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.

Summary

  • Endianness is which byte of a multi-byte integer sits at the lowest memory address; big-endian puts the most significant byte first, little-endian the least. Most machines you'll use are little-endian.
  • The wire uses one fixed order — network byte order = big-endian. Convert host->network before sending, network->host after receiving.
  • 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.
  • Prefer 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.

Practice with these exercises