Skip to Content
Course content

221: Building a Simple Chat Server with Sockets

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of a socket server like a hotel receptionist. The receptionist doesn't leave the front desk to spend the whole afternoon chatting with one guest; if they did, the lobby would fill up with angry people waiting to check in. Instead, the receptionist stays at the desk, waits for a guest to arrive, and the moment someone walks through the door, they assign a bellhop to take care of that specific person. The bellhop handles the guest's luggage and questions in a private area, leaving the receptionist free to greet the next person who walks in.

In Java, the ServerSocket is your receptionist. It sits on a specific port and listens. When a client connects, serverSocket.accept() triggers, which is essentially the guest walking through the door. But here is the catch: accept() returns a Socket object—your "bellhop." To keep the server responsive, we wrap that bellhop in a new Thread. This allows the receptionist to immediately go back to listening for new connections while the thread handles the actual conversation with the client.

Wiring the Socket Plumbing

To get a chat server running, we need two things: a way to read what the client says and a way to send messages back. We use BufferedReader for the input and PrintWriter for the output. I usually prefer PrintWriter because it has an println() method that feels natural, and it handles the newline characters for us, which is critical for the client to know when a message actually ends.

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

public class SimpleChatServer {
    // We keep a list of all active "bellhops" to broadcast messages to everyone
    private static Set<PrintWriter> clientWriters = new HashSet<>();

    public static void main(String[] args) throws IOException {
        System.out.println("Server starting... waiting for guests.");
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            while (true) {
                // The receptionist waits here until a guest arrives
                Socket clientSocket = serverSocket.accept();
                System.out.println("New guest connected!");
                
                // Hand the guest off to a dedicated thread (the bellhop)
                new Thread(new ClientHandler(clientSocket)).start();
            }
        }
    }

    private static class ClientHandler implements Runnable {
        private Socket socket;

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

        public void run() {
            try {
                // Set up the communication pipes
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                
                synchronized (clientWriters) {
                    clientWriters.add(out);
                }

                String message;
                while ((message = in.readLine()) != null) {
                    System.out.println("Received: " + message);
                    broadcast(message);
                }
            } catch (IOException e) {
                System.out.println("Guest disconnected abruptly.");
            } finally {
                // Clean up when the guest leaves
                // (Logic to remove from clientWriters would go here)
            }
        }

        private void broadcast(String message) {
            synchronized (clientWriters) {
                for (PrintWriter writer : clientWriters) {
                    writer.println(message);
                }
            }
        }
    }
}

Dealing with the Concurrency Headache

You'll notice I used a Set of PrintWriter objects and wrapped the access in a synchronized block. This is where things get real. Since every client is running on their own thread, they are all trying to access the clientWriters list at the same time. Without synchronization, you'll run into a ConcurrentModificationException the second two people try to send a message simultaneously. It's a classic race condition—basically, two bellhops trying to write in the same logbook at the exact same time.

I'll be honest: writing socket code can feel clunky because of the amount of boilerplate required for streams. But once you realize it's just a pipeline of bytes being turned into characters, it clicks. The most important part is ensuring you close your sockets in a finally block or use try-with-resources; otherwise, you'll leak ports, and eventually, your OS will refuse to let you start the server again until you reboot.




📋 Practical Task

Implementing a "/quit" Command for Graceful Disconnection

In the current SimpleChatServer, the only way a client disconnects is by closing the application or losing the connection. This often leaves the server side hanging or throwing a messy IOException.

Your Task: Modify the ClientHandler class to intercept messages. If a client sends the exact string "/quit", the server should:

  • Send a final goodbye message back to that specific client: "You have left the chat. Goodbye!"
  • Remove that client's PrintWriter from the clientWriters set.
  • Break out of the while loop and close the socket connection gracefully.
  • Broadcast a message to all other users: "A user has left the building."
Rating
0 0

There are no comments for now.

to be the first to leave a comment.