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
Payloadobjects sent overObjectOutputStream -
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 |
Sends typed |
Usernames |
Identified clients by JVM thread id only |
Adds |
Client IDs |
Used |
Server assigns sequential IDs via |
User list |
|
Server pushes |
Singleton access |
Passed |
|
|
One class handled both socket plumbing and command parsing |
|
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.INSTANCEandClient.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
enumto implement Singleton for bothServerandClient-
Server.INSTANCE- the one server object -
Client.INSTANCE- the one client object
-
-
In Part 1,
Serverwas a plain object created inmain()and passed into everyServerThreadconstructor -
With Singleton,
ServerThreadcan callServer.INSTANCEdirectly - no constructor argument needed -
Why
enuminstead of a private constructor pattern:-
The JVM guarantees an enum value is initialized exactly once
-
No
synchronizedblock 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
-
Client Package: Client
-
Clientis nowpublic enum Client { INSTANCE; }- same Singleton reasoning asServer -
Part 2 client emphasis:
-
Adds
myUserandknownUsersfor identity and live user cache -
Uses typed payload routing for incoming messages instead of raw string printing
-
Requires setting
/namebefore/connect
-
Client Properties
-
server: Socket- the connection to the Server -
out: ObjectOutputStreamandin: ObjectInputStream- communication channels -
isRunning: volatile boolean- shared acrosslistenToServer(),listenToInput(), and shutdown/close flow -
myUser: User- this client’s own identity;clientIdis filled in whenCLIENT_IDarrives from the server -
knownUsers: ConcurrentHashMap<Long, User>- live cache of connected users; updated bySERVER_JOIN,SERVER_LEAVE, andSERVER_SYNCpayloads
Client: Command Enum
-
Commandenum defines recognized client-side commands -
Values:
CONNECT,DISCONNECT,QUIT,USERS,REVERSE,SET_NAME-
SET_NAMEis new - maps to/name
-
-
Command.fromText(String)- static helper that matches each command trigger -
processClientCommand()switches on the result - replaces the chainedstartsWithchecks from Part 1
Client: connect() and listenToServer()
-
connect(String address, int port):-
Still opens
ObjectOutputStreambeforeObjectInputStreamand startslistenToServer()in the background -
Returns
trueifisConnected()after the attempt
-
-
listenToServer()- runs in the background:-
Change: received objects are now routed by
PayloadTypeviaprocessPayload()instead of printing raw strings directly -
Keeps the same overall resilience model (recoverable per-message cast failures, non-recoverable stream break exits)
-
finallystill closes connection resources
-
Client: listenToInput() and processClientCommand()
-
listenToInput()loops onscanner.nextLine(), passes input toprocessClientCommand(), and falls back tosendMessage(text)if no command matched -
processClientCommand()changes from Part 1:-
Addition:
SET_NAME(/name Alice) stores name inmyUser.clientName; it must be set before/connect -
Change:
CONNECTsendsCLIENT_CONNECTpayload after socket connect -
Change:
DISCONNECTandREVERSEsend typed payloads instead of[cmd]strings -
Deletion:
/usersno longer requests data from the server; it reads localknownUsers -
Unchanged:
QUITstill closes the client
-
Client: send*() Methods
send*() methods build payloads for specific intents, then delegate transport to sendToServer() (client → server)
|
-
sendConnectionData(String clientName)- sendsCLIENT_CONNECTas aConnectionPayloadwith the client’s name; triggers the server’s initialization flow -
sendDisconnect()- sends aDISCONNECTPayload -
sendReverse(String text)- sends aREVERSEPayloadwithmessageset to the text -
sendMessage(String text)- wraps text in aMESSAGEPayload -
sendToServer(Payload)- the underlying helper; callsout.writeObject()andout.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 onp.getPayloadType() -
processClientId(Payload)- handlesCLIENT_ID; casts toConnectionPayload, setsmyUser.clientIdandmyUser.clientName, adds self toknownUsers -
processClientStatus(Payload)- handlesSERVER_JOIN,SERVER_LEAVE,SERVER_SYNC:-
SERVER_JOIN- prints a join message then falls through toSERVER_SYNChandling (intentional fall-through) -
SERVER_SYNC- callsknownUsers.putIfAbsent(clientId, user)silently -
SERVER_LEAVE- removes the user fromknownUsersand prints a leave message
-
-
processMessage(Payload)andprocessReverse(Payload)- print the message to the console
Client: close() and closeServerConnection()
-
close()- setsisRunning = falseand callscloseServerConnection() -
closeServerConnection():-
Calls
myUser.reset()andknownUsers.clear()so identity state does not bleed into a future reconnect -
Closes
out,in, andserverin three separatetry/catchblocks -
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
-
Payloadis the base class for all data sent between client and server -
Replaces the raw
Stringobjects Part 1 sent overObjectOutputStream -
Implements
Serializable- required forObjectOutputStreamto 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 byMESSAGEandREVERSE)
-
-
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
-
PayloadTypeis the protocol map - every kind of message in the system has one value here -
The receiver switches on
PayloadTypeinstead of parsing a string prefix -
Values used in this part:
| Value | Direction | Purpose |
|---|---|---|
|
Client → Server |
Sends the client’s chosen name; triggers initialization |
|
Server → Client |
Tells the client what ID the server assigned and confirms the name |
|
Both |
Graceful disconnect request or acknowledgement |
|
Both |
Chat message to broadcast |
|
Both |
Text to reverse (request and response share the type) |
|
Server → All |
Update Client’s user list about connected client |
|
Server → All |
Update Client’s user list about disconnected client |
|
Server → New Joiner |
Silent bulk sync of existing users on initial connect |
-
File: PayloadType.java
Common Package: ConnectionPayload
-
ConnectionPayloadextendsPayloadwith one extra field:clientName: String -
Used for any message that needs to carry a name alongside the standard
Payloadfields -
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
-
Useris 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 toConstants.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
UserfromCommonso display formatting is always consistent -
File: User.java
Common Package: Constants and TextFX
Constants.java
-
Same purpose as Part 1: an
abstractclass holding shared constant values so magic numbers are not scattered across files -
Changes from Part 1:
-
COMMAND_TRIGGERis removed (the[cmd]prefix no longer exists) -
DEFAULT_CLIENT_ID = -1Lremains - used byUserto 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
Commonso bothServerandClientcan import it from one place -
Reminder: importing the inner
Colorclass can conflict withjava.awt.Coloronce a UI is added -
File: TextFX.java
Server Package: Server
-
Serveris nowpublic enum Server { INSTANCE; }- a Singleton accessed asServer.INSTANCE -
The central coordinator: manages all
ServerThreadinstances and routes application-level events -
No longer needs to be passed as a constructor argument - any class calls
Server.INSTANCEdirectly
Server Properties
-
port: int- the port to listen on (default3000) -
serverSocket: ServerSocket- stored as a field (was a local variable in Part 1) soshutdown()can close it and unblockaccept() -
connectedClients: ConcurrentHashMap<Long, ServerThread>- mapsclientIdto itsServerThread; 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 throughsynchronizedmethods -
nextClientId: long- starts at 1, incremented on each new connection; replacesthis.threadId()from Part 1 -
isRunning: boolean- controls theaccept()loop instart()
Server: start() and shutdown()
-
start(int port):-
Part 2 stores
ServerSocketon the instance soshutdown()can close it -
Passes
this::onServerThreadInitializedand defersconnectedClientsmap registration until thread initialization completes
-
-
shutdown()(called by the JVM shutdown hook onCtrl+C):-
Sets
isRunning = false -
Closes
serverSocket- this unblocks the blockingaccept()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
ServerThreadonce its name is set and streams are ready -
Runs on the thread of the
ServerThread, not the main Server thread - markedsynchronized -
Steps in order:
-
Assigns
clientIdusingnextClientId++and callsserverThread.sendClientId() -
Applies a simple overflow guard (
if (nextClientId < 0) nextClientId = 1) so IDs reset safely iflongwraps -
Adds the thread to
connectedClients -
Calls
unicastClientStatus(serverThread)to sendSERVER_SYNCfor each user currently inconnectedClients(including the new client) -
Calls
broadcastClientStatus(serverThread, isJoin=true, isSync=false)to sendSERVER_JOINto 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
connectedClientswith aforloop (notforEach()method) so it canbreakearly on failure -
Sends a
SERVER_SYNCpayload 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
targetto all connected clients -
Delegates to
sendOrDisconnect()with a lambda that callsserverThread.sendClientStatus(…)
-
Server: sendOrDisconnect() and processDisconnectedBuffer()
-
Part 1 used
connectedClients.values().removeIf(st → !st.sendToClient(message))inline inbroadcast()-
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
sendActionto every client; failures are added todisconnectedBufferand 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_LEAVEto all remaining clients so they updateknownUsers
-
-
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)- callsbroadcast()to relay the message to all clients -
handleReverseText(ServerThread sender, String text)- reverses the string and callsbroadcast() -
handleDisconnect(ServerThread sender)- delegates to privatedisconnect(serverThread):-
Checks that the client is still in
connectedClients(avoids double-leave announcements) -
Calls
serverThread.sendDisconnectTrigger()then removes from map and callsbroadcastClientStatus(…, isJoin=false)
-
-
All
handle*methods aresynchronizedonServer.INSTANCE- only one runs at a time -
File: Server.java
Server Package: BaseServerThread
-
BaseServerThreadis an abstract class that extendsThread -
It was extracted from
ServerThreadin Part 1, which mixed socket plumbing with command parsing in one class -
BaseServerThreadowns everything that every server-side thread needs regardless of the protocol:-
stream setup, the read loop, exception handling,
finallycleanup, 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
ServerThreadcan 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 inrun()and used bysendToClient() -
client: Socket- private field set by constructor and used for stream setup/cleanup -
user: User(private) - holds this client’sclientIdandclientName; 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 -
ServerThreaduses the socket constructor path viasuper(…)
BaseServerThread: run()
-
Core read-loop behavior remains the same as Part 1 (stream open order, blocking
readObject(), and cleanup infinally) -
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:
finallyconditionally callsServer.INSTANCE.handleDisconnect()only whenclientIdis assigned, then runscleanup()
-
BaseServerThread: sendToClient() and disconnect()
-
sendToClient(Payload payload)- writes the payload toout, flushes, returnstrueon success-
Returns
falseonIOException- the caller uses this to detect a lost connection
-
-
disconnect()- called from outside this thread (e.g., byServer) to stop it-
Sets
isRunning = false, then callsthis.interrupt()to unblock the blockingreadObject()call -
Does not call
cleanup()directly - thefinallyblock inrun()always handles that
-
-
cleanup()- closes theclientsocket and callsuser.reset()-
Streams are closed automatically by try-with-resources in
run()
-
-
File: BaseServerThread.java
Server Package: ServerThread
-
ServerThreadextendsBaseServerThread -
Its only responsibility is protocol routing - reading a
PayloadTypeand 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;ServerThreadis much shorter and easier to read
ServerThread Constructor
-
Takes the
Socketand aConsumer<ServerThread>callback - noServerargument -
Passes the socket to
BaseServerThreadviasuper(Objects.requireNonNull(myClient, …)) -
Validates and stores the callback with
Objects.requireNonNull(…)-
Part 1 passed
Server serveras a constructor argument and stored it as a field; that field is gone -
ServerThreadnow callsServer.INSTANCEdirectly 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 aprocess*()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 toConnectionPayload, readsclientName, callssetClientName()-
setClientName()is defined inBaseServerThreadand triggersonInitialized()once the name is set -
onInitialized()invokes the callback which runsServer.INSTANCE.onServerThreadInitialized(this)
-
-
processMessage(Payload)- callsServer.INSTANCE.handleMessage(this, message) -
processReverse(Payload)- callsServer.INSTANCE.handleReverseText(this, message) -
processDisconnect(Payload)- callsServer.INSTANCE.handleDisconnect(this)
ServerThread: send*() Methods
send*() methods send outbound server payloads (server → client)
|
-
sendClientId()- sends aCLIENT_IDConnectionPayloadback to this client with the server-assigned id and name -
sendClientStatus(clientId, clientName, isJoin, isSync)- sendsSERVER_JOIN,SERVER_LEAVE, orSERVER_SYNCdepending on the flag values-
isSync = truesendsSERVER_SYNC(silent background sync);isSync = falsesendsSERVER_JOINorSERVER_LEAVE
-
-
sendMessage(String)- wraps text in aMESSAGEpayload and sends it to this client -
sendDisconnectTrigger()- sends aDISCONNECTpayload then callsdisconnect()-
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:
javacuses the file system path with slashes;javauses 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
ServerandClientvia Javaenum -
Payloadobject protocol replacing the[cmd],name,datastring format -
Usermodel shared across server and client -
BaseServerThreadextracted fromServerThreadso responsibilities are clear -
Server-pushed join/leave/sync events keeping every client’s user list current automatically
-
sendOrDisconnect()pattern that announcesSERVER_LEAVEwhen 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,dataparsing withPayloadTypeenum routing -
On-demand user list - replaced the
/usersserver round-trip with a liveknownUserscache -
No structured join/leave - replaced plain string broadcasts with typed
SERVER_JOINandSERVER_LEAVEpayloads -
Mixed responsibilities - split the monolithic
ServerThreadintoBaseServerThread(plumbing) andServerThread(routing) -
Constructor argument wiring - replaced passing
Serverinto everyServerThreadwithServer.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
mainand merge it -
git checkout main(switch back to main branch) -
git pull origin main(pull latest changes)
| This is pretty much Milestone1 |