Add (v1) NetworkManager, Session and Transport #173

Merged
lars.winzer merged 26 commits from feat/network-manager into main 2026-03-13 15:49:56 +01:00
22 changed files with 429 additions and 3 deletions
Showing only changes of commit b1ab8f3982 - Show all commits
+18 -2
View File
@@ -104,5 +104,21 @@ $RECYCLE.BIN/
## Jenv
.java-version
## VSCode
.vscode
## diagrams.net / draw.io
# Draw.io temporary files
*.drawio.bkp
*.drawio.tmp
# Auto-save files
*.drawio.autosave
# Exported images (optional nur wenn du Exporte nicht versionieren willst)
*.drawio.png
*.drawio.svg
*.drawio.pdf
## IntelliJ
.idea
## bin
bin/
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"vscjava.vscode-java-pack",
"mhutchie.git-graph",
"gitlab.gitlab-workflow",
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}
+17
View File
@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Export drawio to SVG",
"type": "shell",
"command": "./scripts/export-drawio.sh",
"problemMatcher": []
},
{
"label": "Export puml to SVG",
"type": "shell",
"command": "./scripts/export-plantuml.sh",
"problemMatcher": []
}
]
}
+2
View File
@@ -35,6 +35,8 @@ This protocol handles game state synchronization, player actions, lobby and glob
│ └── test/ # Unit tests
├── documents/ # Project resources and documentation
│ ├── images/ # Images for README and documentation
│ ├── diary # Diary files for each organizational meetup
│ ├── blog # Blog files covering project-related topics
│ ├── docs # Documentation
│ ├── milestones/ # Milestone deliverables (6 milestones)
│ └── (Other resources) # Additional project materials
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

