AlgoMaster Logo

TCP Socket Programming

Medium Priority30 min readUpdated August 14, 2026
Listen to this chapter
Unlock Audio

A TCP server starts with a surprisingly small set of operations: create a socket, bind it to an address, listen, accept a connection, exchange bytes, and close the connection. A client follows an even shorter path: create a socket, connect, exchange bytes, and close.

The calls are simple. Using them correctly requires a precise understanding of what TCP provides. TCP is a reliable, ordered byte stream. It does not preserve application message boundaries, it may return fewer bytes than a program requested, and a successful write does not mean the peer's application has processed the data.

This chapter builds the same small TCP echo service in Python and Java using only their standard libraries. The service handles one request per connection:

The server accepts clients sequentially so the fundamental socket lifecycle remains visible. The result is not intended to be a high-concurrency production server; it is a correct foundation for understanding TCP network code.

The TCP Server and Client Lifecycles

A TCP server and client perform complementary operations.

Listener stays open for the next clientsocket()bind(local address)listen()connect(server address)accept()new accepted socketsend request bytessend response bytesclose()close()Server applicationListening socketClient socketAccepted socketServer applicationListening socketClient socketAccepted socket
11 / 11
algomaster.io

The server uses two kinds of sockets:

  • The listening socket receives connection requests at a known local address.
  • An accepted socket carries data for one established client connection.

accept() does not transform the listener into a connected socket. It returns a new socket while the listener remains available for future connections.

The client has one connected socket. Its operating system normally assigns the local IP address and an ephemeral port automatically when connect() runs.

Loading simulation...

The Echo Protocol

TCP carries bytes, so the applications must define where each message ends. This example uses a simple length-prefixed protocol.

Every message has two parts:

For the UTF-8 text hello, the payload contains 5 bytes:

The four-byte integer uses network byte order, which is big-endian. The most significant byte appears first.

The protocol accepts payloads up to 64 KiB:

The limit protects the server from blindly allocating an amount of memory chosen by a client. Real protocols must validate lengths before allocating buffers or reading large bodies.

This echo service processes exactly one framed request and one framed response per TCP connection. That rule keeps the example compact while still handling stream framing correctly.

Why One recv() or read() Is Not Enough

A common first attempt assumes that one receive operation returns everything sent by one write:

This call returns up to 65,536 bytes. It may return fewer even when more bytes are on the way.

The same issue applies to the four-byte length field. A receive operation can return one byte now and three bytes later:

TCP preserves byte order, so the pieces remain in the correct sequence. It does not promise how those bytes are divided across receive calls. The division depends on timing, buffering, the operating system, and the network.

Correct stream code therefore repeats reads until it has:

  1. All four bytes of the length prefix.
  2. Exactly the declared number of payload bytes.

If the peer closes the connection before those bytes arrive, the message is incomplete and must not be treated as valid.

Loading simulation...

Python: Reading and Writing Framed Messages

Python's socket module exposes the operating system's socket interface. The following helpers implement the framing protocol.

struct.pack("!I", value) encodes an unsigned four-byte integer. The ! requests network byte order, and I requests an unsigned integer.

receive_exactly() loops because recv() may produce a short read. When recv() returns b"", TCP has reached end-of-stream: the peer closed its sending side and no more bytes will arrive. If the required message is incomplete at that point, the helper raises EOFError.

sendall() keeps sending until all supplied bytes have been accepted by the local socket or an error occurs. In contrast, send() can report that it accepted only part of the buffer, leaving the caller responsible for the remainder.

For an empty payload, receive_exactly(sock, 0) returns immediately. Empty messages are therefore valid under this protocol.

Python TCP Server

Save the following program as tcp_server.py:

The setup calls establish the listener:

AF_INET selects IPv4, and SOCK_STREAM selects a TCP stream socket. bind() associates it with the loopback address and port 5000. Because the service binds to 127.0.0.1, only clients on the same machine can reach it.

listen() changes the socket into a passive TCP listener. The operating system can then queue connection requests until the application calls accept().

The program also sets:

This option makes local development restarts less likely to fail because a recently used address is still involved in TCP cleanup. It does not mean that unrelated servers can freely share the same listening endpoint; address-reuse rules vary by operating system and socket state.

The main loop waits here:

accept() returns:

  • connection: a new connected socket for this client
  • address: the client's IP address and port

