Merge branch 'main' into 'feat/lobby-ui'
This commit is contained in:
+4
-1
@@ -118,4 +118,7 @@ $RECYCLE.BIN/
|
||||
*.drawio.pdf
|
||||
|
||||
## IntelliJ
|
||||
.idea
|
||||
.idea
|
||||
|
||||
## bin
|
||||
bin/
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"java.configuration.updateBuildConfiguration": "automatic"
|
||||
}
|
||||
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 |
+14
-1
@@ -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")
|
||||
implementation 'org.json:json:20240303'
|
||||
@@ -42,6 +49,12 @@ tasks.named('jar', Jar) {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('cruntest', JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = 'ch.unibas.dmi.dbis.cs108.casono.Main'
|
||||
jvmArgs '--enable-native-access=ALL-UNNAMED'
|
||||
args 'client', '0.0.0.0:1234'
|
||||
}
|
||||
|
||||
tasks.register('fatJar', Jar) {
|
||||
group = 'build'
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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>
|
||||
Reference in New Issue
Block a user