Skip to content
Java networking 8 min read

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:

ClassRole
ServerSocketOpens a port and waits for incoming client connections (the “listener”)
SocketRepresents 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. Use new 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:

  1. Server bindsnew ServerSocket(port) tells the OS to reserve the port.
  2. Server listensaccept() blocks, queuing incoming connection requests (backlog default: 50).
  3. Client connectsnew Socket(host, port) triggers the TCP handshake.
  4. accept() returns — the server gets a new Socket object for that specific client; ServerSocket continues listening for the next connection.
  5. Data exchange — both sides read/write through streams.
  6. 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) with Executors.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 prefer socket.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 a BufferedOutputStream. 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:

MethodWhat 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 ObjectOutputStream over a socket.

Under the Hood

When new Socket("localhost", 9000) runs, several layers come into play:

  1. DNS resolution"localhost" is resolved to 127.0.0.1 (IPv4) or ::1 (IPv6) using the OS resolver. Remote hostnames are looked up via DNS and briefly cached by the JVM.
  2. TCP three-way handshake — the OS sends SYN → SYN-ACK → ACK. Only after this exchange does the Socket constructor return.
  3. 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+.
  4. Kernel buffers — each socket gets a send buffer and a receive buffer in kernel memory (controlled by setSendBufferSize/setReceiveBufferSize). Calls to write() copy data into the send buffer; the kernel handles actual transmission asynchronously.
  5. Stream wrappingsocket.getInputStream() returns a raw stream backed by the kernel receive buffer. Wrapping it in a BufferedReader adds a userspace buffer (default 8 KB), reducing expensive read() system calls significantly.
  6. accept() queue — the second argument to new 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 confusionsocket.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.
  • ServerSocket not re-usedserver.accept() can be called in a loop on the same ServerSocket. You do not need a new ServerSocket for each client.
  • Networking Basics — an overview of Java’s full networking API, from InetAddress to HttpURLConnection
  • Multithreading — essential for writing servers that handle more than one client at a time
  • Thread Pools & Executors — use ExecutorService to 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/OutputStream API as files, so I/O knowledge transfers directly
Last updated June 13, 2026
Was this helpful?