The accepted socket receives a ten-second I/O timeout. A client that connects and then stops midway through a message cannot hold this sequential server forever.

Both the listener and accepted sockets use with blocks. Python closes each socket when its block ends, including when an exception occurs.

Python TCP Client

Save this program as tcp_client.py:

socket.create_connection() creates a TCP socket and connects it to the supplied server address. Its five-second timeout bounds connection establishment. After connecting, the program changes the timeout to ten seconds for reads and writes.

The connected socket has two endpoint addresses:

The local output might be:

The exact client port changes because the operating system selects an available ephemeral port.

Running the Python Programs

Start the server in one terminal:

It waits for a connection:

Run the client from another terminal:

The client prints output similar to:

The server reports:

Run the client several times. Each run creates a new TCP connection and normally uses a different local ephemeral port. The server handles one connection, closes its accepted socket, and returns to accept() for the next one.

Press Ctrl+C in the server terminal to stop it.

Java: Reading and Writing Framed Messages

Java's classic blocking socket API lives in java.net, while byte-stream helpers live in java.io.

DataInputStream and DataOutputStream are convenient for this protocol:

readInt() reads a four-byte signed Java int in big-endian order. Valid protocol lengths are non-negative and no larger than 65,536, so negative values are rejected.

readFully(payload) does what a single InputStream.read() cannot promise: it keeps reading until the array is full or throws EOFException if the peer closes too early.

writeInt() produces the same four network-order bytes as Python's struct.pack("!I", length). This shared wire format allows the Python and Java implementations to communicate with each other.

flush() propagates a flush through the output-stream chain. This example does not add a separate BufferedOutputStream, but an explicit flush makes the protocol boundary clear and remains correct if buffering is introduced. It still does not mean the remote application has processed the bytes.

Java TCP Server

Save the following program as TcpServer.java:

ServerSocket represents the listener. Calling bind() associates it with 127.0.0.1:5000 and starts listening for TCP connections.

Each call to:

returns a new connected Socket. The try-with-resources statement closes that client socket after one request and response, even if protocol parsing or I/O fails. The outer try closes the listener if the main method exits.

setSoTimeout(10_000) places a ten-second timeout on blocking reads from the accepted socket. If a connected client stops sending before its frame is complete, the read throws SocketTimeoutException and the server closes that client connection.

Like the Python version, this server is sequential. It does not return to accept() until the current client completes, fails, or times out.

Java TCP Client

Save this program as TcpClient.java:

The client separates connection and I/O timeouts:

The first value limits how long connection establishment may wait. setSoTimeout() limits blocking reads after the connection exists. Java does not use it as a write timeout.

The client's local endpoint contains an automatically selected ephemeral port, while its remote endpoint is the server at port 5000.

Running the Java Programs

Compile both files:

Start the server:

In another terminal, run:

The client prints output similar to:

The leading slash is part of Java's InetSocketAddress string representation. It is not part of the IP address.

Python and Java Can Talk to Each Other

The protocol is independent of the programming language. Both implementations send:

That means either client can communicate with either server:

To verify this, start TcpServer and run tcp_client.py, or start tcp_server.py and run TcpClient.

Interoperability comes from agreeing on the bytes, not from using the same classes or language. The socket APIs differ, but TCP sees only an ordered byte stream.

This is also why protocol details must be explicit. Both sides need to agree on:

  • Field sizes and order
  • Integer byte order
  • Character encoding
  • Message-size limits
  • The number and meaning of messages on a connection

If one side writes a little-endian length or counts characters while the other expects a big-endian byte count, the code compiles but the applications do not speak the same protocol.

Bytes, Text, and Character Counts

Sockets transmit bytes. Text must be encoded before sending and decoded after receiving.

Both clients use UTF-8:

The length prefix contains the number of encoded bytes, not the number of characters.

For ASCII text, the counts are often equal:

For other text, they may differ:

Using a character count as the payload length would cause the receiver to read too few bytes and leave the stream misaligned. Always frame the encoded byte sequence that is actually written to the socket.

Closing and End-of-Stream

Closing a connected TCP socket releases the application's resource and begins orderly connection shutdown.

At the receiving API, orderly end-of-stream appears as:

An empty Python byte string is different from a timeout and different from "no data yet." It means no more stream bytes will arrive from that peer.

The framed readers treat early end-of-stream as an error. If the length says 100 bytes but the peer closes after sending 40, the program has not received a valid message.

