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:
| Class | Role |
ServerSocket | Binds to a port and blocks on accept() until a client connects |
Socket | Represents one end of a TCP connection — used by both client and accepted server side |
InetAddress | Resolves hostnames to IP addresses |
InetSocketAddress | Host + 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:
| Option | Method | Purpose |
SO_TIMEOUT | socket.setSoTimeout(ms) | Read timeout; throws SocketTimeoutException if exceeded |
TCP_NODELAY | socket.setTcpNoDelay(true) | Disable Nagle's algorithm for low-latency messaging |
SO_KEEPALIVE | socket.setKeepAlive(true) | Send TCP keep-alive probes to detect dead peers |
SO_REUSEADDR | server.setReuseAddress(true) | Allow re-bind while port is in TIME_WAIT (important on restart) |
SO_RCVBUF / SO_SNDBUF | socket.setReceiveBufferSize(n) | Tune OS socket buffer sizes for throughput |
SO_LINGER | socket.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) { /* ... */ }
}
| Pitfall | Fix |
| Forgetting to close sockets → file descriptor leak | Always use try-with-resources |
| Reading partial data because TCP split the write | Use a length-prefix framing protocol |
Connection refused immediately | Server not started, wrong port, firewall blocking |
| Port already in use on restart | Call server.setReuseAddress(true) before bind |
| Blocking reads hang forever | Set socket.setSoTimeout(ms) |
| Unbounded threads — OOM under load | Use a fixed-size ExecutorService |
PrintWriter silently swallows exceptions | Check out.checkError() or use streams directly |