Goal

  • If you have not completed Part 1 yet, review Java Sockets: Multi-Client Chat Server Part 1 first - this lesson builds directly on that foundation

  • Refactor Part 1 into a cleaner, more structured baseline

  • Replace raw-string commands with typed Payload objects sent over ObjectOutputStream

  • Add usernames so clients are identified by name, not just a JVM thread id

  • Broadcast join, leave, and sync events to all clients automatically so every client’s user list stays current

  • Prep

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

    • git pull origin main (pull any latest changes)

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

What Changed from Part 1

Area Part 1 Part 2

Command format

Sent raw strings like [cmd],users and split on commas to detect commands

Sends typed Payload objects and switches on PayloadType - no string parsing

Usernames

Identified clients by JVM thread id only

Adds /name before connect, and the name is sent in CLIENT_CONNECT

Client IDs

Used this.threadId() inside ServerThread

Server assigns sequential IDs via nextClientId

User list

/users round-tripped to the server each time

Server pushes SERVER_JOIN, SERVER_LEAVE, and SERVER_SYNC; clients keep a live local cache

Singleton access

Passed Server into ServerThread constructor

Server and Client are enums so classes can use Server.INSTANCE and Client.INSTANCE directly

ServerThread responsibility

One class handled both socket plumbing and command parsing

BaseServerThread handles plumbing while ServerThread focuses on routing

Package Structure

  • Part 1 had all files flat in one folder (M4/MCCS/Part1/)

  • Part 2 separates responsibilities into packages

    • (Client) M5.MCCS.Part2.Client - client-side command parsing, send/receive handling, and local cache management

      • Client

    • (Common) M5.MCCS.Part2.Common - shared models, protocol types, and utilities used by both programs

      • Payload, ConnectionPayload, PayloadType, User, Constants, TextFX

    • (Server) M5.MCCS.Part2.Server - server-side lifecycle, routing, and broadcast logic

      • Server, BaseServerThread, ServerThread

In this lesson, implementation sections use the format: Package: Class

Constants.COMMAND_TRIGGER (the [cmd] prefix string from Part 1) is gone. It only existed to distinguish commands from chat messages, and PayloadType replaces it entirely.

Brief about Design Patterns

  • Design patterns are reusable approaches to common architecture problems

  • This lesson applies one pattern directly (Singleton) and follows others conceptually through structure and flow

Common Patterns Context

  • Singleton - one globally accessible instance

    • Used directly here via Server.INSTANCE and Client.INSTANCE

  • Factory - centralize object creation so callers do not depend on constructor details

    • Related here in the sense that payload creation is centralized in send*() methods

  • Observer - one-to-many event propagation

    • Related here in server broadcast behavior (SERVER_JOIN, SERVER_LEAVE, SERVER_SYNC, MESSAGE)

  • Decorator - extend behavior without changing a base class directly

    • Not implemented directly in this lesson, but useful later for layered features like logging/metrics wrappers

Design Pattern: Singleton

  • A Singleton is a class (or enum) that has exactly one instance for the lifetime of the program

  • Part 2 uses Java’s enum to implement Singleton for both Server and Client

    • Server.INSTANCE - the one server object

    • Client.INSTANCE - the one client object

  • In Part 1, Server was a plain object created in main() and passed into every ServerThread constructor

  • With Singleton, ServerThread can call Server.INSTANCE directly - no constructor argument needed

  • Why enum instead of a private constructor pattern:

    • The JVM guarantees an enum value is initialized exactly once

    • No synchronized block needed to prevent double-initialization

    • Cannot be accidentally instantiated a second time

  • Resource

Architecture Overview

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

  • Each connected Client still gets its own ServerThread - the 1:1 relationship is unchanged

  • New additions:

    • BaseServerThread - abstract base for all server-side thread lifecycle code

    • Revised package structure to separate server, client, and shared code

Diagram

Client Package: Client

  • Client is now public enum Client { INSTANCE; } - same Singleton reasoning as Server

  • Part 2 client emphasis:

    • Adds myUser and knownUsers for identity and live user cache

    • Uses typed payload routing for incoming messages instead of raw string printing

    • Requires setting /name before /connect