Closing after the full response is safe in this example because the protocol defines exactly one exchange per connection. A protocol that permits several requests on one connection needs an explicit rule for when the conversation is complete; it cannot use every message boundary as a reason to close.

Timeouts Bound Waiting

Blocking socket calls are convenient, but unbounded blocking lets a stalled peer consume a connection indefinitely.

This chapter uses separate limits:

A connection timeout bounds how long the client waits to establish the TCP connection. A read timeout bounds how long it waits for stream data after connecting.

The correct values depend on the system. A service on the same machine can use tighter limits than a client communicating through a slow or unreliable wide-area network. Values should reflect measured behavior and the application's latency budget.

A timeout does not prove that the remote process is dead. The response may be delayed by network loss, server overload, a long operation, or an unsuitable deadline. Timeout handling must therefore respect the semantics of the application operation. Retrying a read-only request can be safe, while automatically repeating a payment command can duplicate work unless the protocol provides an idempotency mechanism.

Common Failures and Their Meaning

Socket APIs surface network and protocol failures as return values or exceptions.

Connection refused

The destination host actively reports that nothing is accepting TCP connections at that address and port. Common causes are a stopped server, the wrong port, or binding only to a different local address.

In Python this commonly appears as ConnectionRefusedError. Java commonly throws ConnectException.

Connection timed out

The client did not complete connection establishment within its deadline. Packets may be filtered, the address may be unreachable, or the remote system may not be responding.

Read timed out

The connection exists, but the expected bytes did not arrive before the I/O deadline. The peer may be slow, stalled, overloaded, or sending an incomplete frame.

Connection reset

The peer or an intermediary terminated the connection abruptly. Some bytes may have been exchanged before the reset.

Broken pipe or failed write

The application tried to write after the connection could no longer carry data. Depending on timing, an earlier write may have appeared successful because the local operating system had not yet learned that the peer was gone.

Unexpected end-of-stream

The peer closed cleanly before sending all bytes promised by the frame. TCP delivered the available bytes correctly, but the application message was incomplete.

Invalid length

The four-byte header declares a payload larger than the protocol limit. The receiver rejects it before allocating the declared buffer. This is an application-protocol failure, not a TCP failure.

Code should log enough context to diagnose these cases without logging sensitive payloads. Useful context includes the local endpoint, remote endpoint, operation, timeout, declared message size, and exception type.

Practical Mistakes to Avoid

Calling recv() once for an entire message. TCP can split the stream across any number of reads. Loop until the framing rule is satisfied.

Assuming one send becomes one receive. TCP preserves byte order, not application write boundaries.

Using send() without handling partial progress. In Python, use sendall() when the entire buffer must be submitted or loop over the unsent portion yourself.

Ignoring byte order. Multi-byte integers need an agreed representation. This protocol uses big-endian network byte order.

Framing text by character count. Frame the encoded bytes, because UTF-8 characters can occupy different numbers of bytes.

Trusting a peer-provided length. Validate it before allocating memory or waiting for the declared payload.

Leaving reads without timeouts. A peer can connect and then stop midway through a frame.

Forgetting to close accepted sockets. The listener and every accepted connection are separate resources with separate lifetimes.

Sending data through the listening socket. The listener accepts connections. Application data travels through the connected socket returned by accept().

Treating write success as business success. A socket write does not prove that the remote application processed the request. Use a meaningful application response when confirmation matters.

Binding to the wrong address. 127.0.0.1 permits only local clients. A specific interface address or wildcard bind changes reachability and should be chosen intentionally.

Adding concurrency before defining the protocol. Concurrency does not repair ambiguous framing, unchecked lengths, missing timeouts, or leaked sockets. Make one connection correct first.

Summary

A TCP server creates, binds, and listens on one socket, then receives a connected socket from accept() for each client. A client usually connects from an automatically assigned local endpoint to the server's known endpoint.

Because TCP preserves bytes rather than messages, applications need explicit framing. Length-prefixed protocols require exact-read loops because reads may be short. Python's sendall() and Java data streams simplify complete transfers, but lengths still require validation. End-of-stream before a complete frame is a protocol failure.

Python and Java interoperate only when they agree on wire format, byte order, and text encoding. Timeouts bound stalled peers, while structured cleanup prevents leaks.

Correct TCP programs reason in bytes, validate peer input, and give every socket a clear, finite lifetime.

Quiz

TCP Socket Programming Quiz

5 quizzes