JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Addresses, Ports, and Byte Order

Guide 1 gave you the socket idea. Now we fill in the three small but unforgiving details a real connection needs: which machine (the IP address), which program on it (the port), and how multi-byte numbers must be laid out on the wire (network byte order). Get these right once and the client and server you write next will just work.

An address names a machine; a port names a program

From guide 1 you already know a socket is an endpoint and that the TCP/IP model stacks responsibilities in layers. Now we name the endpoint precisely. Two numbers do it: an IP address that picks one machine out of all the machines reachable on a network, and a port that picks one program out of the many running on that machine. Think of the address and port like a postal address plus an apartment number: the street address gets the letter to the right building, and the apartment number gets it to the right door inside. Neither alone is enough — a packet carrying only an IP would arrive at the machine with no idea which program should receive it.

An IPv4 address is 32 bits — four bytes — usually written in dotted-decimal form like 93.184.216.34, where each of the four parts is one byte in the range 0 to 255. So 93.184.216.34 is really the four-byte sequence 0x5d 0xb8 0xd8 0x22. A port is a 16-bit number, an unsigned value from 0 to 65535, which is why you store it in a uint16_t and why the type is unsigned. The low ports under 1024 (so 80 for HTTP, 22 for ssh, 443 for HTTPS) are well-known ports reserved for standard services and usually need privilege to bind; the high ports are free for your own programs to grab.

Why byte order even exists

Here is the trap that catches every beginner once. A port like 0x1234 does not fit in a single byte; it is a two-byte number, and memory is a flat array of bytes, so the machine must decide which byte comes first. This choice is called endianness, and you met the concept back in the data-representation rung. A little-endian machine (x86, most ARM today) stores the least-significant byte first, so 0x1234 lands in memory as the bytes 0x34 then 0x12. A big-endian machine stores the most-significant byte first: 0x12 then 0x34. Same number, two different byte layouts.

Inside one machine this never matters — every read and write uses the same convention, so the layout is invisible to you. But a network connects machines that may disagree. If a little-endian laptop sends the raw two bytes of port 0x1234 as 0x34 0x12, and a big-endian server reads those same two bytes in its own order, it sees 0x3412 — a completely different port. Two computers that each work perfectly alone would fail to talk, not because of a bug in either, but because they never agreed on byte order. The wire needs one fixed convention that both sides honor regardless of their internal habits.

Network byte order, and the four functions that get you there

The agreed convention is simple: network byte order is big-endian. Every multi-byte value that travels in an IP or TCP header — addresses, ports, lengths — is sent most-significant byte first, by definition. Your machine's own layout is called host byte order, which on x86 is little-endian and therefore disagrees with the network. So you must convert at the boundary: host order on the inside, network order on the wire. The standard library gives you exactly four conversion functions, named by a tidy scheme — h for host, n for network, s for short (16-bit), l for long (32-bit):

htons(x)   host  -> network,  16-bit   (use for a PORT)
htonl(x)   host  -> network,  32-bit   (use for an IPv4 ADDRESS)
ntohs(x)   network -> host,    16-bit   (a port you just received)
ntohl(x)   network -> host,    32-bit   (an address you just received)

/* read the name left to right: h-to-n-s = host to network, short */
The four byte-order functions. On a big-endian host they do nothing (host already equals network); on a little-endian host they swap the bytes. Crucially, your code is the same either way — that is the point of calling them.

The discipline is mechanical once you see it. Any port or address you put into a sockaddr struct goes through htons() or htonl() first; any port or address you read out of one received from the network goes through ntohs() or ntohl(). Calling htons() on a port you forgot to convert is the single most common networking bug for beginners: the connection refuses or lands on the wrong service, and everything else looks fine. Write the call the moment you write the assignment, and you never debug it.

Filling in a sockaddr_in, step by step

All of this comes together in one small struct. For IPv4 the kernel wants a struct sockaddr_in, which bundles three things: the address family, the port (in network order), and the 32-bit IPv4 address (also in network order). You fill it in, then hand its address to bind() on a server or connect() on a client. The socket address is just this struct: the family field says "this is IPv4", and the other two fields are the where-and-which we have been building toward.

  1. Zero the whole struct first: memset(&addr, 0, sizeof(addr)). This clears padding and any fields you do not set explicitly, so no stale bytes leak into the kernel.
  2. Set the family: addr.sin_family = AF_INET. This is what tells the kernel to read the rest as an IPv4 address rather than IPv6 or a Unix socket.
  3. Set the port through htons: addr.sin_port = htons(8080). Forgetting htons here is the classic bug — on x86 your port silently ends up byte-swapped.
  4. Set the address: use inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) to parse a dotted-decimal string into the field in network order, and check it returns 1.

Two convenient shortcuts deserve a name. The address 0.0.0.0, written as the constant INADDR_ANY, means "bind to every network interface this machine has" — a server uses it to accept connections arriving on any of its addresses. And 127.0.0.1 is the loopback address, a virtual interface that loops straight back into the same machine without touching any real network hardware; it is how you run a client and server on one laptop to test, which is exactly what the next two guides will do. INADDR_ANY is itself a number that, when you assign it, still goes through htonl() to be correct on every host.

Names, numbers, and what is next

One honest gap remains: humans type names like example.com, not numbers like 93.184.216.34. The translation from a name to an address is done by DNS, the Domain Name System, a distributed directory the network consults for you. In code you reach it through getaddrinfo(), which takes a host name and a service (a port or a name like "http") and hands back a ready-made sockaddr, doing the IPv4-or-IPv6 and the byte-order work for you. It is the modern, recommended front door; the manual sockaddr_in we just filled is what getaddrinfo() produces under the hood, and seeing it by hand once is why the automatic version will never feel like magic.

Notice that none of this is specific to TCP. An address, a port, and network byte order are needed whether you open a reliable stream or fire off datagrams — the TCP-versus-UDP choice changes how the bytes flow, not how you name the endpoints. So everything in this guide is groundwork you will reuse in guide 5's UDP examples just as much as in the TCP work coming immediately next. The addressing layer sits below the transport choice.

You now have the vocabulary a connection is built from: an IP address and port to name an endpoint, the endianness problem and why network byte order solves it, the htons/htonl/ntohs/ntohl quartet, and a filled-in sockaddr_in ready to hand the kernel. Guide 3, Writing a TCP Client, takes this struct straight into connect() and opens your first real connection; guide 4 turns it around into a listening server. The hard, easy-to-miss detail — byte order — is now behind you, so those guides can focus on the flow of data rather than on the layout of a single number.