Client Properties

  • server: Socket - the connection to the Server

  • out: ObjectOutputStream and in: ObjectInputStream - communication channels

  • isRunning: volatile boolean - shared across listenToServer(), listenToInput(), and shutdown/close flow

  • myUser: User - this client’s own identity; clientId is filled in when CLIENT_ID arrives from the server

  • knownUsers: ConcurrentHashMap<Long, User> - live cache of connected users; updated by SERVER_JOIN, SERVER_LEAVE, and SERVER_SYNC payloads

Client: Command Enum

  • Command enum defines recognized client-side commands

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

    • SET_NAME is new - maps to /name

  • Command.fromText(String) - static helper that matches each command trigger

  • processClientCommand() switches on the result - replaces the chained startsWith checks from Part 1

Client: connect() and listenToServer()

  • connect(String address, int port):

    • Still opens ObjectOutputStream before ObjectInputStream and starts listenToServer() in the background

    • Returns true if isConnected() after the attempt

  • listenToServer() - runs in the background:

    • Change: received objects are now routed by PayloadType via processPayload() instead of printing raw strings directly

    • Keeps the same overall resilience model (recoverable per-message cast failures, non-recoverable stream break exits)

    • finally still closes connection resources

Client: listenToInput() and processClientCommand()

  • listenToInput() loops on scanner.nextLine(), passes input to processClientCommand(), and falls back to sendMessage(text) if no command matched

  • processClientCommand() changes from Part 1:

    • Addition: SET_NAME (/name Alice) stores name in myUser.clientName; it must be set before /connect

    • Change: CONNECT sends CLIENT_CONNECT payload after socket connect

    • Change: DISCONNECT and REVERSE send typed payloads instead of [cmd] strings

    • Deletion: /users no longer requests data from the server; it reads local knownUsers

    • Unchanged: QUIT still closes the client

Client: send*() Methods

send*() methods build payloads for specific intents, then delegate transport to sendToServer() (client → server)
  • sendConnectionData(String clientName) - sends CLIENT_CONNECT as a ConnectionPayload with the client’s name; triggers the server’s initialization flow

  • sendDisconnect() - sends a DISCONNECT Payload

  • sendReverse(String text) - sends a REVERSE Payload with message set to the text

  • sendMessage(String text) - wraps text in a MESSAGE Payload

  • sendToServer(Payload) - the underlying helper; calls out.writeObject() and out.flush()

Client: process*() Methods

process*() methods handle received payloads from server to client; they are invoked by listenToServer()
  • processPayload(Payload p) - the top-level router; switches on p.getPayloadType()

  • processClientId(Payload) - handles CLIENT_ID; casts to ConnectionPayload, sets myUser.clientId and myUser.clientName, adds self to knownUsers

  • processClientStatus(Payload) - handles SERVER_JOIN, SERVER_LEAVE, SERVER_SYNC:

    • SERVER_JOIN - prints a join message then falls through to SERVER_SYNC handling (intentional fall-through)

    • SERVER_SYNC - calls knownUsers.putIfAbsent(clientId, user) silently

    • SERVER_LEAVE - removes the user from knownUsers and prints a leave message

  • processMessage(Payload) and processReverse(Payload) - print the message to the console

Client: close() and closeServerConnection()

  • close() - sets isRunning = false and calls closeServerConnection()

  • closeServerConnection():

    • Calls myUser.reset() and knownUsers.clear() so identity state does not bleed into a future reconnect

    • 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: Client.java

Common Package: Payload

  • Payload is the base class for all data sent between client and server

  • Replaces the raw String objects Part 1 sent over ObjectOutputStream

  • Implements Serializable - required for ObjectOutputStream to transmit a Java object over a socket

  • Fields:

    • payloadType: PayloadType - the routing key; the receiver switches on this to decide what to do

    • clientId: long - who sent this payload

    • message: String - optional text content (used by MESSAGE and REVERSE)

  • Overrides toString() for debugging/logging readable payload content in terminal output

  • Adding a new message type in Part 1 required new string-parsing code; here it means adding one enum value and one case in a switch

  • File: Payload.java

Common Package: PayloadType

  • PayloadType is the protocol map - every kind of message in the system has one value here

  • The receiver switches on PayloadType instead of parsing a string prefix

  • Values used in this part:

Value Direction Purpose

CLIENT_CONNECT

Client → Server

Sends the client’s chosen name; triggers initialization

CLIENT_ID

Server → Client

Tells the client what ID the server assigned and confirms the name

DISCONNECT

Both

Graceful disconnect request or acknowledgement

MESSAGE

Both

Chat message to broadcast

