Java Sockets

Multi-Client Chat Server Part 1

Goal

  • Build a multi-client chat server where any number of clients can connect and exchange messages

  • Use ObjectInputStream and ObjectOutputStream for all data transfer over the socket

    • These classes send Java objects directly over the wire - a String is a Java object, so we can use them immediately

    • This sets the foundation for Part 2 where we replace raw strings with a structured Payload class

  • Support a /users command - the server generates the current user list and sends it back to the requesting client

  • Prep

    • git checkout main (ensure you’re on the main branch)

    • git pull origin main (pull any latest changes)

    • git checkout -b Module04-Sockets-MCCS-Part1 (create a new branch for this lesson)

Architecture Overview

  • There are two programs: a Server and one or more instances of Client

  • The Server runs continuously, accepting new Client connections

  • Each connected Client gets its own ServerThread on the Server - this is how the Server handles many clients at the same time

  • Clients send strings to the Server; the Server broadcasts them to all connected Clients

Diagram

Background: Threads

  • If you haven’t read the sockets overview yet, review the Blocking Operations and Threads section first - this lesson assumes you understand what blocking is and why threads are needed

  • Remember: A thread is a single flow of execution

  • Here is how threads apply specifically to this codebase:

    • The Server runs accept() on the main thread, hands each new connection off to a new ServerThread, and immediately goes back to waiting for the next one

    • Each ServerThread blocks on readObject() independently - one slow or disconnected client does not hold up any of the others

    • The Client runs listenToServer() in a background thread so it can receive messages while simultaneously reading keyboard input on the main thread

  • Resources:

Communication Patterns

  • When sending data across a network, there are three distinct patterns:

    • Unicast - one sender, one specific receiver

      • Example: the server sends the user list only to the client who requested it

    • Broadcast - one sender, all receivers unconditionally

      • Example: a chat message is relayed to every connected client regardless of any grouping

      • This is the pattern this lesson implements

    • Multicast - one sender, a subscribed group of receivers

      • Example: a message sent only to clients who have joined a specific room or group

  • Using the correct term matters - calling a broadcast a "multicast" implies group subscription logic that does not exist yet

Helper Files

Constants.java

  • An abstract class holding shared constant values used across all classes

    • Making it abstract prevents instantiation - no one should ever do new Constants()

  • Properties:

    • COMMAND_TRIGGER - the string prefix that marks a message as a command (e.g., "[cmd]")

    • DEFAULT_CLIENT_ID - value of -1L, used to represent an unassigned client id

Centralizing constants avoids "magic strings" - hardcoded text scattered across files that is easy to mistype and hard to maintain

TextFX.java

ServerThread

  • ServerThread extends Thread, making each instance its own independent flow of execution

  • Every connected Client gets exactly one ServerThread on the Server side - a 1:1 relationship

  • It is responsible for all sending and receiving with its specific Client

  • The following subsections cover the properties and methods of ServerThread in detail, but the overall flow is:

    1. The Server accepts a new connection and creates a ServerThread for it

    2. The ServerThread opens its streams and enters a loop blocking on readObject()

    3. When a message is received, it checks if it’s a command; if not, it passes it to the Server to broadcast to all clients

    4. If the client disconnects or an error occurs, it cleans up and exits

