The Socket API

Two programs on two machines want to talk. One is a web browser in a café in Lisbon; the other is a server in a data centre in Virginia. Between them lie a Wi-Fi access point, a home router, a dozen ISP backbones, an ocean of fibre, and a load balancer. And yet the code that opens that conversation is astonishingly small — a handful of function calls. The abstraction that makes this possible, invented for Berkeley Unix in 1983 and essentially unchanged since, is the socket.

A socket is the program's handle on a network connection: you write bytes into one end and they come out the other, on a different computer, in order. It is deliberately shaped like a file — you read() and write() a socket much as you read and write a file — because the Unix designers had already taught a generation of programmers that everything is a file, and a network connection is just a file whose other end is far away. This page is about what that handle really is, the exact sequence of calls that sets one up, and the single most important truth that trips up everyone writing their first networked program: a TCP socket is a stream of bytes, not a stream of messages.

What a socket actually identifies

When your program asks the operating system for a socket and connects it, the kernel creates an entry in its table of open connections. A live TCP connection is pinned down by a 4-tuple — four numbers that, together, are unique across the whole machine:

(\text{source IP},\ \text{source port},\ \text{destination IP},\ \text{destination port})

The IP addresses say which machines; the ports say which program on each machine. A port is a 16-bit number (0–65535) that the transport layer uses to demultiplex arriving bytes to the right socket. This is why a single server machine can hold tens of thousands of simultaneous connections on port 443: they all share the server's IP and port 443, but each has a different client IP-and-port, so every 4-tuple is still distinct. The socket is your program's name for one such tuple.

The lifecycle: two roles, seven calls

A TCP conversation is asymmetric. The server opens a door and waits; the client walks through it. Each role runs a fixed sequence of system calls, and it is worth memorising the shape once — every networked program you ever write is a variation on this skeleton.

Three subtleties hide in that picture. First, accept() does not return the listening socket — it returns a brand-new socket dedicated to this one client, leaving the original free to keep accepting more. Second, accept() and connect() block: the thread stops there until, respectively, a client shows up or the handshake completes. Third, that handshake — SYN → SYN-ACK → ACK — happens inside connect(), invisibly; by the time connect() returns, the two kernels have already agreed sequence numbers and the pipe is live.

A TCP echo server and client

Here is the skeleton in TypeScript-flavoured pseudocode, close to Node's net module but annotated with the underlying calls. Read it as the canonical shape, not as a fragment to run — real sockets need a real network, which our sandbox does not have.

// --- SERVER --- const server = createServer((conn) => { // callback fires once per accept() conn.on("data", (bytes) => { // read(): a chunk of the byte stream conn.write(bytes); // write(): echo it straight back }); conn.on("end", () => conn.close()); // the client sent FIN -> tear down }); server.bind(8080); // bind() to the port server.listen(); // listen(): now accepting // --- CLIENT --- const sock = connect("server.example", 8080); // socket() + connect() + handshake sock.write("hello"); // push 5 bytes into the stream sock.on("data", (reply) => { console.log(reply.toString()); // "hello" comes echoing back sock.close(); // close(): send FIN });

Blink and you miss the whole networking stack: connect hides IP routing and the handshake, write hides segmentation and retransmission, data hides reassembly. That is exactly the point of the abstraction. But one leak in it is unavoidable, and it is the thing to internalise next.

The big one: TCP is a stream, not messages

A beginner writes write("hello") then write("world") and imagines the receiver will get two data events, "hello" and "world". It won't necessarily. TCP does not preserve write boundaries. It guarantees only that the bytes arrive in order; it is free to glue your two writes into one delivery (helloworld), or split one write across two deliveries (hel then lo), depending on buffering, the network's MTU (maximum transmission unit), and Nagle's algorithm. The receiver sees a faucet of bytes, not a stack of envelopes.

The demo below makes this concrete. We send three logical messages, but simulate TCP delivering the combined byte stream in arbitrary chunks. Watch how a naïve "one chunk = one message" reader gets it hopelessly wrong — and why you must impose your own framing.

// Three application-level messages we "send" over one TCP connection. const messages = ["LOGIN alice", "MSG hi there", "LOGOUT"]; // TCP concatenates them into ONE byte stream — write boundaries are gone. const wire = messages.join(""); console.log(`On the wire, ${messages.length} messages became ${wire.length} bytes:`); console.log(` "${wire}"`); // TCP delivers that stream in whatever chunk sizes it likes. We fake a few. const arrivals = ["LOGIN aliceMSG h", "i ther", "eLOGOUT"]; // A NAIVE reader assumes "one recv() == one message". It is wrong: console.log("\nNaive reader (1 chunk = 1 message):"); for (const chunk of arrivals) console.log(` got message: "${chunk}"`); // The FIX: length-prefix each message so the reader knows where each ends. // Frame = 2-digit length + payload. "LOGIN alice" (11 chars) -> "11LOGIN alice". const framed = messages.map((m) => String(m.length).padStart(2, "0") + m).join(""); console.log(`\nLength-prefixed stream: "${framed}"`); // A framed reader recovers the ORIGINAL messages no matter how bytes are chunked: console.log("Framed reader recovers:"); let i = 0; while (i < framed.length) { const len = Number(framed.slice(i, i + 2)); // read the 2-byte length header const payload = framed.slice(i + 2, i + 2 + len); console.log(` message: "${payload}"`); i += 2 + len; // advance past header + payload }

On localhost, with tiny messages and no real latency, TCP very often delivers each write() as exactly one data event. So the naïve reader appears to work perfectly — through development, through testing, through the demo to your boss. Then you deploy across a real network, packets get coalesced or fragmented, two messages arrive stuck together, and the parser explodes in production. This is one of the most common networking bugs in existence, and the cause is always the same: treating a byte stream as if it preserved message boundaries. The cure is framing — length prefixes (as above), or a delimiter like \n, or a self-describing format — applied on every connection from day one.

Blocking, and why one thread isn't enough

Look again at the server skeleton. accept() blocks; read() blocks until bytes arrive; write() can block if the send buffer is full. A single-threaded server that calls accept(), then talks to that one client with blocking reads, is deaf to every other client for the entire duration of the conversation. One slow client — a phone on a train, or a malicious client that opens a connection and never sends anything — freezes the whole server.

There are three classic escapes from this trap, and each is its own lesson:

The socket API you have just met is the foundation all three build on. Master these seven calls and the stream-not-messages truth, and the rest of network programming is choosing how to wait.

The metaphor is an electrical socket in the wall. A program "plugs into" the network through this standard receptacle without caring what is on the other end — a printer, a database, another continent. The name (and the API) came from a 1983 group at UC Berkeley building networking into BSD Unix under a DARPA contract; their design was so clean that Windows' Winsock, decades of Unix, and the guts of every modern language's networking library are all recognisably the same seven calls. Few 40-year-old APIs are still the first thing every programmer reaches for.