REVERSE

Both

Text to reverse (request and response share the type)

SERVER_JOIN

Server → All

Update Client’s user list about connected client

SERVER_LEAVE

Server → All

Update Client’s user list about disconnected client

SERVER_SYNC

Server → New Joiner

Silent bulk sync of existing users on initial connect

Common Package: ConnectionPayload

  • ConnectionPayload extends Payload with one extra field: clientName: String

  • Used for any message that needs to carry a name alongside the standard Payload fields

  • Applies to: CLIENT_CONNECT, CLIENT_ID, SERVER_JOIN, SERVER_LEAVE, SERVER_SYNC

  • Design rule: if every payload needs a field, add it to Payload; if only some need it, subclass

    • Reminder, we won’t be updating the Payload class and will always be sub-classing

  • File: ConnectionPayload.java

Common Package: User

  • User is a shared model class that did not exist in Part 1

  • Part 1 identified clients only by a numeric thread id

  • Part 2 pairs an id with a human-chosen name

  • Fields:

    • clientId: long - server-assigned id; defaults to Constants.DEFAULT_CLIENT_ID (-1) when unset

    • clientName: String - name chosen by the user with /name

  • Methods:

    • getDisplayName() - returns a formatted name string; one place to change display format for everyone

    • reset() - clears both fields back to defaults; used on disconnect to avoid state leaking into a reconnect

  • Both server and client import User from Common so display formatting is always consistent

  • File: User.java

Common Package: Constants and TextFX

Constants.java

  • Same purpose as Part 1: an abstract class holding shared constant values so magic numbers are not scattered across files

  • Changes from Part 1:

    • COMMAND_TRIGGER is removed (the [cmd] prefix no longer exists)

    • DEFAULT_CLIENT_ID = -1L remains - used by User to mean "not yet assigned by server"

  • File: Constants.java

TextFX.java

  • Unchanged from Part 1 - a utility for printing colored text to the terminal

  • Moved to Common so both Server and Client can import it from one place

  • Reminder: importing the inner Color class can conflict with java.awt.Color once a UI is added

  • File: TextFX.java

Server Package: Server

  • Server is now public enum Server { INSTANCE; } - a Singleton accessed as Server.INSTANCE

  • The central coordinator: manages all ServerThread instances and routes application-level events

  • No longer needs to be passed as a constructor argument - any class calls Server.INSTANCE directly

Server Properties

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

  • serverSocket: ServerSocket - stored as a field (was a local variable in Part 1) so shutdown() can close it and unblock accept()

  • connectedClients: ConcurrentHashMap<Long, ServerThread> - maps clientId to its ServerThread; thread-safe because many threads call server methods concurrently

  • disconnectedBuffer: ArrayList<ServerThread> - collects failed sends during a broadcast so cleanup happens after the iteration, not during it; safe because all access flows through synchronized methods

  • nextClientId: long - starts at 1, incremented on each new connection; replaces this.threadId() from Part 1

  • isRunning: boolean - controls the accept() loop in start()

Server: start() and shutdown()

  • start(int port):

    • Part 2 stores ServerSocket on the instance so shutdown() can close it

    • Passes this::onServerThreadInitialized and defers connectedClients map registration until thread initialization completes

  • shutdown() (called by the JVM shutdown hook on Ctrl+C):

    • Sets isRunning = false

    • Closes serverSocket - this unblocks the blocking accept() call so the loop exits cleanly

    • Calls disconnect() on every connected client

Closing serverSocket first prevents new clients from joining while the shutdown iterates over connectedClients. In Part 1 the ServerSocket was a local variable and could not be closed from outside start().

Server: onServerThreadInitialized()

  • The callback called by ServerThread once its name is set and streams are ready

  • Runs on the thread of the ServerThread, not the main Server thread - marked synchronized

  • Steps in order:

    1. Assigns clientId using nextClientId++ and calls serverThread.sendClientId()

    2. Applies a simple overflow guard (if (nextClientId < 0) nextClientId = 1) so IDs reset safely if long wraps

    3. Adds the thread to connectedClients

    4. Calls unicastClientStatus(serverThread) to send SERVER_SYNC for each user currently in connectedClients (including the new client)

    5. Calls broadcastClientStatus(serverThread, isJoin=true, isSync=false) to send SERVER_JOIN to everyone