ServerThread Properties

  • Socket named client - the communication channel to this specific connected client

  • volatile boolean named isRunning - controls the read loop

    • Without volatile, the JVM is allowed to cache a field’s value per-thread for performance - one thread could set it to false but another thread might still see the stale true value from its own cache

    • volatile forces every read to go to main memory, so all threads immediately see the latest value

    • This matters because disconnect() can be called from a different thread than the one running run()

  • ObjectOutputStream named out - used to send data back to this client

    • Stored as a field so sendToClient() can access it after it’s initialized inside run()

  • Server reference named server - provides access to server-level actions like handleMessage()

  • long named clientId - a unique identifier for this client; currently set to this.threadId()

  • Consumer<ServerThread> named onInitializationComplete - a callback to notify the Server once this thread is fully ready to receive data

    • Consumer<T> is a functional interface from java.util.function - it holds a reference to a method that accepts one argument and returns nothing

    • Here it stores a reference to the Server’s `onServerThreadInitialized() method, to be called when setup is done

ServerThread Constructor

  • Takes the Socket, a Server reference, and the callback

  • Uses Objects.requireNonNull() to validate each argument

    • This causes a clear, immediate failure if something is passed incorrectly rather than crashing later with a confusing error

Create ObjectOutputStream before ObjectInputStream on both sides of the connection. The constructors exchange stream header bytes, and if both sides try to read first, the program will deadlock - each waiting for the other to send first.

ServerThread: run()

  • Called automatically when serverThread.start() is called by the Server

  • Opens the ObjectOutputStream first, then ObjectInputStream, using try-with-resources so both close automatically when the block exits

    • try-with-resources automatically calls close() on any declared resource when the block exits, whether it exits normally or due to an exception - no manual cleanup needed for the streams

  • Sets isRunning = true and fires onInitializationComplete to notify the Server

  • Enters a loop blocking on in.readObject():

    • If the result is null, the client disconnected - throw an IOException to break cleanly

    • Otherwise, pass the received string to processPayload()

    • A ClassCastException here means the other side sent an unexpected type - log it but continue

    • An IOException during read usually means the connection dropped - break out of the loop

  • The finally block always runs, ensuring cleanup() is called even on unexpected exits

    • finally runs whether the try block completes normally, throws an exception, or hits a break - it is the guaranteed cleanup point

ServerThread: processPayload() and processCommand()

  • processPayload(String incoming) is the entry point for all data received from the Client

    • Passes the incoming string to processCommand() first

    • If processCommand() returns false, the message was not a command, so passes it to server.handleMessage(this, incoming) to be broadcast to all clients

  • processCommand(String message) checks for the Constants.COMMAND_TRIGGER prefix

    • Expected format: [cmd],commandName or [cmd],commandName,data

    • Splits on , to extract the command name at index 1

    • Recognized commands:

      • disconnect - calls server.handleDisconnect(this) to cleanly remove this client

      • users - calls server.handleGetUserList(this) to send the user list back to just this client

    • Returns true if a command matched and was processed, false otherwise

This [cmd] string format is intentionally simple for now. In Part 2 it is replaced by a Payload class which is more structured and easier to extend.

ServerThread: sendToClient()

  • Accepts a String and writes it to out via out.writeObject()

  • Calls out.flush() immediately after to push the data out of any internal buffer

  • Returns true on success, false if an IOException occurs

    • A failed write almost always means the client disconnected - the run() loop will detect the broken state and the finally block handles cleanup

ServerThread: disconnect() and cleanup()

  • disconnect() - intended to be called from outside this thread (e.g., by Server) to stop it

    • Checks isRunning first to prevent running twice if called repeatedly

    • Sets isRunning = false, then calls this.interrupt() to wake up the blocking readObject() call

    • Does not call cleanup() directly - the finally block in run() always runs after the loop exits and handles cleanup there

  • cleanup() - closes the client Socket

    • The streams themselves are closed automatically by the try-with-resources in run()

  • ServerThread.java: https://github.com/MattToegel/IT114-2026/blob/Module04-Sockets-MCCS-Part1/M4/MCCS/Part1/ServerThread.java

Server

  • The Server class is the central coordinator - it manages all ServerThreads and routes data between them

  • Runs on the main thread; does not extend Thread

Server Properties

  • int named port - the port to listen on (default 3000)

  • ConcurrentHashMap<Long, ServerThread> named connectedClients - maps each clientId to its ServerThread

    • A regular HashMap is not safe when multiple threads access it simultaneously

    • ConcurrentHashMap handles concurrent reads and writes without requiring manual locking on every access

    • Oracle JDK 21 - ConcurrentHashMap

  • boolean named isRunning - controls the main accept loop

Server: start()

  • Creates a ServerSocket bound to the specified port - the OS will reject connections to any other port

  • Loops calling serverSocket.accept() (blocking) to wait for the next incoming connection

  • On a new connection:

    • Creates a new ServerThread, passing the socket, this, and the this::onServerThreadInitialized callback

      • this::onServerThreadInitialized is a method reference - shorthand for (st) → this.onServerThreadInitialized(st); it passes the method itself as a value without calling it yet

    • Calls serverThread.start() to begin the thread

    • The Server does not add the ServerThread to connectedClients yet - that happens in the callback after initialization succeeds

      • Important: run() must open the streams before anyone can send data to this client. Adding to the map before the streams are ready would cause a NullPointerException if another client’s broadcast arrived too early

Server: onServerThreadInitialized()

  • The callback fired by a ServerThread once its streams are open and it’s ready to send/receive data

  • Adds the ServerThread to connectedClients using clientId as the key

  • Calls broadcast(null, …​) to send a join message to all currently connected clients

  • This runs on the new `ServerThread’s own thread, not the main Server thread

Server: broadcast()

  • Sends a formatted message to every entry in connectedClients - this is a broadcast (one sender, all receivers unconditionally)

  • Builds the sender label: User[123] for a client, Server if sender is null

  • Iterates using connectedClients.values().removeIf(…​):

    • removeIf() accepts a lambda - an inline function written as condition → result - and calls it once per entry; returning true removes that entry, false keeps it

    • Calls sendToClient() on each ServerThread

    • If the send fails, removeIf() removes that entry from the map (the lambda returns true on failure)

    • This cleanly prunes disconnected clients during the broadcast without a separate pass

  • Marked synchronized - multiple ServerThreads can call methods on Server at the same time, and synchronized ensures only one executes the method at a time, preventing two threads from modifying connectedClients simultaneously

    • All handle* methods and broadcast() are synchronized on the same Server instance, so they form a single lock - only one can run at a time across all of them

