From the machine next door to the machine across the world
You finished the last rung with a quiet confidence: two processes on one machine can talk. Pipes, signals, shared memory, and the Unix domain socket all moved bytes from one program to another, and the kernel sat in the middle as the trusted go-between. But every one of those tricks leaned on a hidden assumption — that both processes live under the same kernel, on the same machine. The pipe was a buffer in one kernel; shared memory was one machine's RAM mapped twice. None of it can reach a process running on a different computer, because there is no shared kernel out there to hold the buffer.
That is the wall this rung is built to cross. The guide that closed the IPC rung, local versus network IPC, pointed straight at it: when the other process is on another host, there is no shared memory, no inherited file descriptor, no common parent — only a wire (or a radio link) carrying electrical or optical signals between two independent computers. To make two strangers on two machines cooperate, we need two new things: an agreed-upon set of rules for what the bytes on the wire mean, and a programming object your code can hold to represent "my end of a conversation with that far machine". The rules are a protocol; the object is a socket.
A protocol is nothing exotic — it is an agreement on the format and ordering of messages so that two parties who have never met can still understand each other. Human conversation has protocols too: you say "hello", you wait, the other speaks, you take turns. On a network the agreement must be far more precise, down to which byte means what, because there is no shared context and no chance to ask "sorry, what did you mean?". A protocol is what replaces that missing common ground with written-down rules both sides implement identically.
Why we stack the rules in layers: the TCP/IP model
Getting a byte from your laptop to a server is a stack of hard problems, and the great idea of networking is to not solve them all at once. Instead we split the job into layers, each solving one problem and trusting the layer below to have solved the next one down. This is the abstraction stack you met long ago, applied to the network: the same discipline that let a C function trust malloc() without knowing the heap's internals lets a web browser trust that bytes will arrive without knowing a thing about cables or routers. The agreed-upon stack the entire Internet runs on is the TCP/IP model.
Read the stack from the bottom up and each layer answers exactly one question. The link layer asks "how do I push bits onto this one wire to the next machine?" — Ethernet, Wi-Fi. Above it the internet layer (the IP in TCP/IP) asks "given the whole tangled web of machines, which next hop gets this packet closer to its destination?" — it gives every host an IP address and routes packets toward it, but makes no promise they arrive or arrive in order. The transport layer sits on top and turns IP's unreliable, best-effort packets into something a program can actually use — and that is where TCP and UDP live. Highest of all, the application layer is your protocol: HTTP, SMTP, or whatever rules your own program invents.
application | HTTP, SMTP, your own protocol (what the bytes MEAN) transport | TCP (reliable stream) / UDP (datagrams) internet | IP (addressing + routing of packets, best-effort) link | Ethernet / Wi-Fi (bits onto one physical hop) send -> data goes DOWN the stack, each layer wraps it in a header recv -> data comes UP the stack, each layer strips its own header
Two honest cautions before we go on. First, this four-layer picture is a useful simplification, not gospel — the older OSI model draws seven layers, and real systems blur the boundaries; treat the layers as a way to think, not as crisp physical objects. Second, the layering buys clarity at a small cost: each layer wraps your data in its own header, so a one-byte message you send is really dozens of bytes on the wire. That overhead is almost always worth it, but it is real, and it is why tiny, frequent messages are far less efficient than they look — a point that will matter when we weigh TCP against UDP.
The socket: a file descriptor for a conversation
Now the central trick, and it is a beautiful one because you already own most of it. When your program wants to talk over the network, it asks the kernel for a socket by calling socket(). What comes back is not some strange new kind of object — it is an ordinary file descriptor, a small int, exactly like the ones open() handed you for files and pipe() handed you for pipes. The kernel has wired the far end of that descriptor not to a file on disk but to the transport layer of the network stack. This is everything is a file reaching its grandest payoff: a network connection wears the same descriptor interface as everything else.
Once a socket is connected, the consequence is wonderful: you talk to a machine in Tokyo using the very same read() and write() calls you used on a local file. write(sockfd, buf, n) hands n bytes to TCP, which chops them into packets, hands those to IP, which routes them across the planet; on the far machine read() lifts those same bytes back up the stack into the other program's buffer. (Networking code often prefers the specialized send() and recv() calls, which are read()/write() plus a flags argument for network-only options — but for a connected TCP socket, plain read() and write() work fine.) The years you spent learning the descriptor interface were, it turns out, also networking lessons.
TCP versus UDP: the two transports a socket can speak
When you call socket() you choose which transport-layer protocol the descriptor will speak, and the two everyday choices have very different personalities. TCP gives you a reliable, ordered byte stream. It runs a three-way handshake to set up a connection, numbers every byte, retransmits anything that gets lost, and reorders packets that arrive out of sequence — so on top of IP's unreliable patchwork, TCP hands your program the illusion of a clean, lossless pipe that the bytes flow through in exactly the order you sent them. A TCP socket is what you reach for when correctness matters more than the last millisecond: web pages, files, email, anything where a dropped byte is a bug.
UDP is the opposite bargain: no handshake, no connection, no retransmission, no ordering. You hand it a chunk of bytes called a datagram and it tosses that single packet toward the destination and forgets about it. It may arrive, may not, may arrive after a later one — UDP promises only that if a datagram arrives it arrives whole. In exchange you get lower latency and message boundaries (each datagram is one distinct message, which TCP's stream deliberately does not give you). UDP is the right tool when stale data is worthless anyway — live audio, a game's position updates, DNS lookups — where re-sending a packet from a second ago would be pointless. The whole TCP-versus-UDP trade is reliability against immediacy, and you pick per application.
Client and server: the two roles, and the calls that come next
Even though both ends hold an ordinary socket, the connection is set up asymmetrically, and naming the two roles makes the rest of this rung click into place. One side is the server: it picks a well-known port, settles down, and waits for callers — like a shop that posts its address and opens its doors. The other is the client: it knows the server's address and reaches out to initiate the conversation — like a customer who walks in. The asymmetry is only in the setup; once the connection exists, the two sockets are peers and bytes flow both ways equally.
These two roles drive two different little dances of system calls, and the rest of the rung walks each one slowly. The client's sequence is short: socket() to make the endpoint, then connect() to dial the server's address — and on success you have a connected socket ready for read()/write(). The server's sequence is a touch longer: socket(), then bind() to claim a specific port, then listen() to announce it is open for business, then accept() in a loop to pick up each incoming caller. A subtlety to hold lightly for now: the server ends up with two kinds of socket — one listening socket that only produces new connections, and one connected socket per accepted client that actually carries data.
- Pick the transport. Call socket() choosing TCP (a reliable stream) or UDP (datagrams). You get back a file descriptor — a small int — and nothing has crossed the network yet.
- Server side: bind() the socket to a port to claim an address, listen() to mark it as accepting connections, then accept() to block until a client arrives, which returns a fresh connected socket for that one client.
- Client side: connect() the socket to the server's address. Under the hood this runs the three-way handshake; on success the socket is connected and ready.
- Both sides now read()/write() (or recv()/send()) the connected socket as if it were a file or a pipe, then close() it when the conversation ends.
That is the whole map of the rung. The next guide nails down addresses, ports, and byte order so you can actually fill in a destination; then one guide builds a working TCP client and another a TCP server, end to end and every call checked; and the last takes on UDP and on blocking versus non-blocking sockets — how a server stops freezing on one slow client and learns to juggle many at once. A friendly way to practice all of this without owning two computers is the loopback interface, the address 127.0.0.1, which loops the network stack back to your own machine: client and server run side by side on one laptop, talking over real TCP that never touches a wire. Keep the socket-as-descriptor idea firmly in hand — everything that follows is just filling in the four steps above.