The new client receives their own SERVER_JOIN and a SERVER_SYNC about themselves. This is intentional - the client uses putIfAbsent so a duplicate entry never overwrites an existing one.

Server: unicastClientStatus() and broadcastClientStatus()

  • unicastClientStatus(incomingServerThread):

    • Iterates connectedClients with a for loop (not forEach() method) so it can break early on failure

    • Sends a SERVER_SYNC payload about each existing user to the newly connected client

    • If any send fails, calls disconnect(incomingServerThread) and breaks - no point continuing if they can’t receive data

  • broadcastClientStatus(target, isJoin, isSync):

    • Sends a status update about target to all connected clients

    • Delegates to sendOrDisconnect() with a lambda that calls serverThread.sendClientStatus(…​)

Server: sendOrDisconnect() and processDisconnectedBuffer()

  • Part 1 used connectedClients.values().removeIf(st → !st.sendToClient(message)) inline in broadcast()

    • This pruned failed clients but sent no announcement to remaining clients that a user had left

  • Part 2 separates the concern:

    • sendOrDisconnect(Function<ServerThread, Boolean> sendAction):

      • Applies sendAction to every client; failures are added to disconnectedBuffer and the entry is removed from the map

      • Calls processDisconnectedBuffer() when done

    • processDisconnectedBuffer():

      • Drains the buffer to a local snapshot, clears it, then calls broadcastClientStatus(…​, isJoin=false) for each

      • This sends SERVER_LEAVE to all remaining clients so they update knownUsers

Server: handle*() Methods

handle*() methods are server-side entry points invoked by ServerThread when client-originated payloads need coordinated server actions
  • handleMessage(ServerThread sender, String text) - calls broadcast() to relay the message to all clients

  • handleReverseText(ServerThread sender, String text) - reverses the string and calls broadcast()

  • handleDisconnect(ServerThread sender) - delegates to private disconnect(serverThread):

    • Checks that the client is still in connectedClients (avoids double-leave announcements)

    • Calls serverThread.sendDisconnectTrigger() then removes from map and calls broadcastClientStatus(…​, isJoin=false)

  • All handle* methods are synchronized on Server.INSTANCE - only one runs at a time

  • File: Server.java

Server Package: BaseServerThread

  • BaseServerThread is an abstract class that extends Thread

  • It was extracted from ServerThread in Part 1, which mixed socket plumbing with command parsing in one class

  • BaseServerThread owns everything that every server-side thread needs regardless of the protocol:

    • stream setup, the read loop, exception handling, finally cleanup, user identity fields

  • Concrete subclasses implement three abstract methods:

    • info(String) - how to log messages for this specific thread type

    • onInitialized() - called when the client name is set and the handshake is complete

    • processPayload(Payload) - called for each successfully read payload object

  • This separation means ServerThread can focus entirely on routing logic

BaseServerThread Properties

  • isRunning: volatile boolean - same role as Part 1 (read-loop control across threads)

  • out: ObjectOutputStream - private field written in run() and used by sendToClient()

  • client: Socket - private field set by constructor and used for stream setup/cleanup

  • user: User (private) - holds this client’s clientId and clientName; exposed via getters and setters so subclasses never touch it directly

BaseServerThread Constructor

  • BaseServerThread() - no-arg constructor

  • BaseServerThread(Socket client) - stores the socket for this thread instance

  • ServerThread uses the socket constructor path via super(…​)

BaseServerThread: run()

  • Core read-loop behavior remains the same as Part 1 (stream open order, blocking readObject(), and cleanup in finally)

  • Part 2 updates in run():

    • Addition: a 3-second name handshake timer disconnects unnamed clients

    • Change: per-message bad payload casts are handled while the loop continues

    • Change: finally conditionally calls Server.INSTANCE.handleDisconnect() only when clientId is assigned, then runs cleanup()

BaseServerThread: sendToClient() and disconnect()

  • sendToClient(Payload payload) - writes the payload to out, flushes, returns true on success

    • Returns false on IOException - the caller uses this to detect a lost connection

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

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

    • Does not call cleanup() directly - the finally block in run() always handles that

  • cleanup() - closes the client socket and calls user.reset()

    • Streams are closed automatically by try-with-resources in run()

  • File: BaseServerThread.java

Server Package: ServerThread

  • ServerThread extends BaseServerThread

  • Its only responsibility is protocol routing - reading a PayloadType and calling the right handler

  • In Part 1, this class also owned the socket streams, the read loop, and string command parsing

  • Now all of that is in BaseServerThread; ServerThread is much shorter and easier to read

