Port 8080 became port 36895
You write addr.sin_port = 8080;. You connect. The connection fails,
or worse, reaches a different service. tcpdump shows the packet going
to port 36895. You did not type 36895 anywhere.
8080 is 0x1F90. 36895 is 0x901F. The same two bytes, in the
opposite order. Your CPU wrote them one way and the network read them the other.
This lesson is about the three integers that identify a network endpoint —
address, port, family — how they sit in memory, and why one function call stands
between you and this bug.
Addresses and ports are integers
Lesson 01 showed the IPv4 header as a struct: src_addr and
dst_addr are each a uint32_t. 192.168.1.7 is
not a string the network understands. It is how humans write the four bytes
C0 A8 01 07, which together are the number 3232235783. A port is a
uint16_t: 0 to 65535. The pair (address, port) names one socket on
one machine, the way (array, index) names one element.
Ports below 1024 need root to bind. Ports 1024–49151 are registered to services by convention (5432 is Postgres, 8080 is "some HTTP"). Ports 49152–65535 are ephemeral: when you connect without choosing a port, the kernel picks a free one from this range for you. That is the 51234 you saw as the source port in lesson 01.
Two byte orders
A uint16_t occupies two bytes. Which byte goes at the lower address?
Your CPU — x86, ARM, almost everything you will run — puts the least
significant byte first. That is little-endian. 0x1F90 sits in RAM as
90 1F.
The network made the opposite choice decades ago: the most significant
byte goes first. That is big-endian, called network byte order.
0x1F90 travels as 1F 90. Every multi-byte field in every header from
lesson 01 — ports, lengths, addresses, checksums — is big-endian on the wire.
So before an integer goes into a header, something has to swap its bytes. Four functions do this, and their names encode the direction and the size:
| function | reads as | use for |
|---|---|---|
| htons(x) | host to network, short (16-bit) | ports, before filling a struct |
| htonl(x) | host to network, long (32-bit) | addresses built by hand, lengths |
| ntohs(x) | network to host, short | reading a port out of a struct or header |
| ntohl(x) | network to host, long | reading an address or 32-bit field |
htons converts at the boundary when a value
goes in; ntohs converts when it comes out. On a big-endian CPU these
functions do nothing, which is exactly why you call them anyway: the code stays
correct everywhere.
struct sockaddr_in: 16 bytes
The kernel wants the family, port, and address together in one struct. Its layout is fixed and you should know it the way you know the layout of a struct you wrote yourself:
Both structs are 16 bytes and both begin with a 2-byte family field.
That is the trick. connect, bind, sendto
and friends are declared to take struct sockaddr * so one signature
can serve IPv4, IPv6, and Unix sockets. You build the specific struct, cast its
address to the generic pointer, and pass the size. The kernel reads the first two
bytes, learns it is AF_INET, and reinterprets the remaining 14 as a
sockaddr_in. This is C's version of polymorphism: a common prefix and a cast.
Watch the struct fill in
The visualization below first shows the two byte orders for a port and an address,
then lays out the 16 bytes of a sockaddr_in on the stack and fills
them one assignment at a time. Watch which bytes each call writes, and in which order.
Filling it correctly
Notice the asymmetry. The port needs htons because you have it as a
host-order integer. The address does not need htonl because
inet_pton parses the string straight into network order — the string
"192.168.1.7" already lists the bytes most-significant first.
If you ever build an address from an integer by hand, you do need
htonl.
addr.sin_port = 8080; without htons compiles cleanly and
connects to port 36895. If a test server happens to listen on both, you will never
notice. Printing addr.sin_port without ntohs is the mirror
image: it prints 36895 for a socket that is really on 8080. Any time a port looks
random, swap the bytes in your head first.
bind(): claiming an address
A client fills a sockaddr_in to say where it wants to go.
A server fills one to say where it will listen, and passes it to
bind. bind attaches the (address, port) pair to your
socket in the kernel. From then on, packets arriving for that pair are delivered
to your receive buffer. Two sockets cannot bind the same pair; the second
bind fails with EADDRINUSE.
The address you bind decides who can reach you. 127.0.0.1 binds only
the loopback interface: only programs on this machine. 0.0.0.0
(INADDR_ANY) binds every interface the machine has: the LAN, Wi-Fi,
and anything else. A port of 0 asks the kernel to pick an ephemeral one.
Names instead of numbers
Nobody types 93.184.216.34. They type example.com, and
something has to turn that name into a uint32_t. getaddrinfo
does it: it asks DNS, fills in a linked list of ready-made sockaddr
structs — already in network order — and you connect to the first one that works.
The result is the same 16 bytes you built by hand above. getaddrinfo
just saved you the string parsing and the byte swapping. When something goes
wrong, it is still those 16 bytes you print and inspect.
An endpoint is three integers in a 16-byte struct — family, port, address — and the port and address must be stored big-endian with htons/inet_pton, because the wire and your CPU disagree about which byte comes first.