JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Writing a TCP Server and Client

Now that a socket is just a file you read and write, let's wire up an actual conversation. We'll walk the exact sequence of calls a TCP server and client each make, see why a server needs two kinds of socket, and meet the first hazard every beginner trips over: TCP is a byte stream, not a message service.

Two roles, two scripts

In the previous guide you met the Berkeley socket API and the big idea that a network connection is handed to you as something that behaves like a file: you read bytes out of it and write bytes into it. That is the gentle illusion. This guide turns the illusion into a working program by walking the exact list of calls each side makes to open a TCP conversation. The shape comes straight from the client-server model: one side waits, the other side initiates.

The asymmetry matters. A server is the side that is up first, sitting at a known address and a known port, waiting for anyone to knock. A client is the side that knows where the server lives and starts the conversation. A web browser is a client; the web server it dials is a server. The two follow different scripts, and the most important thing to grasp is that they do not call the same functions — the server runs a longer ceremony, the client a shorter one.

The server's ceremony: socket, bind, listen, accept

The server's job is to become reachable and then patiently take callers one at a time. It does this in four steps. First it calls `socket()` to create a fresh, unused endpoint — a bare socket with no address yet. Then it calls bind() to nail that socket to a specific local (IP, port) — for example, "port 8080 on all my interfaces" — which is how it declares the address clients should dial. Then listen() flips the socket into passive mode, telling the kernel "this is a doorway; start queuing incoming connection requests for me."

The fourth step is the one beginners find surprising. When the server calls `accept()`, it blocks (sleeps) until a client connection arrives — and when one does, `accept()` returns a brand-new socket dedicated to that single client. So a busy server holds two kinds of socket at once: one listening socket that does nothing but accept newcomers, and one connected socket per active conversation. You talk to each client through its own connected socket and never through the listening one. That separation is what lets the server loop back to `accept()` and welcome the next caller while older conversations keep going.

  SERVER                              CLIENT
  ------                              ------
  s = socket()                        c = socket()
  bind(s, 0.0.0.0:8080)
  listen(s)
  conn = accept(s)  <----- connect(c, 1.2.3.4:8080)
         |                                |
         |   read(conn) / write(conn)  <-> write(c) / read(c)
         |                                |
  close(conn)                         close(c)
  (loop back to accept for next client)

  s    = listening socket (one, passive)
  conn = connected socket (one PER client)
The two scripts side by side. The server's listening socket s only produces connected sockets; all real reading and writing happens on conn (server) and c (client).

The client's shorter path: socket, connect

The client has it easier. It calls `socket()` to make its own endpoint, then connect(), naming the server's (IP, port) it wants to reach. That single call is what kicks off the three-way handshake you met earlier — the "SYN, SYN-ACK, ACK" exchange that synchronizes sequence numbers — under the hood. The client does not call `bind()`, and that omission is doing quiet, useful work.

Because the client did not bind a port, the kernel quietly picks an unused one for it from a high-numbered range — an ephemeral port, a temporary number borrowed just for this connection and released when it ends. This is why a server lives at a fixed, well-known port (a web server at port 80 or 443) but a client's port is some forgettable number like 51324. It also explains how your laptop can open ten tabs to the same site at once: each connection is told apart by its full four-tuple — source IP, source port, destination IP, destination port — and the ten different ephemeral ports keep the ten conversations distinct even though three of the four numbers are identical.

  1. Server: socket() — make a bare endpoint with no address.
  2. Server: bind() to a fixed (IP, port), then listen() to go passive, then accept() and block, waiting for a knock.
  3. Client: socket(), then connect() to the server's (IP, port) — this triggers the three-way handshake.
  4. Handshake completes: the server's accept() wakes up and returns a fresh connected socket for this one client.
  5. Both sides read() and write() bytes over their connected sockets, then close() when done.

The first real trap: TCP is a byte stream, not messages

Here is the mistake almost everyone makes once. You write "HELLO" with one `write()` and "WORLD" with a second, and you expect the other side to read() them back as two tidy messages. TCP makes no such promise. It gives you the byte stream abstraction: a single, gapless river of bytes, with no record of where one write ended and the next began. The receiver might read() "HELLOWORLD" in one gulp, or "HEL" then "LOWORLD", or any other split. The boundaries you wrote in are simply not preserved — TCP is free to merge or chop your data on the way.

This is not a bug; it is the honest consequence of TCP delivering bytes rather than messages. The cure is that the application must do its own message framing — it must build a way to find where each message ends inside the stream. There are two classic recipes: prefix every message with its length ("the next 5 bytes are one message"), or end every message with an agreed delimiter (a newline, say, the way many text protocols do). Either way, the receiver keeps reading and buffering until it has a whole message, then splits it off. The very next guide in this rung is devoted to framing and to UDP, where the trade-off is exactly reversed.

One at a time? Blocking, and what comes next

The simplest server — socket, bind, listen, accept, then serve one client fully before looping back — works, but notice its ceiling. By default every socket call is blocking: `accept()` sleeps until a client knocks, and `read()` sleeps until bytes actually arrive. While the server is blocked reading from a slow client, it cannot accept anyone else. A web server built this naively would serve exactly one visitor at a time, and everyone else would wait in line.

There are three escapes, and they set up the rest of this rung. You can spawn a thread or process per client (simple, but heavy at thousands of clients). You can switch sockets to non-blocking mode so a call returns immediately instead of sleeping. Or — the scalable answer — you can use I/O multiplexing (select, poll, or Linux's epoll) to watch many sockets with a single thread and act only on the ones that are actually ready. Guide 4 of this rung builds exactly that, the technique behind servers that hold tens of thousands of connections at once.

Two more hazards are worth naming now, both saved for guide 5. After you `close()` a connection, the side that closed first lingers for a while in a TIME-WAIT state — a deliberate safety pause that can surprise you when restarting a server ("address already in use"). And TCP may quietly hold back tiny writes to batch them, a behavior called Nagle's algorithm, which helps bulk transfers but can add a frustrating delay to a chatty, interactive protocol. Both are features, not bugs — but knowing they exist is what separates code that works on your laptop from code that works in the world.