Network Chat is a small Java 21 TCP chat application with a deliberately simple runtime shape:
clients <-> ChatConnection <-> ChatProtocol <-> ChatServer
ChatServeropens aServerSocketon the configured port.- Each accepted socket is handled by a bounded client executor.
- The server sends
NAME_REQUEST, waits forUSER_NAME, validates token credentials when accounts are configured, validates uniqueness, then responds withNAME_ACCEPTED. - All registered users, including the current client, are sent to the new client as
USER_ADDEDevents. - The client joins
generalby default; additional rooms are created when a client sendsROOM_JOIN. ROOM_TEXTmessages are broadcast only to members of the target room, including the sender.PRIVATE_TEXTmessages are delivered only to the sender and recipient.- If history is configured, persistable text frames are stored in a bounded JSONL file and recent
room history is replayed after
ROOM_JOINED. - Admin users can send
/healthto receive a private server status frame. - Closing or failed connections are removed and announced with
USER_REMOVED.
Frames are one-line UTF-8 JSON objects serialized by ChatProtocol.
typeis required for every frame.protocolVersionmust match the currentChatMessage.PROTOCOL_VERSION; unversioned frames are rejected with an explicitERROR.datais optional for control frames, but required and non-blank forTEXT,ROOM_TEXT, andPRIVATE_TEXT.sendercarries the author for text frames; clients own display formatting.roomcarries room routing forROOM_TEXT,ROOM_JOIN,ROOM_LEAVE,ROOM_ADDED,ROOM_JOINED, andROOM_LEFT.recipientcarries the target user forPRIVATE_TEXT.timestampandmessageIdare generated when a frame is created and preserved when the server echoes a normalized room or private message.datais limited byChatMessage.MAX_DATA_LENGTH.- Account tokens are carried in the existing
USER_NAMEframe asusername|base64(token)so older plain username clients remain compatible when accounts are disabled.
ChatServerConfig centralizes runtime limits:
port- TCP port, default1500.maxClients- maximum concurrent client handler threads, default100.handshakeTimeout- maximum time to complete username registration, default10s.readTimeout- idle socket read timeout after handshake, default5m.historyFile/historyLimit/historyReplayLimit- optional replayable JSONL history.accountFile- optional account registry.tls- optional JSSE TLS server socket configuration.
The legacy new ChatServer(int port) constructor delegates to ChatServerConfig.ofPort(port).
TLS is transport-level and optional. The server loads a Java keystore from --tls-keystore and
--tls-password; clients opt in through NETWORK_CHAT_TLS plus an optional truststore. Plain TCP
remains the default for local development and integration tests.
Accounts are optional and loaded from a CSV file:
username,role,salt,sha256(salt:token)
AccountTool / ./gradlew createAccount --args="alice USER secret" prints one ready-to-append row.
When accounts are enabled, clients must provide the token through the GUI token field or
NETWORK_CHAT_TOKEN. Roles are deliberately coarse: USER can chat, ADMIN can additionally run
server-only admin commands such as /health.
Server logs use compact JSON-line messages for important lifecycle/auth events while keeping Java
java.util.logging as the runtime logging backend.
ChatClient owns the socket lifecycle and exposes hooks for console, bot, and Swing clients.
Connection status is updated through a final template method before client-specific UI or console
side effects run, so subclasses cannot skip the shared latch/status update.
The Swing client keeps connection settings in a small local preferences store, renders an embedded connection panel instead of modal startup prompts, and switches to a read-only disconnected state with a reconnect action when the socket loop fails after a successful session.
The Swing model stores a bounded local timeline for the current app session. Text frames are
deduplicated by messageId, own messages are rendered as Вы, and user add/remove protocol events
are appended as service events instead of ordinary chat text.
Room membership lives on the server. general always exists, room creation is idempotent through
ROOM_JOIN, and clients that try to send to a room before joining receive an explicit ERROR.
History is optional and enabled through ChatServerConfig.withHistory(...) or server CLI
--history. The store is a UTF-8 JSONL file where each line is a versioned ChatMessage.
ROOM_TEXTandPRIVATE_TEXTare persistable.- The server keeps a bounded in-memory view and rewrites the file after saves to enforce rotation.
- Invalid/corrupt lines are skipped at startup so one bad record does not prevent the server from starting.
- Legacy unversioned
TEXTrecords are migrated to currentROOM_TEXT/generalrecords at load time. - Known rooms can be recovered from stored room messages.
- On room join the server replays the last
historyReplayLimitroom messages to that client.
./gradlew releaseBundle builds:
network-chat-<version>-windows.zipwithbin/*.batlaunchers and runtime dependencies.checksums.txtwith SHA-256 digests.provenance.jsonwith local SLSA-style subject metadata for the zip.