Java socket programming lets two programs talk over a network through the java.net package. A server opens a ServerSocket on a port and waits. A client connects with a Socket, and both sides read and write through ordinary Java streams. That's enough to build a working chat app with no framework.
This guide covers the client-server model, IP addresses and ports, then builds the chat step by step: plain text first, then messages as serialized objects, then a server with a thread per client. The code uses Java 17+.
Client-server basics
A standalone program runs entirely on one machine. A client-server program talks to other programs over a network: the server waits for requests, and clients connect to it. A chat app is client-server. Every user runs a client, and one server relays messages between them.
IP addresses, ports and protocols
To reach a program on another machine you need:
- An IP address identifies the machine on the network, for example
192.168.1.20, or127.0.0.1for your own computer. - A port identifies a program on that machine. It's a 16-bit number from 0 to 65535.
| Port range | Name | Use it for your app? |
|---|---|---|
| 0–1023 | Well-known ports (HTTP 80, HTTPS 443, SSH 22) | No, reserved for system services |
| 1024–49151 | Registered ports | Yes, pick one that's free |
| 49152–65535 | Dynamic/ephemeral ports | No, the OS assigns these to outgoing connections |
On Windows, Hyper-V and WSL can reserve blocks of high ports, and binding to one fails with BindException even when nothing is listening (netsh int ipv4 show excludedportrange protocol=tcp lists them). This guide uses port 5050.
A protocol is the set of rules both sides agree on. HTTP, for example, runs on top of TCP, the transport protocol that Socket and ServerSocket use. TCP gives you a reliable, ordered stream of bytes: if the network drops a packet, TCP resends it. Your chat protocol sits on top and decides what those bytes mean.
A socket is one endpoint of a connection, an IP address plus a port. The workflow is always open, send and receive, close.
Java socket programming with java.net
The server:
- Creates a
ServerSocketbound to a port. - Calls
accept(), which blocks until a client connects and returns aSocketfor it. - Reads and writes through that socket's streams, closes it, and goes back to
accept().
The client creates a Socket with the server's host and port (the constructor connects immediately), then uses its streams the same way.
Step 1: a plain-text server and client
The first version sends lines of text. The server prints whatever it receives.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SimpleServer {
public static void main(String[] args) throws IOException {
try (ServerSocket serverSocket = new ServerSocket(5050)) {
System.out.println("Waiting for clients on port 5050...");
while (true) {
try (Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) { // null = client closed the connection
System.out.println(socket.getRemoteSocketAddress() + " says: " + line);
}
}
}
}
}
}import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SimpleClient {
public static void main(String[] args) throws IOException {
String host = args.length > 0 ? args[0] : "127.0.0.1";
try (Socket socket = new Socket(host, 5050);
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true)) {
out.println("Hello from the client");
out.println("สวัสดีครับ");
}
}
}A few details matter here:
- try-with-resources closes the socket and streams even when an exception is thrown.
- Always set the charset. Without
StandardCharsets.UTF_8, Thai text can arrive garbled if the machines use different default encodings. PrintWriter(..., true)enables auto-flush onprintln. Without it, data can sit in a buffer and never reach the server.readLine()returnsnullwhen the client closes the connection. That's how the server knows a client is done.
This server handles only one client at a time: while it reads from the first, the second waits at accept(). A chat needs everyone connected at once.
Step 2: send chat messages as objects
Once a message needs several fields, such as sender, text and time, you can send a whole Java object instead of inventing a text format. Object serialization converts an object into a sequence of bytes that can travel over the network. ObjectOutputStream writes objects, and ObjectInputStream rebuilds them on the other side.
import java.io.Serializable;
import java.time.Instant;
public record ChatMessage(String sender, String text, Instant sentAt) implements Serializable {
}A record keeps the message immutable, and Instant replaces the old java.util.Date. A regular class should also declare private static final long serialVersionUID = 1L; so both sides agree on the class version. Records don't need one.
Two rules keep object streams from hanging:
- Create the
ObjectOutputStreamfirst and flush it before creating theObjectInputStream. The input stream's constructor blocks until it reads a header from the other side. If both sides create their input stream first, both wait forever. - Call
reset()after each message on long-lived connections.ObjectOutputStreamremembers every object it has written so it can send back-references, so withoutreset()memory grows with every message.
Step 3: a chat server with a thread per client
The server accepts connections in a loop and hands each client to its own thread from an ExecutorService. Each handler reads messages from its client and broadcasts them to everyone.
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.Instant;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ChatServer {
static final int PORT = 5050;
// Only these classes may be deserialized; everything else is rejected.
private static final ObjectInputFilter FILTER = ObjectInputFilter.Config.createFilter(
"maxdepth=5;ChatMessage;java.time.Instant;java.time.Ser;!*");
private final Set<ClientHandler> clients = new CopyOnWriteArraySet<>();
public static void main(String[] args) throws IOException {
new ChatServer().start();
}
void start() throws IOException {
ExecutorService pool = Executors.newCachedThreadPool();
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("Chat server listening on port " + PORT);
while (true) {
Socket socket = serverSocket.accept(); // blocks until a client connects
pool.execute(new ClientHandler(socket)); // one thread per client
}
} finally {
pool.shutdownNow();
}
}
void broadcast(ChatMessage message) {
for (ClientHandler client : clients) {
client.send(message);
}
}
static ObjectOutputStream openOutput(Socket socket) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.flush(); // push the stream header now so the peer's ObjectInputStream can start
return out;
}
private class ClientHandler implements Runnable {
private final Socket socket;
private ObjectOutputStream out;
ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (socket;
ObjectOutputStream out = openOutput(socket);
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
in.setObjectInputFilter(FILTER);
this.out = out;
clients.add(this);
broadcast(new ChatMessage("server", socket.getRemoteSocketAddress() + " joined", Instant.now()));
while (true) {
if (in.readObject() instanceof ChatMessage message) {
broadcast(message);
}
}
} catch (EOFException e) {
// the client closed the connection normally
} catch (IOException | ClassNotFoundException e) {
System.err.println("Client error: " + e.getMessage());
} finally {
clients.remove(this);
}
}
synchronized void send(ChatMessage message) {
try {
out.writeObject(message);
out.reset(); // forget previously written objects so memory does not grow
out.flush();
} catch (IOException e) {
clients.remove(this);
}
}
}
}How it fits together:
accept()stays on the main thread. All reading happens in handler threads, so a slow client never blocks new connections.CopyOnWriteArraySetis safe to iterate while other threads add or remove clients, and broadcasts are far more frequent than joins.sendissynchronizedbecause several handlers can broadcast to the same client at once, and concurrent writes would corrupt theObjectOutputStream.- The
ObjectInputFiltermatters because deserializing arbitrary classes from the network is a well-known attack vector. It allows onlyChatMessageand whatInstantneeds.
For the threading side (ExecutorService, thread pools, shutting down cleanly), see Java threads and multithreading.
Step 4: the chat client
The client does two things at once: a background thread prints incoming messages while the main thread reads the keyboard.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class ChatClient {
private static final DateTimeFormatter TIME =
DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault());
public static void main(String[] args) throws IOException {
String host = args.length > 0 ? args[0] : "127.0.0.1";
String name = args.length > 1 ? args[1] : "guest";
try (Socket socket = new Socket(host, 5050);
ObjectOutputStream out = openOutput(socket);
ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
Thread reader = new Thread(() -> {
try {
while (true) {
if (in.readObject() instanceof ChatMessage m) {
System.out.printf("[%s] %s: %s%n", TIME.format(m.sentAt()), m.sender(), m.text());
}
}
} catch (IOException | ClassNotFoundException e) {
System.out.println("Disconnected from server.");
}
});
reader.setDaemon(true);
reader.start();
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
String line;
while ((line = console.readLine()) != null && !line.equals("/quit")) {
out.writeObject(new ChatMessage(name, line, Instant.now()));
out.reset();
out.flush();
}
}
}
private static ObjectOutputStream openOutput(Socket socket) throws IOException {
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
out.flush();
return out;
}
}Typing /quit ends the loop, try-with-resources closes the socket, and the server's handler gets an EOFException and removes the client.
If you build a Swing interface instead of a console client, don't read from the socket on the event dispatch thread, or the window will freeze. Read on a background thread and update components with SwingUtilities.invokeLater. The Java Swing guide covers the UI side.
Running the chat
Put the three files in one folder, then:
javac ChatMessage.java ChatServer.java ChatClient.java
java ChatServer # terminal 1
java ChatClient 127.0.0.1 alice # terminal 2
java ChatClient 127.0.0.1 bob # terminal 3Anything alice types appears in both terminals. Across machines, pass the server's LAN IP to the clients and allow port 5050 through the firewall.
Common socket problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Both sides hang on connect | Both created ObjectInputStream first | Create and flush ObjectOutputStream first |
| Messages never arrive | Output not flushed | flush() or auto-flush PrintWriter |
BindException on start | Port in use or reserved by the OS | Choose another port in 1024–49151 |
| Server memory keeps growing | ObjectOutputStream caching written objects | reset() after each message |
| Garbled Thai text | Platform default charset | Specify UTF-8 explicitly |
| One slow client delays everyone | Broadcast writes directly to each socket | Give each client its own outgoing queue |
FAQ
Should a chat app use TCP or UDP?
TCP, because chat messages must arrive complete and in order. UDP suits data where speed matters more than a lost packet, such as voice.
Which port should I use for a Java socket server?
Any free port between 1024 and 49151. Avoid the well-known ports below 1024 and the dynamic range above 49151.
Is Java serialization safe to use over a network?
Only with a strict ObjectInputFilter and between programs you control. For public or cross-language APIs, send JSON or Protocol Buffers instead.
How many clients can a thread-per-client server handle?
With platform threads, typically thousands rather than tens of thousands, because each thread reserves its own stack. On Java 21+, virtual threads (Executors.newVirtualThreadPerTaskExecutor()) let the same design scale much further.
Takeaways
ServerSocket.accept()waits for clients, and eachSocketgives you an input and output stream.- Use try-with-resources for every socket and stream, and set UTF-8 explicitly for text.
- Handle each client on its own thread so one connection can't block the others.
- With object streams, create and flush the output first,
reset()after each message, and filter what you deserialize.
Browser clients would use WebSockets instead, and at scale you'd add pub/sub across servers. If you're building a real-time product and want an experienced team, Vectorkub can help.
