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.
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:
The
SOCK_STREAM, backed by
SOCK_DGRAM, backed by
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.
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.
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.
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
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.
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:
accept() in a loop, hand each
new socket to a fresh
select/epoll, and service only those. One thread,
tens of thousands of connections. This is how
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.