Matt Toegel matthew.toegel@njit.edu
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)
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

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:
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
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 |
A utility class for printing colored text to the terminal
Used throughout for readability when debugging (e.g., received messages in cyan, commands in yellow)
Be careful when importing Color from this class - it can conflict with java.awt.Color once UI is introduced
TextFX.java: https://github.com/MattToegel/IT114-2026/blob/Module04-Sockets-MCCS-Part1/M4/MCCS/Part1/TextFX.java
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:
The Server accepts a new connection and creates a ServerThread for it
The ServerThread opens its streams and enters a loop blocking on readObject()
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
If the client disconnects or an error occurs, it cleans up and exits
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
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. |
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
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. |
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
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
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
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
boolean named isRunning - controls the main accept loop
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
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
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
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
handleMessage(ServerThread sender, String text) - calls broadcast() to send the message to all clients
handleDisconnect(ServerThread sender) - calls the private disconnect() method
Private disconnect() calls serverThread.disconnect(), removes the entry from connectedClients, and broadcasts a disconnect notice to remaining clients
Server.java: https://github.com/MattToegel/IT114-2026/blob/Module04-Sockets-MCCS-Part1/M4/MCCS/Part1/Server.java
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
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
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
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()
runAsync() submits the method to a shared thread pool and returns immediately, so connect() doesn’t block
Returns true if the connection succeeded
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
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
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
Calls out.writeObject(message) and out.flush()
Prints a helpful hint if not yet connected
close() - sets isRunning = false and calls closeServerConnection()
closeServerConnection() - closes out, in, and server in three separate try-catch blocks
Separate blocks ensure that even if one resource fails to close, the remaining ones still attempt to close
Client.java: https://github.com/MattToegel/IT114-2026/blob/Module04-Sockets-MCCS-Part1/M4/MCCS/Part1/Client.java
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
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
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)