Server: handleGetUserList()

  • Called when a client sends the /users command

  • Builds a formatted string listing all connected clientId values from connectedClients

    • The requesting client’s own entry is tagged with (you) so they can identify themselves in the list

  • Sends the result only to the requesting ServerThread using serverThread.sendToClient() directly

    • This is a unicast (one sender, one receiver) - only the client who asked sees the list

  • Marked synchronized since it reads from connectedClients

Server: handleMessage() and handleDisconnect()

Client

  • The Client class is the user-facing program

  • Two blocking operations need to run at the same time:

    • Reading incoming messages from the server

    • Reading keyboard input from the user

  • These are split into separate threads so neither blocks the other

Client Properties

  • Socket named server - the connection to the Server

    • Named from the client’s perspective: this socket represents the server end of the channel

  • ObjectOutputStream named out and ObjectInputStream named in - the communication channels

  • Pattern references ipAddressPattern and localhostPattern - regex patterns to recognize a /connect command

  • volatile boolean named isRunning - shared between the main thread and the listenToServer() background thread

Client: Command Enum

  • A Command enum defines the recognized client-side commands

    • Makes command matching cleaner and less error-prone than comparing raw strings with equals()

    • Values: CONNECT, DISCONNECT, QUIT, USERS, REVERSE

  • processClientCommand() uses this enum to route to the correct action

Client: connect()

  • Takes a host string and a port number

  • Creates a Socket to the Server

  • Opens ObjectOutputStream first, then ObjectInputStream

    • Order must match the server - both sides create output before input

  • Starts listenToServer() in a background thread using CompletableFuture.runAsync()

  • Returns true if the connection succeeded

Client: listenToServer()

  • Runs in the background thread started by connect()

  • Loops on in.readObject() - blocks until the server sends something

  • Prints each received string to the console

  • On null or any IOException, exits the loop and calls closeServerConnection() in a finally block

Client: listenToInput()

  • Runs as the primary input handler (inside a CompletableFuture in start())

  • Loops reading from Scanner(System.in) - each nextLine() call blocks until the user presses Enter

  • Passes each line to processClientCommand()

    • If no command matched, calls sendToServer() to send it as a chat message

Client: processClientCommand()

  • Uses the Command enum to identify the user’s intent

  • Recognized commands:

    • CONNECT (/connect host:port) - extracts host/port using regex and calls connect()

    • QUIT (/quit) - calls close() to terminate the program

    • DISCONNECT (/disconnect) - sends [cmd],disconnect to the Server

    • USERS (/users) - sends [cmd],users to the Server, which replies with the user list

    • REVERSE (/reverse text) - sends the text to the Server to be reversed and broadcast back

  • Returns true if a command was processed, false if it was a regular message

Client: sendToServer()

  • Calls out.writeObject(message) and out.flush()

  • Prints a helpful hint if not yet connected

Client: close() and closeServerConnection()

Running the Sample

  • Note: Use the CLI instead of the IDE’s "run" button

  • Assumes code is under a M4/MCCS/Part1/ folder structure (adjust as needed):

    • javac M4/MCCS/Part1/Server.java (compile the server and its dependencies)

    • java M4.MCCS.Part1.Server (run the server - default port 3000)

    • javac M4/MCCS/Part1/Client.java (compile the client)

    • java M4.MCCS.Part1.Client (run the client)

  • Open at least two terminal windows and start a Client in each

    • Type /connect localhost:3000 in each to connect

    • Type a message and press Enter - both clients should see it

    • Type /users to see the current list of connected client ids

    • Type /disconnect to disconnect from the server

Run the Server first. A Client that tries to connect before the Server is listening will immediately fail.
  • Remember: javac uses the file system path with slashes; java uses the package path with dots

Summary

  • A multi-client chat server using ObjectInputStream and ObjectOutputStream for all data transfer

  • Key concepts covered:

    • A Thread per client (ServerThread) allows the Server to handle many connections in parallel

    • ConcurrentHashMap safely tracks connected clients across multiple threads

    • The Consumer<ServerThread> callback pattern lets ServerThread notify Server when it is ready without Server having to poll for readiness

    • Broadcasting with removeIf() cleanly prunes failed connections during message delivery

    • Unicast (single client reply) and broadcast (all clients) as two distinct patterns - multicast (subscribed group) will be introduced when rooms are added

  • Current limitations (to be addressed in later parts):

    • Clients are only identified by a thread-generated id - there are no usernames yet

    • Client join and disconnect events are not automatically pushed to everyone - the user list is only accurate at the moment /users is sent

    • The [cmd],command,data string format is functional but rigid - Part 2 replaces it with a Payload class

  • Part 1 Checkpoint: https://github.com/MattToegel/IT114-2026/tree/Module04-Sockets-MCCS-Part1

Additional Resources

Wrap Up

  • git add M4 (track changes)

  • git commit -m "add Module04-Sockets-MCCS-Part1 sample code" (commit changes)

  • git push origin Module04-Sockets-MCCS-Part1 (push changes to GitHub)

  • Create a pull request to main and merge it

  • git checkout main (switch back to main branch)

  • git pull origin main (pull latest changes)