TCP (Transmission Control Protocol) provides:

  • Reliability — lost packets are automatically retransmitted.
  • Ordering — bytes arrive in the order they were sent.
  • Flow & congestion control — prevents sender from overwhelming receiver or network.

The Java classes you need are all in java.net:

ClassRole
ServerSocketBinds to a port and blocks on accept() until a client connects
SocketRepresents one end of a TCP connection — used by both client and accepted server side
InetAddressResolves hostnames to IP addresses
InetSocketAddressHost + port pair for binding and connecting

The simplest possible server: accept one connection, echo every line back, then close.

import java.io.*; import java.net.*; public class EchoServer { public static void main(String[] args) throws IOException { // bind to port 9000, backlog = 50 pending connections try (ServerSocket server = new ServerSocket(9000, 50)) { System.out.println("Listening on port 9000..."); try (Socket client = server.accept()) { // blocks here System.out.println("Connected: " + client.getRemoteSocketAddress()); var in = new BufferedReader(new InputStreamReader(client.getInputStream())); var out = new PrintWriter(client.getOutputStream(), true); String line; while ((line = in.readLine()) != null) { out.println("ECHO: " + line); } } } } }

And the matching client:

import java.io.*; import java.net.*; public class EchoClient { public static void main(String[] args) throws IOException { try (Socket socket = new Socket("localhost", 9000); var out = new PrintWriter(socket.getOutputStream(), true); var in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) { out.println("Hello, server!"); System.out.println(in.readLine()); // ECHO: Hello, server! } } }

A single-threaded server can only serve one client at a time. The classic fix is to spawn a thread for each accepted connection.

import java.io.*; import java.net.*; public class ThreadedEchoServer { public static void main(String[] args) throws IOException { try (ServerSocket server = new ServerSocket(9000)) { System.out.println("Threaded echo server started on port 9000"); while (true) { Socket client = server.accept(); // do NOT close here new Thread(() -> handle(client)).start(); } } } private static void handle(Socket client) { try (client; var in = new BufferedReader(new InputStreamReader(client.getInputStream())); var out = new PrintWriter(client.getOutputStream(), true)) { System.out.println("New client: " + client.getRemoteSocketAddress()); String line; while ((line = in.readLine()) != null) { out.println("ECHO: " + line); } } catch (IOException e) { System.err.println("Client error: " + e.getMessage()); } } } Spawning an unbounded thread per connection does not scale. For high concurrency use a thread pool (see below) or switch to NIO non-blocking I/O.

Cap the concurrency by submitting each accepted connection to a fixed-size thread pool.

import java.io.*; import java.net.*; import java.util.concurrent.*; public class PooledEchoServer { private static final int PORT = 9000; private static final int THREADS = 50; public static void main(String[] args) throws IOException { ExecutorService pool = Executors.newFixedThreadPool(THREADS); try (ServerSocket server = new ServerSocket(PORT)) { System.out.printf("Pool server on port %d (threads=%d)%n", PORT, THREADS); // register shutdown hook for graceful stop Runtime.getRuntime().addShutdownHook(new Thread(() -> { pool.shutdown(); System.out.println("Pool shut down."); })); while (!Thread.currentThread().isInterrupted()) { Socket client = server.accept(); pool.submit(() -> handle(client)); } } } private static void handle(Socket client) { try (client; var in = new BufferedReader(new InputStreamReader(client.getInputStream())); var out = new PrintWriter(client.getOutputStream(), true)) { String line; while ((line = in.readLine()) != null) { out.println("ECHO: " + line); } } catch (IOException e) { System.err.println(e.getMessage()); } } }

Important socket options to set before connecting or accepting:

OptionMethodPurpose
SO_TIMEOUTsocket.setSoTimeout(ms)Read timeout; throws SocketTimeoutException if exceeded
TCP_NODELAYsocket.setTcpNoDelay(true)Disable Nagle's algorithm for low-latency messaging
SO_KEEPALIVEsocket.setKeepAlive(true)Send TCP keep-alive probes to detect dead peers
SO_REUSEADDRserver.setReuseAddress(true)Allow re-bind while port is in TIME_WAIT (important on restart)
SO_RCVBUF / SO_SNDBUFsocket.setReceiveBufferSize(n)Tune OS socket buffer sizes for throughput
SO_LINGERsocket.setSoLinger(true, 0)RST instead of FIN on close — avoids TIME_WAIT accumulation
ServerSocket server = new ServerSocket(); server.setReuseAddress(true); server.bind(new InetSocketAddress(9000)); Socket client = server.accept(); client.setSoTimeout(30_000); // 30 s read timeout client.setTcpNoDelay(true); client.setKeepAlive(true);

For binary protocols use DataInputStream and DataOutputStream, which can read/write primitives in a defined byte order.

// Server: read a 4-byte length prefix then a UTF-8 message try (Socket client = server.accept(); var din = new DataInputStream(client.getInputStream()); var dout = new DataOutputStream(client.getOutputStream())) { int length = din.readInt(); // 4-byte big-endian length byte[] body = din.readNBytes(length); String msg = new String(body, StandardCharsets.UTF_8); System.out.println("Received: " + msg); // echo back byte[] response = ("ACK: " + msg).getBytes(StandardCharsets.UTF_8); dout.writeInt(response.length); dout.write(response); dout.flush(); } Always define a framing protocol (length prefix, delimiter, fixed width) before sending data over a raw TCP stream. TCP may split or coalesce writes — you cannot assume one write() = one read().

To stop a server cleanly: set a flag, interrupt the accept loop by closing the ServerSocket, then await thread pool termination.

public class GracefulServer { private final ServerSocket serverSocket; private final ExecutorService pool = Executors.newFixedThreadPool(20); private volatile boolean running = true; public GracefulServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() { while (running) { try { Socket client = serverSocket.accept(); pool.submit(() -> handle(client)); } catch (SocketException e) { if (!running) break; // expected on shutdown e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } public void stop() throws IOException, InterruptedException { running = false; serverSocket.close(); // unblocks accept() pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS); } private void handle(Socket client) { /* ... */ } }
PitfallFix
Forgetting to close sockets → file descriptor leakAlways use try-with-resources
Reading partial data because TCP split the writeUse a length-prefix framing protocol
Connection refused immediatelyServer not started, wrong port, firewall blocking
Port already in use on restartCall server.setReuseAddress(true) before bind
Blocking reads hang foreverSet socket.setSoTimeout(ms)
Unbounded threads — OOM under loadUse a fixed-size ExecutorService
PrintWriter silently swallows exceptionsCheck out.checkError() or use streams directly