Chat App

A simple chat application based on the Client-Server architecture using Java Sockets. This project is intentionally left incomplete to help you learn networking and multi-threading concepts hands-on.


Architecture

+-------------------+          +-------------------+
|   ChatClient      | <------> |   ChatServer      |
|   (Client-Side)   |   TCP    |   (Server-Side)   |
+-------------------+          +-------------------+
                                       |
                              +--------+--------+
                              |  ClientHandler   |
                              |  (Per Client)    |
                              +-----------------+

The project consists of two main parts:

1. Server-Side

ChatServer.java -- The core server

  • Creates a ServerSocket on a specified port and waits for client connections
  • Wraps each new client in a ClientHandler
  • Uses an ExecutorService (Thread Pool) to manage clients concurrently
  • The connectedClients set is wrapped with Collections.synchronizedSet to ensure thread safety
  • TODOs: broadcast(), broadcastSystemMessage(), and removeClient() methods are incomplete

ClientHandler.java -- Manages a single client

  • Implements Runnable so each client runs in its own thread
  • Requests a username upon connection
  • Forwards received messages to the server for broadcasting
  • Provides sendMessage() to send messages back to this specific client

2. Client-Side

ChatClient.java -- The user-facing client

  • Connects to the server and starts a separate thread for receiving messages
  • Reads console input and sends it to the server
  • TODOs: connectToServer(), readUserInputAndSend(), and cleanup() are incomplete

MessageListener.java -- Listens for server messages

  • Implements Runnable to run in a separate thread
  • Continuously prints messages received from the server
  • TODO: run() method is incomplete

Concepts You Will Learn

Concept Description Why It Matters
Socket Programming Communication between programs over a network using ServerSocket and Socket at the TCP level Foundation of all real-world network communication (web, chat, online games)
Multi-threading Running multiple tasks concurrently with Thread, Runnable, and ExecutorService Without it, one slow client could block the entire server
Thread Pool Using Executors.newCachedThreadPool() for efficient thread management Creating a new thread per client is expensive; Thread Pools recycle threads
I/O Streams Reading and writing data with BufferedReader and PrintWriter over the network Learn how data is transferred between different systems
Synchronized Collections Making shared collections thread-safe with Collections.synchronizedSet() When multiple threads access a collection without coordination, data gets corrupted
Daemon Thread A thread that automatically terminates when the program exits via setDaemon(true) No need to manually close it; prevents thread leaks
Client-Server Architecture Separating server and client logic into distinct classes Learn how distributed systems are designed

Completing the Project -- TODOs

Parts of the code are marked with TODO and need to be completed:

ChatServer.java

public void broadcast(String message, ClientHandler sender) {
    synchronized (connectedClients) {
        for (ClientHandler client : connectedClients) {
            if (client != sender) {
                client.sendMessage(message);
            }
        }
    }
}

public void broadcastSystemMessage(String message) {
    synchronized (connectedClients) {
        for (ClientHandler client : connectedClients) {
            client.sendMessage(message);
        }
    }
}

public void removeClient(ClientHandler clientHandler) {
    connectedClients.remove(clientHandler);
    broadcastSystemMessage("[System] " + clientHandler.getUsername() + " left the chat.");
}

ChatClient.java

private void connectToServer() throws IOException {
    socket = new Socket(host, port);
    out = new PrintWriter(socket.getOutputStream(), true);
    in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
}

private void readUserInputAndSend() throws IOException {
    String input;
    while ((input = consoleReader.readLine()) != null) {
        out.println(input);
    }
}

private void cleanup() {
    try {
        if (socket != null && !socket.isClosed()) {
            socket.close();
        }
    } catch (IOException e) {
        System.err.println("Error closing socket: " + e.getMessage());
    }
}

MessageListener.java

@Override
public void run() {
    try {
        String message;
        while ((message = inputReader.readLine()) != null) {
            System.out.println(message);
        }
    } catch (IOException e) {
        System.err.println("Connection lost: " + e.getMessage());
    }
}

How to Run

Prerequisites

  • Java 21+
  • Maven

Compile

mvn clean compile

Start the Server

java -cp target/classes chat.ChatServer

Start a Client (in a separate terminal)

java -cp target/classes chat.ChatClient

You can also specify a custom port and host:

java -cp target/classes chat.ChatServer 12345
java -cp target/classes chat.ChatClient 127.0.0.1 12345

Important Notes

  • Synchronization: Always use synchronized (connectedClients) inside broadcast(). If two threads modify the set concurrently, a ConcurrentModificationException will be thrown.
  • Resource Cleanup: Always close Socket and streams in a finally block or use try-with-resources to avoid resource leaks.
  • Thread Naming: Use setName() on threads for easier debugging.
  • Port Selection: Ports below 1024 are reserved by the OS. Use higher ports (e.g., 12345).
  • Local Testing: Server and client can be tested on the same machine using 127.0.0.1.

Common Mistakes to Understand

Mistake Explanation
Not using synchronized in broadcast Causes ConcurrentModificationException or lost messages
Not closing streams in finally If an exception occurs, the port remains occupied by the OS for a long time
Forgetting to skip the sender in broadcast The sender receives their own message back (echo)
Using new Thread instead of ExecutorService Creates a new thread per client, which is expensive and unmanageable
Not reading continuously from inputReader in MessageListener If the listener thread sleeps, server messages never reach the user
Not using setDaemon(true) When the client exits, the listener thread keeps running and the program won't terminate
Ignoring null from readLine() When the connection is closed, readLine() returns null; without checking, the loop runs forever

Project Structure

chat-app/
├── pom.xml
├── README.md
├── src/
│   └── main/
│       └── java/
│           └── chat/
│               ├── ChatServer.java       # The chat server
│               ├── ClientHandler.java    # Manages a single client
│               ├── ChatClient.java       # The chat client
│               └── MessageListener.java  # Listens for incoming messages
└── target/          # (generated by Maven -- in .gitignore)
S
Description
No description provided
Readme
42 KiB
Languages
Java 100%