Skip to main content

Ch 16.3 TCP Socket Programming

TCP (Transmission Control Protocol) socket programming implements reliable, connection-based 1-to-1 communication between a client and server — similar to a telephone call where both parties establish a direct line before speaking.

Network communication over TCP ultimately uses the I/O streams from Chapter 15. A socket acts as the endpoint (connection terminal) for these streams.

1. TCP Socket Core Structure

TCP communication requires clearly defined roles on each side:

  1. Server (ServerSocket): Opens a specific port and waits (accept()) for incoming connections. When a client connects, it creates a dedicated Socket for 1-to-1 communication with that client.
  2. Client (Socket): Connects to the server using the server's IP address and port number.
Server                              Client
| |
| ServerSocket(port) |
| accept() -- waiting... |
| new Socket(ip, port) --> connect
| accept() returns Socket <--------|
| |
| InputStream / OutputStream | InputStream / OutputStream
|<--- send/receive via streams ---->|
| |
| close() | close()

2. Simple Echo Server

The server receives a message from the client and echoes it back.

Server

import java.io.*;
import java.net.*;

public class TcpServer {
public static void main(String[] args) {
try {
// 1. Create a ServerSocket listening on port 7777
ServerSocket serverSocket = new ServerSocket(7777);
System.out.println("Server started. Waiting for clients...");

// 2. Block until a client connects; returns a dedicated Socket
Socket socket = serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());

// 3. Wrap the socket's streams for text communication
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

// 4. Read the client's message and echo it back
String clientMsg = in.readLine();
System.out.println("Received: " + clientMsg);

out.println("Echo: " + clientMsg);

// 5. Clean up
in.close(); out.close();
socket.close();
serverSocket.close();

} catch (IOException e) {
e.printStackTrace();
}
}
}

Client

import java.io.*;
import java.net.*;

public class TcpClient {
public static void main(String[] args) {
try {
// 1. Connect to server at localhost:7777
Socket socket = new Socket("127.0.0.1", 7777);
System.out.println("Connected to server.");

// 2. Set up streams
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));

// 3. Send a message
out.println("Hello from TCP client!");

// 4. Receive the echo
String reply = in.readLine();
System.out.println("Server replied: " + reply);

// 5. Clean up
in.close(); out.close();
socket.close();

} catch (IOException e) {
e.printStackTrace();
}
}
}

3. Multi-Client Server with Threading

A single-threaded server can only handle one client at a time. To support multiple concurrent clients, spawn a new thread for each connection.

import java.io.*;
import java.net.*;

public class MultiClientServer {

// Handler for a single client connection
static class ClientHandler implements Runnable {
private final Socket socket;

ClientHandler(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
String clientId = socket.getInetAddress() + ":" + socket.getPort();
System.out.println("[" + clientId + "] Connected.");

try (
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
String line;
while ((line = in.readLine()) != null) {
System.out.println("[" + clientId + "] > " + line);
if ("quit".equalsIgnoreCase(line)) {
out.println("Goodbye!");
break;
}
out.println("Echo: " + line.toUpperCase());
}
} catch (IOException e) {
System.out.println("[" + clientId + "] Disconnected: " + e.getMessage());
} finally {
try { socket.close(); } catch (IOException ignored) {}
System.out.println("[" + clientId + "] Connection closed.");
}
}
}

public static void main(String[] args) throws IOException {
int port = 7777;
System.out.println("Multi-client server listening on port " + port);

try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) { // accept connections indefinitely
Socket clientSocket = serverSocket.accept();
// spawn a new thread for each client
Thread thread = new Thread(new ClientHandler(clientSocket));
thread.start();
}
}
}
}

4. Thread Pool Server with ExecutorService

Creating a new thread per connection is wasteful under heavy load. Use a thread pool instead.

import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class ThreadPoolServer {

static class ClientHandler implements Runnable {
private final Socket socket;
private final int clientId;

ClientHandler(Socket socket, int clientId) {
this.socket = socket;
this.clientId = clientId;
}

@Override
public void run() {
System.out.println("[Client-" + clientId + "] Handling on thread: "
+ Thread.currentThread().getName());

try (
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
Socket s = socket // auto-close
) {
out.println("Welcome, Client-" + clientId + "! Type 'bye' to exit.");
String line;
while ((line = in.readLine()) != null) {
if ("bye".equalsIgnoreCase(line.trim())) {
out.println("Bye!");
break;
}
out.println("[Server] " + line);
}
} catch (IOException e) {
System.out.println("[Client-" + clientId + "] Error: " + e.getMessage());
}
System.out.println("[Client-" + clientId + "] Done.");
}
}

public static void main(String[] args) throws IOException {
int port = 8888;
int poolSize = 10; // handle up to 10 concurrent clients

ExecutorService pool = Executors.newFixedThreadPool(poolSize);
System.out.println("Thread-pool server on port " + port + " (pool=" + poolSize + ")");

int clientCount = 0;
try (ServerSocket serverSocket = new ServerSocket(port)) {
while (true) {
Socket socket = serverSocket.accept();
pool.submit(new ClientHandler(socket, ++clientCount));
}
} finally {
pool.shutdown();
}
}
}

5. Simple Chat Server

A practical example where connected clients can broadcast messages to each other.

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class ChatServer {
static final Set<PrintWriter> clients = ConcurrentHashMap.newKeySet();

static void broadcast(String message) {
clients.forEach(writer -> writer.println(message));
System.out.println("[Broadcast] " + message);
}

static class ChatClientHandler implements Runnable {
private final Socket socket;

ChatClientHandler(Socket socket) { this.socket = socket; }

@Override
public void run() {
try (
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
clients.add(out);
out.println("Connected to chat! There are " + clients.size() + " user(s) online.");
broadcast("A new user joined. (" + clients.size() + " users)");

String line;
while ((line = in.readLine()) != null) {
if ("quit".equalsIgnoreCase(line.trim())) break;
broadcast("[" + socket.getPort() + "] " + line);
}
} catch (IOException e) {
System.out.println("Client error: " + e.getMessage());
} finally {
// Remove client's writer on disconnect
try (socket) {
// socket closes here
} catch (IOException ignored) {}
broadcast("A user disconnected. (" + (clients.size() - 1) + " users left)");
}
}
}

public static void main(String[] args) throws IOException {
int port = 9999;
ExecutorService pool = Executors.newCachedThreadPool();
System.out.println("Chat server on port " + port);

try (ServerSocket server = new ServerSocket(port)) {
while (true) {
Socket client = server.accept();
pool.submit(new ChatClientHandler(client));
}
}
}
}

6. Reliable try-with-resources Pattern

import java.io.*;
import java.net.*;

public class SafeSocketExample {
public static void main(String[] args) {
// Server socket auto-closes with try-with-resources
try (ServerSocket serverSocket = new ServerSocket(7070)) {
serverSocket.setSoTimeout(30_000); // stop waiting after 30 seconds

try (Socket socket = serverSocket.accept()) {
socket.setSoTimeout(10_000); // read timeout per operation

try (
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
) {
String msg = in.readLine();
out.println("Received: " + msg);
}
}

} catch (SocketTimeoutException e) {
System.out.println("Timed out waiting for connection.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
tip

Pro tip: When a server needs to handle many concurrent clients efficiently, consider Java NIO (non-blocking I/O) with Selector and SocketChannel, or frameworks like Netty or Spring WebFlux. Traditional thread-per-connection servers are simple but don't scale beyond a few thousand concurrent connections. Thread pool servers with ExecutorService are a good practical middle ground for most applications.