Networking in C · beginner · ~8 min
By the end you can: - Explain why the socket API uses a generic `struct sockaddr *` while you actually fill a family-specific struct. - Declare, zero, and populate a `struct sockaddr_in` with the correct family, port, and IPv4 address. - Choose between `INADDR_LOOPBACK` and `INADDR_ANY` and say what each exposes. - Pass the struct to `bind()`/`connect()` with the right cast and length, and read one back with `getsockname()`. - Recognise the byte-level layout of the struct and why every field except the numeric ones is set once and left alone.
In the previous lesson you called socket() and got back a file descriptor — a bare endpoint with no address attached to it. This lesson gives that endpoint a name: an IPv4 address and port. The socket calls that consume an address (bind, connect, sendto, accept) all take a struct sockaddr *, a deliberately generic type. For IPv4 you never build a sockaddr directly; you build the concrete struct sockaddr_in, fill three fields, and hand over a pointer to it, cast to struct sockaddr *.
Everything here builds on the file descriptor you already know how to create. sockaddr_in is just the small block of memory that tells the kernel where that descriptor should live on the network. Throughout the course we stay on the loopback interface (127.0.0.1) so every example is safe to run on your own machine with no external exposure.
Almost every server and client program starts by constructing an address struct, and small mistakes here have outsized consequences. Forgetting to convert the port to network byte order silently connects you to the wrong port; forgetting to zero the struct can leave garbage in padding that stricter kernels reject; and binding to INADDR_ANY when you meant localhost quietly exposes a service to the entire network — a classic way a debug or admin port ends up reachable from the internet. Getting the address struct right is the difference between a service that is reachable exactly where you intend and one that is either broken or dangerously open.
The socket API was designed before IPv6 existed and had to stay open to address families nobody had invented yet. So the address-taking calls accept a pointer to the generic struct sockaddr, and each protocol family defines its own concrete struct that starts with the same family field. For IPv4 that concrete struct is struct sockaddr_in. You fill the concrete one and cast its pointer to struct sockaddr * at the call site.
struct sockaddr * <-- what bind()/connect() declare
| (cast)
v
+-----------------------------+
| struct sockaddr_in (IPv4) |
| sin_family = AF_INET |
| sin_port = htons(...) |
| sin_addr = htonl(...) |
| sin_zero[8] = 0 padding |
+-----------------------------+
The struct itself:
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* 16-bit port, network order */
struct in_addr sin_addr; /* 32-bit IPv4, network order */
char sin_zero[8]; /* padding, keep zeroed */
};
struct in_addr { in_addr_t s_addr; }; /* the address lives in .s_addr */
Only three fields carry meaning; sin_zero is padding that makes sockaddr_in the same size as the generic sockaddr.
| Field | Type | Holds | How you set it |
|---|---|---|---|
sin_family |
sa_family_t (u16 on Linux) |
address family | AF_INET |
sin_port |
in_port_t (u16) |
port number | htons(port) |
sin_addr.s_addr |
in_addr_t (u32) |
IPv4 address | htonl(INADDR_LOOPBACK) or inet_pton() |
sin_zero[8] |
char[8] |
nothing | leave 0 (set by memset) |
Note the nesting: the address is not sin_addr directly but sin_addr.s_addr, because sin_addr is itself a one-field struct (struct in_addr). This wrapper exists for historical API reasons; just remember to write .s_addr.
The first thing to do with a fresh sockaddr_in is wipe it:
struct sockaddr_in a;
memset(&a, 0, sizeof a);
This does two jobs at once. It clears sin_zero so no stack garbage travels into the kernel, and it gives every field a defined starting value. Skipping this is undefined behaviour waiting to happen: an uninitialised automatic struct contains whatever was on the stack before, and a kernel that inspects the padding may reject the address with EINVAL.
Knowledge check: after memset(&a, 0, sizeof a), which fields still need to be assigned before you can bind() on IPv4?
sin_family,sin_port, andsin_addr.s_addr. Thememsetalready handledsin_zero, and it also zeroed the three meaningful fields — but a zero family (0, notAF_INET) and a zero port/address are almost never what you want, so you set those three explicitly. (Binding withsin_port = 0is the deliberate exception: it asks the kernel to pick an ephemeral port.)
sin_port and sin_addr.s_addr are stored in network byte order (big-endian). Your CPU is probably little-endian, so you must convert:
htons() — host-to-network short (16-bit) for the port.htonl() — host-to-network long (32-bit) for the address constants.The next lesson covers byte order in depth; here just remember that the two numeric fields always pass through an hton* conversion. If you memorise sin_port = htons(port) and s_addr = htonl(INADDR_...) you will not go wrong.
Two constants pick which interface a server binds to. They only matter for bind(); a client using connect() names the peer's real address instead.
| Constant | Value | Meaning | Reachable from |
|---|---|---|---|
INADDR_LOOPBACK |
127.0.0.1 |
localhost only | the same machine only |
INADDR_ANY |
0.0.0.0 |
every local interface | anything that can route to the host |
INADDR_ANY is convenient but is also how internal-only services accidentally become internet-reachable. Bind to INADDR_LOOPBACK unless you have a specific reason to accept remote connections, and when you do bind broadly, put a firewall or explicit access control in front of it. In this course every server binds to loopback.
Knowledge check: a colleague binds a debug endpoint with htonl(INADDR_ANY) on a cloud VM and is surprised strangers hit it. What changed vs. INADDR_LOOPBACK?
INADDR_ANY(0.0.0.0) accepts connections arriving on any interface, including the VM's public one.INADDR_LOOPBACK(127.0.0.1) accepts only connections that originate on the same host, so nothing off-box can reach it. The defensive default is loopback; widen it deliberately, not by habit.
#include <netinet/in.h> /* struct sockaddr_in, in_addr, INADDR_* */
#include <arpa/inet.h> /* htons, htonl, inet_pton, inet_ntop */
#include <sys/socket.h> /* bind, connect, getsockname */
Types
struct sockaddr_in — the IPv4 address struct (16 bytes on typical systems). Fields: sin_family, sin_port, sin_addr, sin_zero.struct in_addr { in_addr_t s_addr; } — wraps the 32-bit address; the value lives in .s_addr.socklen_t — an integer type for address lengths; use it for the length argument of getsockname()/accept().Byte-order helpers (from <arpa/inet.h>)
uint16_t htons(uint16_t) — host to network, 16-bit. Use for sin_port. Cannot fail.uint32_t htonl(uint32_t) — host to network, 32-bit. Use for INADDR_* constants. Cannot fail.uint16_t ntohs(uint16_t), uint32_t ntohl(uint32_t) — the reverse, for printing values you read back.Parsing/formatting addresses
int inet_pton(int af, const char *src, void *dst) — parse a text IP (e.g. "127.0.0.1") into dst (point it at &a.sin_addr). Returns 1 on success, 0 if src is not a valid address, -1 on a bad family (sets errno). Result is already in network order — do not wrap it in htonl.const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) — format a network-order address into text. dst must be at least INET_ADDRSTRLEN bytes for IPv4. Returns dst or NULL on error.Using the struct with syscalls
int bind(int fd, const struct sockaddr *addr, socklen_t len) — cast (struct sockaddr *)&a and pass sizeof a. Returns 0 or -1 (sets errno, e.g. EADDRINUSE).int connect(int fd, const struct sockaddr *addr, socklen_t len) — same cast/length; addr is the peer's address.int getsockname(int fd, struct sockaddr *addr, socklen_t *len) — read a socket's own address back. len is in/out: set it to sizeof your buffer before the call; the kernel writes the actual length.No field of sockaddr_in is heap-allocated, so there is nothing to free(). You are responsible for close()-ing the file descriptor, not the address struct.
The socket functions accept a generic address pointer: struct sockaddr *.
That type is deliberately vague. It works for many address families. For IPv4 you fill in a more specific struct, struct sockaddr_in, and pass a pointer to it:
struct sockaddr_in {
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* network byte order */
struct in_addr sin_addr; /* IPv4 address */
char sin_zero[8]; /* padding, set to 0 */
};
Before filling any field, zero the whole struct:
memset(&a, 0, sizeof a);
The sin_zero field is padding. Some older code or stricter kernels can read those bytes, so leaving them uninitialized may cause the address to be rejected. Zeroing once keeps the padding clean.
After that, set three fields: family, port, and address.
Two common constants pick which interface you bind to:
INADDR_ANY is 0.0.0.0. It means "listen on every interface," including external ones.INADDR_LOOPBACK is 127.0.0.1. It means "localhost only."In this course we use INADDR_LOOPBACK.
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h> /* htons, htonl, inet_ntop */
#include <netinet/in.h> /* struct sockaddr_in, INADDR_LOOPBACK */
#include <sys/socket.h> /* socket, bind, getsockname */
#include <unistd.h> /* close */
/* Print the raw bytes of any object so we can *see* the wire layout. */
static void dump_bytes(const char *label, const void *p, size_t n) {
const unsigned char *b = p;
printf("%-18s", label);
for (size_t i = 0; i < n; i++) printf("%02x ", b[i]);
printf("\n");
}
int main(void) {
struct sockaddr_in a;
/* 1. Zero EVERYTHING, including the sin_zero padding. */
memset(&a, 0, sizeof a);
/* 2. Fill the three meaningful fields. */
a.sin_family = AF_INET; /* IPv4 family */
a.sin_port = htons(8080); /* host -> network */
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); /* 127.0.0.1 */
printf("sizeof(struct sockaddr_in) = %zu bytes\n\n", sizeof a);
/* Show the numeric fields in host terms vs. their stored network bytes. */
printf("port (host order) = %u\n", ntohs(a.sin_port));
printf("addr (host order) = 0x%08x (INADDR_LOOPBACK)\n\n",
ntohl(a.sin_addr.s_addr));
dump_bytes("family bytes:", &a.sin_family, sizeof a.sin_family);
dump_bytes("port bytes:", &a.sin_port, sizeof a.sin_port);
dump_bytes("addr bytes:", &a.sin_addr, sizeof a.sin_addr);
dump_bytes("sin_zero bytes:",&a.sin_zero, sizeof a.sin_zero);
/* 3. Prove the struct really works: bind a UDP socket on loopback,
asking the kernel for an ephemeral port (port 0). */
a.sin_port = htons(0);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) { perror("socket"); return 1; }
if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) {
perror("bind");
close(fd);
return 1;
}
/* Read back what the kernel actually assigned. */
struct sockaddr_in got;
socklen_t len = sizeof got;
if (getsockname(fd, (struct sockaddr *)&got, &len) < 0) {
perror("getsockname");
close(fd);
return 1;
}
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &got.sin_addr, ip, sizeof ip);
printf("\nbound to %s:%u (kernel picked the port)\n",
ip, ntohs(got.sin_port));
close(fd);
return 0;
}
#include block — netinet/in.h gives struct sockaddr_in and INADDR_LOOPBACK; arpa/inet.h gives the byte-order helpers and inet_ntop; sys/socket.h gives socket/bind/getsockname; unistd.h gives close.dump_bytes() — a tiny helper that prints the raw bytes of any object. We use it to see that the port and address are stored big-endian, which is otherwise invisible.struct sockaddr_in a; — an uninitialised automatic (stack) variable. Its contents are indeterminate until we clear them.memset(&a, 0, sizeof a); — the mandatory wipe. Clears sin_zero and gives every field a defined value before we touch the three that matter.a.sin_family = AF_INET; — declares this as an IPv4 address so the kernel interprets the following bytes as an IPv4 port/address.a.sin_port = htons(8080); — the port, converted from host to network byte order. On a little-endian CPU 8080 (0x1F90) is stored as the bytes 1f 90.a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); — the address, via the .s_addr sub-field, converted with the 32-bit helper. 127.0.0.1 stores as 7f 00 00 01.sizeof and the ntoh* prints — show the struct size (16) and convert the stored network values back to host order so the printed numbers read normally.dump_bytes calls — reveal the layout: family, then the big-endian port bytes, then the big-endian address bytes, then eight zero padding bytes. This is exactly what travels to the kernel.a.sin_port = htons(0); — we now reuse the struct to actually bind, and port 0 tells the kernel to assign a free ephemeral port for us.socket(AF_INET, SOCK_DGRAM, 0) — from the previous lesson: create a UDP/IPv4 endpoint. We check for -1.bind(fd, (struct sockaddr *)&a, sizeof a) — the payoff: hand the address struct to the kernel with the required cast and length. Errors go through perror.getsockname(fd, ..., &len) — read back the address the kernel actually assigned; len starts as sizeof got and is updated by the kernel.inet_ntop(...) + final printf — format the bound address as text and print the real (non-zero) port the kernel chose, proving the struct was accepted.close(fd) — release the descriptor. The address struct needs no cleanup.1. Forgetting the byte-order conversion on the port.
a.sin_port = 8080; /* WRONG: raw host value */
On a little-endian machine this stores the bytes 90 1f, which the network interprets as port 36895 — you bind/connect to the wrong port and cannot figure out why. Fix:
a.sin_port = htons(8080); /* store big-endian */
2. Wrapping an already-network-order address in htonl.
inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
a.sin_addr.s_addr = htonl(a.sin_addr.s_addr); /* WRONG: double convert */
inet_pton already produces network order; converting again corrupts the address. Fix: use inet_pton's result as-is, and only apply htonl to the INADDR_* constants:
inet_pton(AF_INET, "127.0.0.1", &a.sin_addr); /* done — no htonl */
3. Skipping the zero-initialisation.
struct sockaddr_in a; /* garbage in sin_zero and everywhere */
a.sin_family = AF_INET;
a.sin_port = htons(8080);
/* sin_addr and sin_zero never set */
Uninitialised padding/fields are undefined behaviour and can cause bind to fail with EINVAL. Fix: memset(&a, 0, sizeof a); before setting anything.
4. Passing the wrong length or a stale socklen_t to getsockname.
socklen_t len; /* uninitialised */
getsockname(fd, (struct sockaddr *)&got, &len); /* WRONG */
len is in/out and must start as the size of your buffer. Fix:
socklen_t len = sizeof got;
getsockname(fd, (struct sockaddr *)&got, &len);
bind/connect/getsockname return -1 on failure and set errno. Wrap failures in perror("bind") so you see Address already in use (EADDRINUSE), Invalid argument (EINVAL, often an unzeroed struct or wrong family), or Permission denied (EACCES, e.g. a privileged port <1024).sin_port/sin_addr byte by byte (as the example does). Seeing 90 1f instead of 1f 90 immediately tells you a missing htons.getsockname as ground truth. After binding with port 0, read the struct back to confirm the kernel accepted it and to learn the assigned port. Do the same after connect with getpeername.strace -e trace=bind,connect ./prog shows the exact sockaddr bytes the kernel received and the errno; on macOS use sudo dtruss. This catches byte-order and length bugs the C code hides.valgrind ./prog flags a sockaddr_in that was used before memset ("use of uninitialised value"), pointing straight at a missing zero-init.ss -tulpn / netstat -an confirms what address and port a running server actually bound to — a quick way to catch an accidental 0.0.0.0.struct sockaddr_in starts with indeterminate bytes. Reading or transmitting them before memset is UB and can leak stack data into a syscall. Always zero first.len smaller than sizeof(struct sockaddr_in) can make the kernel read a truncated address; passing a len from the wrong struct is a classic copy-paste bug. Use sizeof a for the concrete IPv4 struct.getsockname/accept write into your buffer. Give them a buffer large enough (struct sockaddr_in, or struct sockaddr_storage if the family may vary) and an in/out socklen_t initialised to that size, or the kernel may write past what you expected.struct sockaddr_in * to struct sockaddr * is explicitly permitted by the sockets API because the layouts are compatible in the family field. Do not, however, cast a smaller/differently-aligned object and claim it is a sockaddr_in.sockaddr_in owns no pointers; the whole thing is a flat value type. The only resource to release is the socket file descriptor via close().inet_ntop. Size the output buffer at least INET_ADDRSTRLEN (16) for IPv4; a short buffer causes truncation or overflow. Never hand inet_ntop a buffer sized by guesswork.sockaddr_in before bind or connect — web servers, databases, game servers, RPC frameworks all start here.127.0.0.1 by default precisely because an accidental 0.0.0.0 bind has caused countless open-database breaches. When you see bind_address/listen config options, they map directly to the constant chosen here.getsockname — the pattern in the example, widely used in unit tests to avoid port collisions.sockaddr_storage for dual-stack code. Real servers that support both IPv4 and IPv6 declare struct sockaddr_storage (big enough for either) and inspect ss_family to decide whether to treat it as sockaddr_in or sockaddr_in6. Learning sockaddr_in is the foundation for that.hton*, prefer inet_pton over the older inet_addr (which cannot report errors cleanly), and always check syscall return values.Fill and print. Write a program that builds a struct sockaddr_in for 127.0.0.1:9000, then prints the port and address back in host order using ntohs/ntohl. Confirm they read as 9000 and 0x7f000001.
Parse from text. Replace the htonl(INADDR_LOOPBACK) line with inet_pton(AF_INET, "127.0.0.1", &a.sin_addr). Check its return value (must be 1) and verify the resulting bytes are identical to the constant version.
Ephemeral bind + read-back. Bind a UDP socket to loopback with sin_port = htons(0), then use getsockname to print the port the kernel assigned. Run it a few times and observe the port changing.
Loopback vs. any experiment. Write two variants that bind a socket to INADDR_LOOPBACK and INADDR_ANY respectively (use a high port like 8080). Use ss -tulpn or netstat -an to observe the difference in the bound address (127.0.0.1 vs 0.0.0.0). Explain in a comment which is safer and why.
Round-trip formatter. Given a sockaddr_in you filled, use inet_ntop to turn sin_addr back into a text string and print "ip:port". Then deliberately omit the memset and run under valgrind to see the uninitialised-value warning, proving why zeroing matters.
struct sockaddr *; for IPv4 you fill a concrete struct sockaddr_in and cast its pointer.memset(&a, 0, sizeof a) first — it clears sin_zero padding and defines every field.sin_family = AF_INET, sin_port = htons(port), sin_addr.s_addr = htonl(INADDR_...) (or inet_pton).htons/htonl, and never double-convert an inet_pton result.INADDR_LOOPBACK (127.0.0.1) is localhost-only and the safe default; INADDR_ANY (0.0.0.0) exposes the service on every interface.(struct sockaddr *)&a with sizeof a; read a socket's address back with getsockname using an in/out socklen_t. The struct owns no memory — only the fd needs close().