Networking in C · beginner · ~8 min
- By the end you can fill in a `struct sockaddr_in` correctly and call `bind()` to attach a socket to a specific local address and port. - You can explain what the kernel records when `bind()` succeeds, and why a server needs a fixed, well-known port while a client usually does not. - You can diagnose and fix the two errors every beginner hits: `EADDRINUSE` (with `SO_REUSEADDR`) and `EACCES` (privileged ports). - You can bind lab servers to `INADDR_LOOPBACK` so they are unreachable from the network, and explain why `INADDR_ANY` is a bigger exposure decision. - You can read back the actual bound port with `getsockname()` after binding to port 0.
You already know from network byte order that multi-byte numbers travelling between machines must be in big-endian order, which is why you wrap ports in htons() and addresses in htonl(). bind() is the first place that knowledge becomes load-bearing: the port and address you hand the kernel live inside a struct sockaddr_in, and if you forget to byte-swap them the kernel will faithfully bind you to the wrong port.
This lesson takes a raw socket file descriptor — an open but unattached endpoint — and gives it a local identity: a specific (address, port) pair on this machine. That identity is what makes a socket reachable. Clients dial (address, port); without a bind(), a server socket has no address to dial. After this you move on to listen()/accept(), which turn a bound socket into one that actually receives connections.
Every server you have ever connected to — a web server, a database, an SSH daemon — called bind() to claim its port. Getting the address argument wrong is a real security decision, not a detail: bind to INADDR_ANY and your service answers on every network interface, including the public one, so a lab tool you thought was private is suddenly exposed to the internet. Bind to INADDR_LOOPBACK and only programs on the same machine can reach it. The classic EADDRINUSE failure after a crash also teaches a durable lesson about TCP's TIME_WAIT state and why SO_REUSEADDR exists.
socket() returns a file descriptor for an endpoint that exists but is anonymous — it has a protocol family and type, but no local address and no local port. bind() is the syscall that stamps a local identity onto it. Think of the socket as a phone that has been powered on but not yet assigned a number; bind() assigns the number.
socket() bind(fd, &addr, len)
--------> [ fd: TCP ] -------------------> [ fd: TCP, 127.0.0.1:8080 ]
anonymous has a local identity
After a successful bind(), the kernel records the pair in its port table, and any packet arriving for (address, port) is routed toward this socket. For a client you normally skip bind() and let connect() auto-assign an ephemeral local port; for a server you bind on purpose so clients know where to find you.
For IPv4 you fill a struct sockaddr_in and pass its address, cast to the generic struct sockaddr *. Three fields matter, and two of them need byte-swapping:
| Field | Meaning | How to set it |
|---|---|---|
sin_family |
Address family | AF_INET (host order — it is not sent on the wire) |
sin_port |
Port number | htons(8080) — big-endian |
sin_addr.s_addr |
IPv4 address | htonl(INADDR_LOOPBACK) or htonl(INADDR_ANY) — big-endian |
Always memset() the struct to zero first, because sockaddr_in contains a padding field (sin_zero) that must be clear. The length argument is sizeof(struct sockaddr_in).
The address you bind is a security boundary, not a formality:
| Value | Binds to | Who can reach it |
|---|---|---|
INADDR_LOOPBACK (127.0.0.1) |
loopback only | only this machine |
INADDR_ANY (0.0.0.0) |
every interface | anyone who can route to any of your IPs |
| a specific interface IP | that one NIC | hosts on that network |
For labs and defensive work, bind INADDR_LOOPBACK by default. INADDR_ANY is convenient but it means a tool you meant to test locally answers to the outside world. Treat switching to INADDR_ANY as a deliberate "I intend this to be network-reachable" decision, ideally paired with a firewall rule.
Knowledge check: you bind to htonl(INADDR_ANY) and start a debug service on port 9000 on your laptop at a coffee shop. Who can potentially connect?
Anyone on the same Wi-Fi (and anyone who can route to your laptop's IP) can reach port 9000, because
INADDR_ANYlistens on every interface, not just loopback. If you only wanted local testing,INADDR_LOOPBACKwould have kept it unreachable from the network. This is exactly why the lesson defaults to loopback.
When a TCP connection closes, the endpoint that closed first lingers in TIME_WAIT for a while (tens of seconds to a couple of minutes) so late packets can drain. During that window the (address, port) is still considered in use, so a fresh bind() to the same port fails with EADDRINUSE — you restart your server and it refuses to start.
run #1: bind :8080 --> serve --> exit
port :8080 held in TIME_WAIT ~60s
run #2 (immediately): bind :8080 --> EADDRINUSE
The fix is the SO_REUSEADDR socket option, set before bind():
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
It tells the kernel "let me re-bind a port that is only held by a lingering TIME_WAIT socket." EADDRINUSE also fires when a different live process genuinely owns the port; SO_REUSEADDR does not (and should not) override that.
Ports 1–1023 are "privileged": binding them requires elevated privilege (root, or a capability like CAP_NET_BIND_SERVICE on Linux). Try to bind port 80 as an ordinary user and you get EACCES. For every exercise here, pick a high port such as 8080 so you never need root — running network experiments as root is itself a bad habit.
#include <sys/socket.h>
#include <netinet/in.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: the descriptor from socket().addr: pointer to your struct sockaddr_in (IPv4), cast to struct sockaddr *.addrlen: sizeof the concrete struct, e.g. sizeof(struct sockaddr_in).0 on success, -1 on error and sets errno (EADDRINUSE, EACCES, EADDRNOTAVAIL, EINVAL). Always check the return value and use perror/strerror.int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
level = SOL_SOCKET, optname = SO_REUSEADDR, optval pointing at an int set to 1, optlen = sizeof(int). Must be done before bind().int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr with the socket's actual local address. Essential when you bind to port 0 (kernel-chosen ephemeral port). addrlen is in/out: set it to the buffer size before the call.struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* port, network byte order */
struct in_addr sin_addr; /* .s_addr = IPv4, network byte order */
/* padding (sin_zero) — memset the struct to zero */
};
No heap allocation is involved here: the struct is a plain local variable and there is nothing to free. Do remember to close(fd) the socket when done.
bind() doesThe call looks like this:
bind(fd, &addr, sizeof addr);
It tells the kernel: "this socket owns port N on address A." Here fd is the socket's file descriptor (the integer that identifies an open socket).
After a successful bind(), any packet sent to that (address, port) pair is routed to this socket.
EADDRINUSE — the address is already in use.
This happens when another process is already bound to the port. It can also happen on your own program: a previous run may have left a socket in the TIME_WAIT state, which briefly holds the port.
To allow rebinding immediately, set the SO_REUSEADDR socket option:
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
EACCES — permission denied.
Ports below 1024 are privileged and require root. For your labs, pick a high port such as 8080.
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* Bind a TCP socket to a kernel-chosen loopback port, read the port back,
then prove that a second bind() to the SAME port fails with EADDRINUSE.
Fully hermetic: loopback only, no root, no real network peer. */
int main(void) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); return 1; }
int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0) {
perror("setsockopt");
return 1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1, lab-only */
addr.sin_port = htons(0); /* 0 = "kernel, pick one" */
if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
perror("bind");
return 1;
}
/* Ask the kernel which port it actually gave us. */
struct sockaddr_in bound;
socklen_t blen = sizeof bound;
if (getsockname(fd, (struct sockaddr *)&bound, &blen) < 0) {
perror("getsockname");
return 1;
}
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &bound.sin_addr, ip, sizeof ip);
unsigned short port = ntohs(bound.sin_port);
printf("socket 1 bound to %s:%u\n", ip, port);
/* Now try to steal the same (address, port) with a second socket. */
int fd2 = socket(AF_INET, SOCK_STREAM, 0);
if (fd2 < 0) { perror("socket"); return 1; }
struct sockaddr_in same;
memset(&same, 0, sizeof same);
same.sin_family = AF_INET;
same.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
same.sin_port = htons(port); /* deliberately the taken port */
if (bind(fd2, (struct sockaddr *)&same, sizeof same) < 0) {
printf("socket 2 bind to port %u failed as expected: %s\n",
port, strerror(errno));
} else {
printf("socket 2 unexpectedly bound too\n");
}
close(fd2);
close(fd);
return 0;
}
socket(AF_INET, SOCK_STREAM, 0): creates an anonymous IPv4/TCP endpoint; the returned fd has no local address yet. We check < 0 because every syscall here can fail.setsockopt(..., SO_REUSEADDR, ...): set before bind() so a lingering TIME_WAIT socket from a prior run won't block us with EADDRINUSE.memset(&addr, 0, sizeof addr): zero the struct so the padding (sin_zero) is clean — skipping this is a classic source of subtle bugs.sin_family = AF_INET: kept in host order because it's interpreted locally, never sent on the wire.sin_addr.s_addr = htonl(INADDR_LOOPBACK): bind to 127.0.0.1 only, so nothing off-machine can reach this socket. htonl because the address is a wire value.sin_port = htons(0): port 0 is a signal — "kernel, assign me any free ephemeral port." Great for hermetic demos and tests because it never collides.bind(fd, (struct sockaddr *)&addr, sizeof addr): the star of the show; the cast to the generic sockaddr * is required by the API; the length is the size of the concrete struct.getsockname(...) + ntohs: because we asked for port 0, we don't know the port until we read it back; getsockname fills in the actual bound address, and ntohs converts the port from network to host order for printing.EADDRINUSE (Address already in use) because a live socket already owns it — proving that SO_REUSEADDR does not let two live sockets share the identical pair.close(fd2); close(fd): release both descriptors; leaking sockets exhausts the process FD limit.1. Forgetting to byte-swap the port.
addr.sin_port = 8080; /* WRONG: host order */
Why it breaks: on a little-endian machine 8080 (0x1F90) is stored byte-reversed, so the kernel binds port 0x901F = 36895. Your client connecting to 8080 never finds you.
addr.sin_port = htons(8080); /* FIXED */
2. Not zeroing the struct.
struct sockaddr_in a; /* WRONG: sin_zero/padding is garbage */
a.sin_family = AF_INET; a.sin_port = htons(8080);
Why it breaks: uninitialized padding can confuse comparisons and some stacks; it is undefined-ish and non-portable.
struct sockaddr_in a; memset(&a, 0, sizeof a); /* FIXED */
3. Ignoring bind()'s return value.
bind(fd, (struct sockaddr *)&a, sizeof a); /* WRONG: no check */
listen(fd, 16); /* proceeds on a failed bind */
Why it breaks: if bind failed (EADDRINUSE/EACCES) you continue with an unbound socket and get baffling later errors.
if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { perror("bind"); return 1; } /* FIXED */
4. Setting SO_REUSEADDR after bind().
bind(fd, ...);
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); /* WRONG: too late */
Why it breaks: the option only affects the bind decision, so setting it afterward does nothing for TIME_WAIT.
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes); /* FIXED: before bind */
bind(fd, ...);
perror/strerror(errno) first. bind returning -1 with Address already in use (EADDRINUSE), Permission denied (EACCES), or Cannot assign requested address (EADDRNOTAVAIL — you tried an IP no local interface has) each points at a distinct fix.lsof -i :8080 (macOS/Linux) or ss -ltnp / netstat -anv | grep 8080 shows the process holding it and whether it's in TIME_WAIT.getsockname() (as in the demo) rather than assuming; this catches byte-order bugs immediately (you'll see a nonsense port like 36895).strace -e trace=bind,setsockopt ./server on Linux (or dtruss on macOS) shows the exact args and errno the kernel returned — invaluable when the C looks right but behaves wrong.bind, then print *(struct sockaddr_in *)addr to inspect family/port/addr as the kernel will see them.sizeof(struct sockaddr_in) for an IPv4 sockaddr_in. Passing a too-large or mismatched length can make the kernel read past your struct (EINVAL at best, garbage at worst).struct sockaddr is smaller than sockaddr_in; only ever take the address of a real sockaddr_in (or sockaddr_storage) and cast the pointer — never declare a bare struct sockaddr and write port/address into it, which would overflow it.getsockname, initialize the in/out addrlen to the buffer size before the call; leaving it uninitialized is undefined behavior and may truncate or overflow.socket() needs a matching close(). Leaking descriptors eventually yields EMFILE (too many open files). There is no heap allocation here, so there is nothing to free, but the FD leak is the analogous hazard.bind() itself is thread-safe per-socket, but never share and bind the same fd from two threads without synchronization; the port table update and the descriptor's state are shared.bind() their listening port at startup. Production servers commonly bind INADDR_ANY deliberately and rely on firewalls; internal-only services often bind 127.0.0.1.127.0.0.1 so it is reachable by a local agent but not the internet — the exact defensive pattern this lesson teaches.getsockname() lets test suites spin up servers on collision-free ephemeral ports.SO_REUSEADDR before bind() on long-running servers so restarts are instant; default to the narrowest address that works; keep servers off privileged ports (put a reverse proxy or CAP_NET_BIND_SERVICE in front instead of running as root).INADDR_LOOPBACK, print success, and confirm with lsof -i :8080 (or ss -ltn) in another terminal while the program sleeps for 20 seconds before closing.SO_REUSEADDR line, run a small server that binds 8080 and exits, then immediately run it again. Capture the exact errno message, then add SO_REUSEADDR back and show the second run now succeeds.int bind_ephemeral(void) that creates a socket, binds 127.0.0.1:0, uses getsockname() to discover the port, prints it, and returns the fd — the pattern real test harnesses use.INADDR_ANY:9000, then use getsockname and a comment to explain which interfaces it now answers on; change it to INADDR_LOOPBACK and explain in a printed line what an outside host would now see (a refused connection). Do all testing on your own machine only.bind() gives an anonymous socket a local identity: a specific (address, port) pair the kernel routes packets to.struct sockaddr_in: sin_family = AF_INET, sin_port = htons(port), sin_addr.s_addr = htonl(addr) — byte-swap the port and address.bind()'s return value; EADDRINUSE means the port is taken (often a TIME_WAIT leftover), EACCES means a privileged port (< 1024).SO_REUSEADDR before bind() to survive TIME_WAIT on restart.INADDR_LOOPBACK for lab/local services — INADDR_ANY exposes you on every interface, a deliberate security choice.getsockname() to get a collision-free ephemeral port; use a high port like 8080 to avoid needing root; close() every socket.