+8 -1
View File
@@ -23,10 +23,17 @@ repositories {
javafx {
version = "25.0.2"
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base']
modules = ['javafx.controls', 'javafx.fxml', 'javafx.base', 'javafx.graphics', 'javafx.web']
}
dependencies {
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.0")
}
+28
View File
@@ -0,0 +1,28 @@
# Blog
## Purpose
This folder contains a collection of blog posts documenting the development process of our application. These blogs serve two key purposes:
1. **For External Audiences**
The blogs provide insights into the internal program flow and the interaction between components without requiring readers to dive into the source code. They offer a mixed-level overview of how different parts of the system work together.
2. **For Our Team**
The blogs serve as a record of our decision-making process. By documenting what decisions were made and why, we can track the evolution of our design choices and understand the reasoning behind them. This is invaluable for maintaining consistency across the team.
But keep in mind that these blogs **do not replace** the detailed source code documentation.
## Format
All blog posts follow the naming convention: `<YYYY-MM-DD>_<Title>.md`
## Blog Posts
### General
*(No posts yet)*
### Client
*(No posts yet)*
### Server
*(No posts yet)*
@@ -0,0 +1,86 @@
# Server-Side Networking — How It All Fits Together
This doc is meant to give a solid mixed-level understanding of how our server-side networking works. No deep dives — just a clear picture of what's happening under the hood when a client connects and sends a command.
> **Note:** This is v1. Annotations and Reflection-based dispatching are on the roadmap but not covered here yet.
> **Disclamer:** This document has been written by Claude. I modified certain parts and verified its contents for correctness.
## Our Protocol at a Glance
Our protocol is inspired by **POP3** - a classic, text-based protocol that communicates over a raw TCP connection. The idea is simple: the client sends a command as a plain-text string, and the server responds with either a success or an error.
A typical exchange looks something like this:
```
Client → GET_DELTA SINCE=42
Server → +OK
PLAYER_FOLDED playerId=3
POT 240
NEXT_TURN playerId=1
.
```
Responses start with `+OK` on success or `-ERR` when something goes wrong. Commands are short, uppercase strings - sometimes followed by arguments.
We don't use HTTP, there's no JSON body, no headers. Just a raw socket, a text stream, and a clearly defined set of commands.
## Core Components & Their Roles
![PlantUML diagramm of all components outlined in this document](/documents/images/docs/networking/server-architecure/networking_components.svg)
Here's a quick rundown of the main building blocks:
### `NetworkManager`
This is the entry point. It binds to a specific port and listens for incoming TCP connections.
Once a client connects, it creates a `Session` adds it to the `SessionManager` and goes back to waiting. Its not the responsibility of the `NetworkManager` to recieve and send data from and to each connected client.
### `Session`
Every client that connects has its own `Session` instance running on its own thread. This component owns the lifecycle of that connection: it reads incoming data, passes it along for processing, and writes responses back.
### `SessionId`
The `SessionId` is used to uniquely identify each session. Essentially its a wrapper arround the UUID type.
### `SessionManager`
The `SessionManager` stores all currently active sessions. It allows for the retrieval of a specific session by its id.
### `ProtocolParser`
Raw text (encapsulated in a `RawRequest`) coming off the socket isn't immediately useful — the `ProtocolParser` turns it with the help of the `Tokenizer` into a `PrimitiveRequest`.
### `RawRequest`
The `RawRequest` object is created after a message has been recieved.
It holds both a reference to the `Session` that recieved the message and the message itself.
### `PrimitiveRequest`
The `PrimitiveRequest` object is created by the `ProtocolParser` after the content of the `RawRequest` has been tokenized by the `Tokenizer`.
The command is stored in its own attribute and the arguments are accessible as a dictionary.
### `Tokenizer`
The job of the `tokenizer` is it, to take the raw string and turn it into tokens.
For example, the command `CHAT_LOBBY GAME=12 MESSAGE='All-in?'` will be decoded as `"CHAT_LOBBY", "GAME", "=", "12", "MESSAGE", "=", "All-in?"`.
It is unaware about the meaning.
### `CommandParser` (per command)
Takes in the `PrimitiveRequest` and checks if all required fields are provided with the correct value.
Creates a command specific `Request` object containing command arguments in a structured manner.
### `Request` (per command)
Has fields common along each implementation such as a reference to the `Session` that recieved the request.
Furthermore each implementation has fields unique to each command. For example, the `ChatLobbyRequest` has fields for the game and the message.
### `CommandRouter`
By identifying the type of class of the `Request`, the `CommandRouter` routes the request to the matching `CommandHandler`.
### `CommandHandler` (per command)
Each supported command has its own handler — a small, focused class that contains the logic for that specific command. They have access to the domain, containing inner parts of the game itself.
### `Response` (per response)
The interface has two sub-interfaces for either failed `ErrorResponse` or successfull `SuccessResponse` execution of commands.
Each step has exactly one responsibility. Increasing the ability to test and extend different components later on.
## Concurrency - Handling Multiple Clients
Every incoming connection spawns a new **Thread**. This means multiple clients can be served simultaneously without blocking each other.
As each `Session` runs entirely on its own thread, there's no shared mutable state between sessions, with exception of the `SessionManager` and other key components explained later.
@@ -0,0 +1,96 @@
@startuml
skinparam classAttributeIconSize 0
skinparam packageStyle rectangle
skinparam linetype ortho
skinparam backgroundColor transparent
' Network Layer
class NetworkManager {
+ start(port: int)
}
class Session {
- id: SessionId
+ Session(id: SessionId)
+ getId(): SessionId
+ run()
+ send(response: String)
}
class SessionId {
- value: UUID
+ SessionId()
+ SessionId(value: UUID)
}
class SessionManager {
- sessions: Map<UUID, Session>
+ add(session: Session)
+ getById(id: UUID): Session
+ remove(id: UUID)
}
NetworkManager --> Session : creates
NetworkManager --> SessionManager : adds session to
Session --> SessionId : identified by
Session --> RawRequest : creates
Session ..> Response : sends
SessionManager o-- Session : manages
' Protocol Layer
class RawRequest {
- session: Session
- rawMessage: String
}
class Tokenizer {
+ tokenize(input: String): List<String>
}
class ProtocolParser {
+ parse(raw: RawRequest): PrimitiveRequest
}
class PrimitiveRequest {
- command: String
- arguments: Map<String, String>
}
RawRequest --> ProtocolParser : passed to
ProtocolParser --> Tokenizer : uses
ProtocolParser --> PrimitiveRequest : produces
' Command Layer
interface CommandParser <<per command>> {
+ parse(primitive: PrimitiveRequest): Request
}
interface Request <<per command>> {
- session: Session
}
class CommandRouter {
+ route(request: Request)
}
interface CommandHandler <<per command>> {
+ handle(request: Request)
}
PrimitiveRequest --> CommandParser : passed to
CommandParser --> Request : produces
CommandRouter --> CommandHandler : dispatches to
CommandRouter ..> Request : receives
' Response Layer
interface Response {
+ encode(): String
}
interface SuccessResponse
interface ErrorResponse
Response <|-- SuccessResponse
Response <|-- ErrorResponse
@enduml
File diff suppressed because one or more lines are too long
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# Check for draw.io installation
DRAWIO_PATH=""
check_drawio_installed() {
if command -v drawio &>/dev/null; then
DRAWIO_PATH=$(command -v drawio)
return 0 # drawio via PATH
elif [[ -x "/Applications/draw.io.app/Contents/MacOS/draw.io" ]]; then
DRAWIO_PATH="/Applications/draw.io.app/Contents/MacOS/draw.io"
return 0 # For macOS check at default path
else
return 1 # not found
fi
}
if ! check_drawio_installed; then
echo "Error: 'drawio' was not found. Make sure, that the draw.io cli is installed."
echo "https://www.diagrams.net/"
exit 1
else
echo "Draw.io installation found at: $DRAWIO_PATH"
fi
# Compare source (draw.io document) and exported file svg on mtime
needs_export() {
local drawio_file="$1"
local svg_file="$2"
# If no file found, export
if [[ ! -f "$svg_file" ]]; then
return 0
fi
if [[ "$drawio_file" -nt "$svg_file" ]]; then
return 0
else
return 1
fi
}
# If (re-)export is necessary export with drawio cli
export_file() {
local drawio_file="$1"
local svg_file="${drawio_file%.drawio}.svg"
if needs_export "$drawio_file" "$svg_file"; then
echo "↻ Exporting: $drawio_file"
echo "$svg_file"
echo "$drawio_file"
"$DRAWIO_PATH" --export --format svg --transparent --output "$svg_file" "$drawio_file"
else
echo "✓ Up-to-date: $drawio_file"
fi
}
# Iterate over all .drawio files
main() {
# Source - https://stackoverflow.com/a/9612232
# Posted by Kevin, modified by community. See post 'Timeline' for change history
# Retrieved 2026-03-09, License - CC BY-SA 4.0
find . -name '*.drawio' -print0 |
while IFS= read -r -d '' line; do
export_file "$line"
done
echo "✓ Exported all files"
}
# Start script at entry point
main
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Check for plantuml installation
PLANTUML_PATH=""
check_plantuml_installed() {
if command -v plantuml &>/dev/null; then
PLANTUML_PATH=$(command -v plantuml)
return 0 # plantuml via PATH
else
return 1 # not found
fi
}
if ! check_plantuml_installed; then
echo "Error: 'plantuml' was not found. Make sure, that plantuml is installed."
echo "https://plantuml.com/starting"
exit 1
else
echo "PlantUML installation found at: $PLANTUML_PATH"
fi
# Compare source (.puml document) and exported file svg on mtime
needs_export() {
local puml_file="$1"
local svg_file="$2"
# If no file found, export
if [[ ! -f "$svg_file" ]]; then
return 0
fi
if [[ "$puml_file" -nt "$svg_file" ]]; then
return 0
else
return 1
fi
}
# If (re-)export is necessary export with plantuml cli
export_file() {
local puml_file="$1"
local svg_file="${puml_file%.puml}.svg"
if needs_export "$puml_file" "$svg_file"; then
echo "↻ Exporting: $puml_file"
"$PLANTUML_PATH" -tsvg "$puml_file"
else
echo "✓ Up-to-date: $puml_file"
fi
}
# Iterate over all .puml files
main() {
# Source - https://stackoverflow.com/a/9612232
# Posted by Kevin, modified by community. See post 'Timeline' for change history
# Retrieved 2026-03-09, License - CC BY-SA 4.0
find . -name '*.puml' -print0 |
while IFS= read -r -d '' line; do
export_file "$line"
done
echo "✓ Exported all files"
}
# Start script at entry point
main
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout disableAnsi="false" pattern=
"%style{%d{HH:mm:ss.SSS}}{bright,black} %highlight{%-8level}{
FATAL=bright red,
ERROR=red,
WARN=yellow,
INFO=blue,
DEBUG=cyan,
TRACE=white
} %style{%-20logger{20}}{magenta} %msg%n"
/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>