Socket Programming
Sockets are the fundamental building block of any networked application — they represent the two endpoints of a connection through which data flows. Java’s java.net.Socket and java.net.ServerSocket classes make it straightforward to build anything from a simple chat client to a custom protocol server, all using the familiar InputStream/OutputStream model you already know from Java I/O.
What Is a Socket?
A socket is one end of a two-way communication link between two programs running across a network. Think of it like a phone call: one side dials (the client), the other side picks up (the server), and then both sides can speak and listen simultaneously until one of them hangs up.
In Java, you work with two classes:
| Class | Role |
|---|---|
ServerSocket | Opens a port and waits for incoming client connections (the “listener”) |
Socket | Represents an active connection — used by both the client and, once accepted, the server |
Both sides then communicate by reading from and writing to the socket’s InputStream and OutputStream.
A Simple TCP Server
The server calls server.accept(), which blocks until a client connects. Once connected, it receives a message and sends one back.
import java.io.*;
import java.net.*;
public class SimpleServer {
public static void main(String[] args) throws IOException {
// Bind to port 9000 and start listening
try (ServerSocket serverSocket = new ServerSocket(9000)) {
System.out.println("Server started. Waiting for client...");
// accept() blocks until a client connects
try (Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true)) {
System.out.println("Client connected: " + clientSocket.getInetAddress());
String message = in.readLine();
System.out.println("Received: " + message);
out.println("Hello from server! You said: " + message);
}
}
}
}
Output (server console):
Server started. Waiting for client...
Client connected: /127.0.0.1
Received: Hi server!
Note:
new ServerSocket(9000)binds your server to all available network interfaces on port 9000. Usenew ServerSocket(9000, 50, InetAddress.getByName("localhost"))to restrict it to loopback only.
A Simple TCP Client
The client creates a Socket pointing at the server’s hostname and port. The OS performs the TCP three-way handshake before the constructor returns.
import java.io.*;
import java.net.*;
public class SimpleClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 9000);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
out.println("Hi server!"); // send a message
String response = in.readLine(); // read the reply
System.out.println("Server says: " + response);
}
}
}
Output (client console):
Server says: Hello from server! You said: Hi server!
Tip: Always use try-with-resources for sockets. Forgetting to close a socket leaks an OS file descriptor — a finite resource. At scale, leaked descriptors cause “Too many open files” errors.
The Socket Lifecycle
Understanding the lifecycle helps you write more reliable code:
- Server binds —
new ServerSocket(port)tells the OS to reserve the port. - Server listens —
accept()blocks, queuing incoming connection requests (backlog default: 50). - Client connects —
new Socket(host, port)triggers the TCP handshake. accept()returns — the server gets a newSocketobject for that specific client;ServerSocketcontinues listening for the next connection.- Data exchange — both sides read/write through streams.
- Connection closed — either side calls
socket.close(), sending a TCP FIN to signal end-of-stream.
Handling Multiple Clients
The single-client example above exits after one connection. Real servers spawn a new thread (or use a thread pool) for each accepted connection so they can serve many clients simultaneously.
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class MultiClientServer {
public static void main(String[] args) throws IOException {
// A thread pool of up to 10 worker threads
ExecutorService pool = Executors.newFixedThreadPool(10);
try (ServerSocket serverSocket = new ServerSocket(9000)) {
System.out.println("Multi-client server running on port 9000...");
while (true) {
Socket clientSocket = serverSocket.accept(); // wait for next client
pool.submit(new ClientHandler(clientSocket)); // hand off to a worker
}
}
}
}
class ClientHandler implements Runnable {
private final Socket socket;
ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (socket;
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("[" + socket.getPort() + "] " + line);
out.println("ACK: " + line);
}
} catch (IOException e) {
System.err.println("Connection error: " + e.getMessage());
}
}
}
Note: Java 21’s Virtual Threads make a thread-per-connection model practical at far greater scale. Replace
Executors.newFixedThreadPool(10)withExecutors.newVirtualThreadPerTaskExecutor()and the JVM manages thousands of lightweight threads with minimal overhead.
Setting Timeouts
Without timeouts, a blocked accept() or read() will hang your thread forever. Always set timeouts in production code.
Socket socket = new Socket();
// 5-second connection timeout
socket.connect(new InetSocketAddress("example.com", 9000), 5000);
// 3-second read timeout (throws SocketTimeoutException if no data arrives)
socket.setSoTimeout(3000);
try (BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
String line = in.readLine(); // throws SocketTimeoutException after 3s
} catch (SocketTimeoutException e) {
System.err.println("Read timed out — server may be unresponsive.");
} finally {
socket.close();
}
Warning:
new Socket("host", port)uses the OS default connection timeout (often minutes on Linux). Always prefersocket.connect(new InetSocketAddress(...), timeoutMs)for predictable behavior.
Reading Binary Data
For protocols that send binary frames rather than text lines, use DataInputStream and DataOutputStream instead of readers and writers.
import java.io.*;
import java.net.*;
public class BinaryClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("localhost", 9000);
DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(socket.getOutputStream()));
DataInputStream in = new DataInputStream(
new BufferedInputStream(socket.getInputStream()))) {
out.writeInt(42); // send an integer
out.writeUTF("ping"); // send a UTF-8 string
out.flush();
int code = in.readInt(); // read int from server
System.out.println("Server returned code: " + code);
}
}
}
Tip: Always call
out.flush()after writing when using aBufferedOutputStream. Without it, data can sit in the userspace buffer and never reach the network.
Useful Socket Options
Java exposes several OS-level socket options through the Socket API:
| Method | What it controls |
|---|---|
setSoTimeout(ms) | Read timeout — throws SocketTimeoutException if exceeded |
setTcpNoDelay(true) | Disables Nagle’s algorithm; reduces latency for small, frequent writes |
setKeepAlive(true) | Enables TCP keepalive probes to detect dead connections |
setReceiveBufferSize(n) | Sets the OS receive buffer size (tune for high-throughput streams) |
setSendBufferSize(n) | Sets the OS send buffer size |
setReuseAddress(true) | Allows binding to a port in TIME_WAIT state (useful after server restart) |
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true); // allow immediate restart on same port
serverSocket.bind(new InetSocketAddress(9000));
Sending Java Objects Over a Socket
You can serialize Java objects and send them directly over a socket using Object Streams. Both sides must have access to the class definition.
// Sender side
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
oos.writeObject(new MyMessage("Hello!")); // MyMessage must implement Serializable
oos.flush();
// Receiver side
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
MyMessage msg = (MyMessage) ois.readObject();
System.out.println(msg.getText());
Warning: Java’s built-in serialization has well-known security pitfalls. For production systems, prefer a text or binary protocol (JSON, Protocol Buffers, MessagePack) rather than
ObjectOutputStreamover a socket.
Under the Hood
When new Socket("localhost", 9000) runs, several layers come into play:
- DNS resolution —
"localhost"is resolved to127.0.0.1(IPv4) or::1(IPv6) using the OS resolver. Remote hostnames are looked up via DNS and briefly cached by the JVM. - TCP three-way handshake — the OS sends
SYN → SYN-ACK → ACK. Only after this exchange does theSocketconstructor return. - File descriptor — the OS allocates a file descriptor for the socket. Check your process limit with
ulimit -n; a typical Linux default is 1024. High-concurrency servers often raise this to 65536+. - Kernel buffers — each socket gets a send buffer and a receive buffer in kernel memory (controlled by
setSendBufferSize/setReceiveBufferSize). Calls towrite()copy data into the send buffer; the kernel handles actual transmission asynchronously. - Stream wrapping —
socket.getInputStream()returns a raw stream backed by the kernel receive buffer. Wrapping it in aBufferedReaderadds a userspace buffer (default 8 KB), reducing expensiveread()system calls significantly. accept()queue — the second argument tonew ServerSocket(port, backlog)controls the OS-level connection queue. If the queue fills while your app is busy, the OS silently drops incoming SYNs. For high-throughput servers, set backlog to at least 128.
Understanding this stack explains why connection pooling, timeouts, and thread pools are not optional niceties — they are essential for reliable production networking.
Common Pitfalls
- Forgetting
flush()— buffered writers/streams hold data in memory. Always flush after a write if you need the other side to receive it promptly. - Blocking reads with no timeout — set
setSoTimeout()on every socket you read from. - One thread per connection at scale — the classic model works up to a few thousand connections; beyond that, use a thread pool (see Thread Pools & Executors) or virtual threads.
- Half-close confusion —
socket.shutdownOutput()sends a TCP FIN to signal you are done writing, while still allowing reads. This is how HTTP/1.0 signals end-of-body without closing the connection. ServerSocketnot re-used —server.accept()can be called in a loop on the sameServerSocket. You do not need a newServerSocketfor each client.
Related Topics
- Networking Basics — an overview of Java’s full networking API, from
InetAddresstoHttpURLConnection - Multithreading — essential for writing servers that handle more than one client at a time
- Thread Pools & Executors — use
ExecutorServiceto manage worker threads for incoming socket connections - Virtual Threads — Java 21’s lightweight threads make massive-scale socket servers practical
- Serialization — send Java objects across a socket by serializing them to a byte stream
- Java I/O — sockets expose the same
InputStream/OutputStreamAPI as files, so I/O knowledge transfers directly