ServerThread Constructor

  • Takes the Socket and a Consumer<ServerThread> callback - no Server argument

  • Passes the socket to BaseServerThread via super(Objects.requireNonNull(myClient, …​))

  • Validates and stores the callback with Objects.requireNonNull(…​)

    • Part 1 passed Server server as a constructor argument and stored it as a field; that field is gone

    • ServerThread now calls Server.INSTANCE directly wherever it needs the server

ObjectOutputStream is opened before ObjectInputStream in BaseServerThread.run(). The same order must be used on the Client side. Reversing it on either end causes a deadlock.

ServerThread: processPayload()

  • Overrides the abstract method from BaseServerThread

  • Switches on incoming.getPayloadType() and delegates to a process*() method

  • Replaces processCommand(String) from Part 1 which split strings on commas

  • Unrecognized types are logged but do not crash the thread

ServerThread: process*() Methods

process*() methods handle inbound client payloads (client → server)
  • processClientConnect(Payload) - casts to ConnectionPayload, reads clientName, calls setClientName()

    • setClientName() is defined in BaseServerThread and triggers onInitialized() once the name is set

    • onInitialized() invokes the callback which runs Server.INSTANCE.onServerThreadInitialized(this)

  • processMessage(Payload) - calls Server.INSTANCE.handleMessage(this, message)

  • processReverse(Payload) - calls Server.INSTANCE.handleReverseText(this, message)

  • processDisconnect(Payload) - calls Server.INSTANCE.handleDisconnect(this)

ServerThread: send*() Methods

send*() methods send outbound server payloads (server → client)
  • sendClientId() - sends a CLIENT_ID ConnectionPayload back to this client with the server-assigned id and name

  • sendClientStatus(clientId, clientName, isJoin, isSync) - sends SERVER_JOIN, SERVER_LEAVE, or SERVER_SYNC depending on the flag values

    • isSync = true sends SERVER_SYNC (silent background sync); isSync = false sends SERVER_JOIN or SERVER_LEAVE

  • sendMessage(String) - wraps text in a MESSAGE payload and sends it to this client

  • sendDisconnectTrigger() - sends a DISCONNECT payload then calls disconnect()

    • Tells the client the server initiated the disconnect so the client can behave differently than it would for a dropped connection

  • File: ServerThread.java

Running the Sample

  • Compile:

    • javac M5/MCCS/Part2/Server/Server.java

    • javac M5/MCCS/Part2/Client/Client.java

  • Run server (one terminal):

    • java M5.MCCS.Part2.Server.Server

  • Run client (open a new terminal for each client):

    • java M5.MCCS.Part2.Client.Client

  • Per client test flow:

    • /name <name> (must be done first - connect will be blocked otherwise)

    • /connect localhost:3000

    • type a chat message and press Enter

    • /users (reads local cache; no server round-trip)

    • /reverse hello (server reverses and broadcasts)

    • /disconnect

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

  • Part 2 is a clean, layered baseline built on the same socket and threading fundamentals as Part 1

  • Key additions:

    • Singleton Server and Client via Java enum

    • Payload object protocol replacing the [cmd],name,data string format

    • User model shared across server and client

    • BaseServerThread extracted from ServerThread so responsibilities are clear

    • Server-pushed join/leave/sync events keeping every client’s user list current automatically

    • sendOrDisconnect() pattern that announces SERVER_LEAVE when a send fails, instead of silently pruning

Part 1 Limitations Addressed

  • No usernames - replaced thread ids with /name + server-assigned sequential ids

  • Fragile string commands - replaced [cmd],name,data parsing with PayloadType enum routing

  • On-demand user list - replaced the /users server round-trip with a live knownUsers cache

  • No structured join/leave - replaced plain string broadcasts with typed SERVER_JOIN and SERVER_LEAVE payloads

  • Mixed responsibilities - split the monolithic ServerThread into BaseServerThread (plumbing) and ServerThread (routing)

  • Constructor argument wiring - replaced passing Server into every ServerThread with Server.INSTANCE

Wrap Up

  • git add M5 (track changes)

  • git commit -m "add Module05-Sockets-MCCS-Part2 sample code" (commit changes)

  • git push origin Module05-Sockets-MCCS-Part2 (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)

This is pretty much Milestone1