docs,chore: single-port 1999 websocket protobuf

Phase 05 — sync infrastructure and documentation with the typed-protobuf
refactor:
- docker-compose.yml: drop 1024/1025 mappings, single "1999:1999"
- server/Dockerfile: EXPOSE 1999, -p 1999 entrypoint
- README.md: rewrite transport description, architecture diagram, protocol
  section, server options, project structure, add proto:gen script note
- docs/project-overview.md: update transport + dependencies sections
- docs/system-architecture.md: rewrite diagrams + pipeline + file inventory
  for the WebSocket-only typed-dispatch path
- docs/codebase-summary.md: refresh file tree, java package inventory,
  gradle deps, vite deps, networking and game-flow sections
- docs/deployment-guide.md: single-port walkthrough for local / docker /
  systemd / nginx; remove all 1024/1025 firewall and troubleshooting
- docs/code-standards.md: replace dead ServerEventListener_CODE_* class-name
  example, fix sample ws:// URL to port 1999
This commit is contained in:
2026-04-11 08:33:46 +07:00
parent ecc617790e
commit cbad690565
8 changed files with 214 additions and 184 deletions
+37 -16
View File
@@ -11,7 +11,7 @@ Built on [Netty](https://netty.io/) (server) and [Phaser 3](https://phaser.io/)
- **Player vs AI (PVE)** — three difficulty levels (Easy, Medium, Hard)
- **Spectator mode** — watch ongoing games in real-time
- **Phaser web client** — professional 2D game UI with canvas board, stone animations, sound effects
- **WebSocket + TCP** — dual protocol support
- **Typed protobuf over WebSocket** — single binary wire protocol on port 1999
## Prerequisites
@@ -27,7 +27,7 @@ cd caro
docker compose up -d
```
Then open `http://localhost:8080/` in your browser. The server listens on ports `1024` (TCP) and `1025` (WebSocket); the client is served at `8080`.
Then open `http://localhost:8080/` in your browser. The server listens on port `1999` (WebSocket); the client is served at `8080`.
## Quick Start (Local)
@@ -35,14 +35,13 @@ Then open `http://localhost:8080/` in your browser. The server listens on ports
```bash
./server/gradlew -p server clean build
java -jar server/build/libs/caro-server-0.0.1.jar -p 1024
java -jar server/build/libs/caro-server-0.0.1.jar -p 1999
```
On Windows use `server\gradlew.bat` instead of `./server/gradlew`.
The server starts two listeners:
- **TCP** on port `1024` (Protobuf)
- **WebSocket** on port `1025` (JSON)
The server starts one listener:
- **WebSocket** on port `1999` (typed protobuf binary frames at `/ratel`)
### 2. Run the client (Vite dev server)
@@ -81,10 +80,23 @@ caro/
```
Client (browser)
|
+-- TCP :1024 --> ProtobufTransferHandler --> ServerEventListener_*
+-- WS :1025 --> WebsocketTransferHandler --> ServerEventListener_*
+-- WS :1999 /ratel --> WebsocketTransferHandler
|
v
RequestConverter (wire Request -> sealed ClientRequest record)
|
v
RequestDispatcher (pattern-match switch on record)
|
v
<Verb>Handler (typed business logic, emits typed Response)
```
Protobuf schemas live at `server/src/main/proto/{request,response}.proto`.
The `com.google.protobuf` Gradle plugin generates Java classes into
`server/build/generated/sources/proto/main/java/com/miti99/caro/protocol/`.
No reflection, no string-keyed lookups.
### Client Architecture
```
@@ -92,7 +104,10 @@ client/src/
main.js Phaser game boot
config/
game-config.js Phaser config (800x800, Scale.FIT)
protocol-constants.js Server/client event code enums
protocol-constants.js ClientEventCode (event bus keys) only
generated/
protocol.js pbjs static-module output (Request/Response)
protocol.d.ts pbts TypeScript typings for protocol.js
scenes/
boot-scene.js Connect to server
menu-scene.js DOM overlay menus
@@ -112,7 +127,7 @@ client/src/
## Server Options
```
-p, -port TCP port (default: 1024, WebSocket = TCP + 1)
-p, -port WebSocket port (default: 1999)
```
## Client Scripts
@@ -121,19 +136,25 @@ client/src/
npm --prefix client run dev # Start Vite dev server (port 5173)
npm --prefix client run build # Production build to client/dist/
npm --prefix client run preview # Preview production build
npm --prefix client run proto:gen # Regenerate client/src/generated/protocol.{js,d.ts}
```
## Protocol
Communication uses JSON messages over WebSocket or Protobuf over TCP.
Communication uses **typed protobuf binary frames** over WebSocket.
WebSocket message format:
```json
{"code": "CODE_GAME_MOVE", "data": "{\"row\":7,\"col\":7}", "info": ""}
Every client-to-server message is an instance of the `Request` oneof wrapper
(`server/src/main/proto/request.proto`); every server-to-client message is an
instance of the `Response` oneof wrapper (`server/src/main/proto/response.proto`).
The oneof case IS the event type — there are no string codes on the wire.
WebSocket endpoint: `ws://host:1999/ratel`
Example (send a move at row 7, col 7):
```js
connectionService.sendGameMove(7, 7); // builds Request { game_move: { row: 7, col: 7 } }
```
WebSocket endpoint: `ws://host:{tcp_port + 1}/ratel`
## Credits
This project is based on [Ratel](https://github.com/ainilili/ratel) by [ainilili](https://github.com/ainilili), originally a Chinese Landlords (Dou Di Zhu) card game. It has been converted to Gomoku (Five-in-a-Row) with a new web client.
+1 -2
View File
@@ -5,8 +5,7 @@ services:
dockerfile: server/Dockerfile
container_name: caro-server
ports:
- "1024:1024" # TCP / Protobuf
- "1025:1025" # WebSocket
- "1999:1999" # WebSocket (typed protobuf binary frames)
restart: unless-stopped
client:
+8 -8
View File
@@ -36,23 +36,23 @@ com.miti99.caro.{common|server}.{subcomponent}
**Patterns:**
- `FooBar` — Regular classes
- `FooBarListener` — Event listeners
- `FooBarHandler` — Protocol/network handlers
- `FooBarProxy` — Message sending proxies
- `FooBarTest` — Unit test classes
- `FooBarImpl` — Concrete implementations (rare)
- `FooRequest` / `FooRequestRecord` — Sealed record variants (typed requests)
- `FooTest` — Unit test classes
- `FooImpl` — Concrete implementations (rare)
- `AbstractFooBar` — Base classes
**Examples:**
```java
// Good
public class Board { ... }
public class ServerEventListener_CODE_GAME_MOVE { ... }
public class GameMoveHandler { ... }
public record GameMoveRequestRecord(...) implements ClientRequest { }
public class GomokuHelper { ... }
// Avoid
public class board { ... } // lowercase
public class GameMoveListener { ... } // unclear purpose
public class board { ... } // lowercase
public class game_move_handler { ... } // snake_case — use PascalCase
```
### File Organization
@@ -411,7 +411,7 @@ const board = Board(); // Should use new
/**
* Connects to the WebSocket server and establishes game communication.
*
* @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:1025/ratel')
* @param {string} url - The WebSocket server URL (e.g., 'ws://localhost:1999/ratel')
* @returns {Promise<WebSocket>} Promise resolving to the connected WebSocket
* @throws {Error} If connection fails or timeout occurs
*/
+50 -48
View File
@@ -11,25 +11,27 @@ caro/
│ ├── src/main/java/com/miti99/caro/
│ │ ├── common/
│ │ │ ├── channel/ Netty utilities (ChannelUtils)
│ │ │ ├── entity/ Data models (Board, Room, GameMove, Msg, ClientSide)
│ │ │ ├── enums/ ServerEventCode, ClientEventCode, PieceType, etc.
│ │ │ ├── entity/ Data models (Board, Room, GameMove, ClientSide)
│ │ │ ├── enums/ ClientEventCode, PieceType, GameResult, RoomType, etc.
│ │ │ ├── exception/ LandlordException
│ │ │ ├── features/ Feature flags
│ │ │ ├── handler/ Protocol codec (DefaultDecoder)
│ │ │ ├── helper/ GomokuHelper, MapHelper
│ │ │ ├── helper/ GomokuHelper (win detection)
│ │ │ ├── print/ SimplePrinter
│ │ │ ├── robot/ AI engine (GomokuAI)
│ │ │ ── transfer/ Binary serialization (ByteKit, ByteLink, TransferProtocolUtils)
│ │ │ └── utils/ JsonUtils (gson), ListUtils, OptionsUtils, StreamUtils
│ │ │ ── utils/ ListUtils, OptionsUtils, StreamUtils
│ │ └── server/
│ │ ├── event/ ServerEventListener_* handlers
│ │ ├── handler/ Netty pipeline (Protobuf/WS codecs)
│ │ ├── proxy/ ProtobufProxy, WebsocketProxy
│ │ ├── event/
│ │ │ ├── request/ ClientRequest sealed interface + record variants (14 types)
│ │ │ ├── handler/ 14 typed request handlers (*Handler.java)
│ │ │ ├── RequestConverter.java Protobuf→ClientRequest conversion
│ │ │ └── RequestDispatcher.java Pattern-match dispatch logic
│ │ ├── handler/ Netty pipeline (WebsocketTransferHandler)
│ │ ├── timer/ RoomClearTask
│ │ ├── SimpleServer.java Server entry point
│ │ └── ServerContains.java Global state container
│ ├── src/main/resources/
│ │ ── proto/ .proto files + generate.sh (future proto-over-WS)
│ ├── src/main/proto/
│ │ ── request.proto Client→server typed message definitions
│ │ └── response.proto Server→client typed message definitions
│ ├── src/test/java/com/miti99/caro/common/
│ │ ├── helper/tests/GomokuHelperTest.java 29 JUnit 5 tests
│ │ └── robot/tests/GomokuAITest.java 8 JUnit 5 tests
@@ -85,9 +87,8 @@ caro/
- `Board.java` — 15x15 game board, move validation, win/draw detection (`BOARD_SIZE=15`, `WIN_CONDITION=5`)
- `Room.java` — Game session state (id, type, status, players, board, moveHistory)
- `GameMove.java` — Single move (row, col, piece, playerId, timestamp)
- `Msg.java` — WebSocket JSON envelope (record: code, data, info)
- `ServerTransferData.java` / `ClientTransferData.java` — Protobuf-generated wire types
- `ClientSide.java` — Player connection state (nickname, status, role)
- Generated proto messages — `Request`, `Response` oneof wrappers (in `server/build/generated/sources/proto/main/java/`)
**Enums:**
- `ServerEventCode` — client→server action codes
@@ -101,37 +102,31 @@ caro/
- `GomokuAI.java` — AI move selection (Easy/Medium/Hard)
**Utilities:**
- `JsonUtils.java` — gson wrapper (`toJson`, `fromJson`)
- `ListUtils`, `OptionsUtils`, `StreamUtils`
- `MapHelper.java` — Fluent map builder (uses gson)
**Protocol:**
- `ByteKit`, `ByteLink` — Byte buffer operations
- `TransferProtocolUtils.java` — Protocol framing (`#...$` delimiters, gson JSON body)
- `DefaultDecoder.java` — Protobuf message decoder
**Dispatch (typed records):**
- `ClientRequest` — Sealed interface representing all possible client requests (14 variants)
- Record types: `SetNicknameRequest`, `CreateRoomRequest`, `CreatePveRoomRequest`, `GetRoomsRequest`, `JoinRoomRequest`, `GameStartingRequest`, `GameReadyRequest`, `GameMoveRequest`, `GameResetRequest`, `WatchGameRequest`, `WatchGameExitRequest`, `ClientExitRequest`, `ClientOfflineRequest`, `SetClientInfoRequest`
---
### `com.miti99.caro.server` (Server code)
**Entry Point:**
- `SimpleServer.java` — Bootstrap (`-p {port}`, default 1024)
- Starts TCP proxy on `port` and WebSocket proxy on `port+1`
- `SimpleServer.java` — Bootstrap (WebSocket server defaults to port 1999 at `/ratel`)
- `ServerContains.java` — Singleton global state (rooms, client sides, channel map)
**Event Handlers (ServerEventListener_*):**
- `CODE_CLIENT_NICKNAME_SET`, `CODE_ROOM_CREATE`, `CODE_ROOM_CREATE_PVE`, `CODE_GET_ROOMS`
- `CODE_ROOM_JOIN`, `CODE_GAME_STARTING`, `CODE_GAME_READY`, `CODE_GAME_MOVE`
- `CODE_GAME_WATCH` / `CODE_GAME_WATCH_EXIT`, `CODE_CLIENT_OFFLINE`
**Event Handlers (14 *Handler classes):**
- `SetNicknameHandler`, `CreateRoomHandler`, `CreatePveRoomHandler`, `GetRoomsHandler`, `JoinRoomHandler`
- `GameStartingHandler`, `GameReadyHandler`, `GameMoveHandler`, `GameResetHandler`
- `WatchGameHandler`, `WatchGameExitHandler`, `ClientExitHandler`, `ClientOfflineHandler`, `SetClientInfoHandler`
**Network Handlers:**
- `ProtobufTransferHandler` — TCP/Protobuf codec
- `WebsocketTransferHandler` — WebSocket JSON codec (uses `JsonUtils.fromJson(text, Msg.class)`)
- `SecondProtobufCodec` — Second-pass protobuf decoder
**Request Processing:**
- `RequestConverter` — Decodes `Request` oneof from binary wire format → typed `ClientRequest` variant
- `RequestDispatcher` — Pattern-matches `ClientRequest` variant → dispatches to corresponding handler
**Message Proxies:**
- `ProtobufProxy` — TCP server bootstrap
- `WebsocketProxy` — WebSocket server bootstrap (no static file handler; non-WS HTTP → default Netty 400/403)
**Network Handler:**
- `WebsocketTransferHandler` — Netty pipeline handler: decodes binary frame to `Request` protobuf message
**Background Tasks:**
- `RoomClearTask` — Periodic cleanup
@@ -181,20 +176,21 @@ caro/
**Plugins:**
- `java` — standard Java plugin
- `com.gradleup.shadow:8.3.5` — fat jar packaging
- `com.google.protobuf:0.9.6` — protobuf code generation
- `com.gradleup.shadow:8.3.8` — fat jar packaging
**Toolchain:** `JavaLanguageVersion.of(25)` — Gradle auto-provisions Java 25 if missing.
**Dependencies:**
- `io.netty:netty-all:4.1.115.Final` — async networking
- `com.google.protobuf:protobuf-java:3.25.5` — binary serialization
- `com.google.code.gson:gson:2.11.0` — JSON (supports records)
- `org.junit:junit-bom:5.11.3` (platform) + `org.junit.jupiter:junit-jupiter` — testing
- `io.netty:netty-all:4.1.128.Final` — async networking
- `com.google.protobuf:protobuf-java:3.25.5` — binary serialization, generated code
- `com.google.protobuf:protoc:3.25.5` — protobuf compiler (build time)
- `org.junit:junit-bom:5.11.4` (platform) + `org.junit.jupiter:junit-jupiter` — testing
**Shadow jar config:**
- Main class: `com.miti99.caro.server.SimpleServer`
- No special command-line args (server defaults to port 1999)
- `mergeServiceFiles()` — preserve Netty SPIs
- `append("META-INF/io.netty.versions.properties")` — merge Netty version file
- Excludes signing metadata (`*.SF`, `*.DSA`, `*.RSA`)
**Build Command:**
@@ -213,10 +209,13 @@ caro/
- `npm run dev` — Dev server (port 5173, hot reload)
- `npm run build` — Production build to `dist/`
- `npm run preview` — Preview production build
- `npm run proto:gen` — Regenerate protobuf code from server `.proto` files (uses `pbjs` + `pbts`)
**Dependencies:**
- `phaser ^3.87.0` — Game engine
- `protobufjs ^7.5.4` — JavaScript protobuf codec
- `vite ^6.3.1` — Bundler (dev-only)
- `@protobufjs/cli ^1.1.3` — Protobuf code generator (dev-only)
**Output:** `client/dist/` (index.html + bundled JS, ~1.5 MB / 346 KB gzipped)
@@ -243,10 +242,11 @@ caro/
| Scope | Dependencies |
|-------|--------------|
| **server runtime** | Netty 4.1.115, Protobuf 3.25.5, gson 2.11.0 |
| **server test** | JUnit Jupiter 5.11.3 |
| **client runtime** | Phaser 3.87 |
| **client build** | Vite 6.3 |
| **server runtime** | Netty 4.1.128, Protobuf 3.25.5 (generated code) |
| **server test** | JUnit Jupiter 5.11.4 |
| **server build** | Gradle 9.2.1, Protobuf Gradle plugin 0.9.6, Shadow 8.3.8 |
| **client runtime** | Phaser 3.87, protobufjs 7.5.4 |
| **client build** | Vite 6.3, protobufjs-cli 1.1.3 |
---
@@ -258,14 +258,16 @@ caro/
- `server/src/main/java/com/miti99/caro/common/robot/GomokuAI.java`
### Networking
- `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java`
- `server/src/main/java/com/miti99/caro/server/handler/ProtobufTransferHandler.java`
- `server/src/main/java/com/miti99/caro/common/channel/ChannelUtils.java`
- `client/src/services/connection-service.js`
- `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java` — Binary decoder
- `server/src/main/java/com/miti99/caro/server/event/RequestConverter.java` — Protobuf→record conversion
- `server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java` — Dispatch logic
- `server/src/main/java/com/miti99/caro/common/channel/ChannelUtils.java` — Response encoder/sender
- `client/src/services/connection-service.js` — WebSocket client (binary mode)
- `server/src/main/proto/*.proto` — Message definitions
### Game Flow
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_MOVE.java`
- `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_CODE_GAME_STARTING.java`
- `server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java`
- `server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java`
- `client/src/scenes/game-scene.js`
### UI
+38 -50
View File
@@ -27,8 +27,7 @@ docker compose up --build -d
Then:
- Client: `http://localhost:8080/`
- Server TCP: `localhost:1024` (Protobuf)
- Server WebSocket: `ws://localhost:1025/ratel`
- Server WebSocket: `ws://localhost:1999/ratel` (binary typed protobuf)
Stop:
```bash
@@ -36,7 +35,7 @@ docker compose down
```
What's running:
- **caro-server** — Java 25 server, listens on `1024` (TCP) + `1025` (WebSocket)
- **caro-server** — Java 25 server, listens on port `1999` (WebSocket only at `/ratel`)
- **caro-client** — Nginx serving the built Vite bundle on host port `8080`
Storage: in-memory only; games are not persisted. Restarting clears all rooms.
@@ -84,20 +83,19 @@ Tests: 29 GomokuHelperTest + 8 GomokuAITest (JUnit 5). All must pass.
### 3. Run the Server
```bash
java -jar server/build/libs/caro-server-0.0.1.jar -p 1024
java -jar server/build/libs/caro-server-0.0.1.jar
```
Output:
```
[INFO] Server listening on port 1024 (TCP)
[INFO] WebSocket server listening on port 1025
[INFO] Server listening on port 1999 (WebSocket)
[INFO] WebSocket endpoint: /ratel
```
What's running:
- **TCP port 1024** — Protobuf protocol
- **WebSocket port 1025** — JSON protocol at `/ratel`
- **WebSocket port 1999** — Binary typed protobuf at `/ratel`
Note: non-WebSocket HTTP requests to `1025` return a 400/403 (no static file serving).
Connect via: `ws://localhost:1999/ratel`
### 4. Run the Client (Vite dev server)
@@ -124,11 +122,13 @@ Open `http://localhost:5173/` — Phaser 3 client with full game UI.
### Option A: Standalone JAR
```bash
java -jar caro-server-0.0.1.jar -p 1024
java -jar caro-server-0.0.1.jar
```
Server defaults to port 1999 (WebSocket only).
For production:
- Run in background: `nohup java -jar ... &`
- Run in background: `nohup java -jar caro-server-0.0.1.jar &`
- Or use systemd (see below)
- Or container (Docker)
@@ -138,10 +138,6 @@ System requirements:
- 100 MB disk
- Java 25 runtime
Port configuration:
- Server listens on `-p {port}` (TCP)
- WebSocket automatically uses `{port}+1`
### Option B: Docker Container
The included `server/Dockerfile` is multi-stage:
@@ -156,8 +152,7 @@ docker build -f server/Dockerfile -t caro-server:0.0.1 .
Run:
```bash
docker run -d --name caro-server \
-p 1024:1024/tcp \
-p 1025:1025/tcp \
-p 1999:1999/tcp \
caro-server:0.0.1
```
@@ -174,7 +169,7 @@ After=network.target
Type=simple
User=gameserver
WorkingDirectory=/opt/caro
ExecStart=/usr/bin/java -jar /opt/caro/caro-server-0.0.1.jar -p 1024
ExecStart=/usr/bin/java -jar /opt/caro/caro-server-0.0.1.jar
Restart=on-failure
RestartSec=10
StandardOutput=journal
@@ -244,7 +239,7 @@ server {
# WebSocket proxy to server
location /ratel {
proxy_pass http://localhost:1025;
proxy_pass http://localhost:1999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
@@ -267,25 +262,17 @@ sudo systemctl restart nginx
### Server Options
```bash
java -jar caro-server-0.0.1.jar [OPTIONS]
```
Server uses defaults. No command-line arguments to configure port. Default port is 1999.
Available options:
```
-p, -port TCP port (default: 1024)
WebSocket uses port + 1
```
Examples:
```bash
java -jar caro-server-0.0.1.jar -p 1024 # TCP:1024, WS:1025
java -jar caro-server-0.0.1.jar -p 8080 # TCP:8080, WS:8081
To change port, modify `SimpleServer.java` and rebuild:
```java
// Default: 1999
private static final int DEFAULT_PORT = 1999;
```
### Client Configuration
Connection endpoint is in `client/src/services/connection-service.js`. By default the client connects to WS on `window.location.hostname:1025/ratel`. To override, edit that file and rebuild.
Connection endpoint is in `client/src/services/connection-service.js`. By default the client connects to WS on `window.location.hostname:1999/ratel`. To override, edit that file and rebuild.
---
@@ -329,13 +316,13 @@ No automated tests currently. Manual testing:
### Server Health Check
```bash
# Check TCP port
netstat -an | grep 1024
# Check WebSocket port
netstat -an | grep 1999
# or
lsof -i :1024
lsof -i :1999
# Test WebSocket
websocat ws://localhost:1025/ratel
websocat ws://localhost:1999/ratel
```
### Logs
@@ -359,7 +346,7 @@ netstat -an | grep -c ESTABLISHED
Restart:
```bash
kill $(pgrep -f caro-server)
java -jar server/build/libs/caro-server-0.0.1.jar -p 1024
java -jar server/build/libs/caro-server-0.0.1.jar
```
---
@@ -369,24 +356,25 @@ java -jar server/build/libs/caro-server-0.0.1.jar -p 1024
### Port Already in Use
```bash
lsof -i :1024
lsof -i :1999
kill -9 <PID>
# Or use a different port
java -jar caro-server-0.0.1.jar -p 9090
```
To use a different port, modify `SimpleServer.java` and rebuild.
### Connection Refused
1. Verify server is running: `ps aux | grep caro-server`
2. Check firewall: `sudo ufw allow 1024/tcp && sudo ufw allow 1025/tcp`
3. Test connectivity: `nc -zv localhost 1024`
2. Check firewall: `sudo ufw allow 1999/tcp`
3. Test connectivity: `nc -zv localhost 1999`
4. Docker: `docker compose ps` and `docker compose port server`
### WebSocket Connection Fails
1. WebSocket listens on `TCP port + 1` (default 1025)
2. Ensure the client URL is correct: `ws://host:1025/ratel`
3. Test directly: `websocat ws://localhost:1025/ratel`
1. WebSocket listens on port 1999 at path `/ratel`
2. Ensure the client URL is correct: `ws://host:1999/ratel`
3. Test directly: `websocat ws://localhost:1999/ratel`
4. Verify server logs for request decoding errors
### High Memory Usage
@@ -395,7 +383,7 @@ java -jar caro-server-0.0.1.jar -p 9090
netstat -an | grep ESTABLISHED | wc -l
# Increase JVM heap
java -Xmx2g -jar caro-server-0.0.1.jar -p 1024
java -Xmx2g -jar caro-server-0.0.1.jar
# Restart to reclaim memory
```
@@ -409,7 +397,7 @@ Before going live:
- [ ] Java 25 installed and verified
- [ ] `./server/gradlew -p server clean build` passes (37 tests)
- [ ] Server boots without errors
- [ ] TCP port 1024 and WebSocket port 1025 open (firewall)
- [ ] WebSocket port 1999 open (firewall)
- [ ] Client built: `npm --prefix client ci && npm --prefix client run build`
- [ ] Client connects to correct server endpoint
- [ ] Manual test: create game, make moves, join room, spectate
@@ -425,7 +413,7 @@ Before going live:
1. Back up current jar
2. Pull + rebuild: `git pull && ./server/gradlew -p server clean build`
3. Stop: `kill $(pgrep -f caro-server)`
4. Start new jar: `java -jar server/build/libs/caro-server-0.0.1.jar -p 1024`
4. Start new jar: `java -jar server/build/libs/caro-server-0.0.1.jar`
### Update Client
@@ -447,7 +435,7 @@ Before going live:
For production servers with 4+ GB RAM:
```bash
java -Xmx4g -XX:+UseG1GC -jar caro-server-0.0.1.jar -p 1024
java -Xmx4g -XX:+UseG1GC -jar caro-server-0.0.1.jar
```
Java 25 defaults are already sensible; tune only if needed.
+19 -16
View File
@@ -36,10 +36,11 @@ Caro (also known as Gomoku or Five-in-a-Row) is a classic strategy board game. T
### Game UI
- **Client (Phaser 3):** 800x800 board with wood texture, stone animations, sound effects, move history panel
### Cross-Protocol Support
- **TCP/Protobuf:** lower latency, binary protocol
- **WebSocket/JSON:** easier browser integration
- Both run simultaneously on different ports (`1024` TCP, `1025` WebSocket by default)
### WebSocket Protocol
- **Binary WebSocket:** BINARY frames carrying TYPED PROTOBUF messages at port 1999
- `ws://localhost:1999/ratel` endpoint
- Typed message dispatch via `ClientRequest` records and sealed interface pattern
- Single-port architecture (no legacy transports)
---
@@ -71,8 +72,8 @@ Caro (also known as Gomoku or Five-in-a-Row) is a classic strategy board game. T
| Component | Technology | Details |
|-----------|-----------|---------|
| **Server** | Java 25 + Netty 4.1 | Asynchronous, event-driven, low-latency |
| **Network Protocol** | Protobuf (TCP) + JSON/gson (WebSocket) | Dual protocol, language-agnostic |
| **Server** | Java 25 + Netty 4.1.128 | Asynchronous, event-driven, low-latency |
| **Network Protocol** | Typed Protobuf over WebSocket (binary frames) | Single protocol, typed record dispatch |
| **Game Logic** | Pure Java 25 (records, switch expressions) | Board state, move validation, win detection, AI |
| **Client** | Phaser 3 + Vite + Vanilla JS | No framework dependencies (besides Phaser) |
| **Build** | Gradle 9.x (Kotlin DSL) + Shadow plugin / Vite | Standalone server jar + static client bundle |
@@ -139,16 +140,18 @@ Caro (also known as Gomoku or Five-in-a-Row) is a classic strategy board game. T
```
┌─────────────────┐ ┌─────────────────────┐
│ Web Browser │◄───────►│ Phaser 3 Client │
│ (http://...) │ WS/JSON │ (Vite + JS, :8080) │
└─────────────────┘ └─────────────────────┘
│ (http://...) │ Typed │ (Vite + JS, :8080) │
└─────────────────┘ Protobuf└─────────────────────┘
│ WebSocket :1025/ratel
WS :1999/ratel
BINARY frames
┌─────────────────────────────────────────────────────┐
│ Java 25 Netty Server (com.miti99.caro.server) │
│ ├─ WebsocketTransferHandler (WS → game events)
│ ├─ ProtobufTransferHandler (TCP → game events)
ServerEventListener_* (process moves, AI)
│ ├─ WebsocketTransferHandler (WS decoder)
│ ├─ RequestConverter (wire → ClientRequest)
RequestDispatcher (pattern-match dispatch)
│ └─ *Handler classes (14 typed request handlers) │
└─────────────────────────────────────────────────────┘
```
@@ -167,7 +170,7 @@ docker compose up --build -d
### Quick Start (Local)
```bash
./server/gradlew -p server build -x test
java -jar server/build/libs/caro-server-0.0.1.jar -p 1024
java -jar server/build/libs/caro-server-0.0.1.jar
# In another terminal:
npm --prefix client install
npm --prefix client run dev
@@ -191,9 +194,9 @@ See `deployment-guide.md` for detailed setup instructions.
| Dependency | Version | Purpose |
|-----------|---------|---------|
| Java | 25 (LTS) | Language runtime |
| Netty | 4.1.115.Final | Async networking |
| Protobuf | 3.25.5 | Binary serialization (TCP wire) |
| gson | 2.11.0 | JSON serialization (WS wire) |
| Netty | 4.1.128.Final | Async networking |
| Protobuf | 3.25.5 | Binary serialization (WS wire) |
| protobufjs | 7.5.4 | JavaScript protobuf codec (client) |
| JUnit Jupiter | 5.11.3 | Test framework |
| Gradle | 9.2.1 (wrapper) | Java build tool |
| Shadow plugin | 8.3.5 | Fat jar packaging |
+58 -41
View File
@@ -2,14 +2,14 @@
## High-Level Overview
Caro is a **client-server multiplayer game** with dual-protocol networking:
Caro is a **client-server multiplayer game** with typed-protobuf WebSocket:
```
┌──────────────┐ WebSocket ┌──────────────────────────────┐
│ Client │◄────/JSON───►│ │
│ (Phaser 3) │ │ Java 25 Netty Server │
└──────────────┘ │ Port 1024: TCP/Protobuf
│ Port 1025: WebSocket/JSON
│ Client │◄──BINARY────►│ │
│ (Phaser 3) │ Typed Proto │ Java 25 Netty Server │
└──────────────┘ │ Port 1999: WebSocket only
│ Path: /ratel
│ │
│ Game Logic: │
│ - Room Management │
@@ -19,8 +19,6 @@ Caro is a **client-server multiplayer game** with dual-protocol networking:
└──────────────────────────────┘
```
Non-WebSocket HTTP requests to port `1025` are rejected by Netty with a default 400/403 (there is no static file handler).
---
## Component Architecture
@@ -30,21 +28,19 @@ Non-WebSocket HTTP requests to port `1025` are rejected by Netty with a default
**File:** `server/src/main/java/com/miti99/caro/server/`
**Responsibilities:**
- Listen on TCP (1024) and WebSocket (1025) simultaneously
- Parse incoming messages (Protobuf or JSON via gson)
- Listen on WebSocket port 1999 at path `/ratel`
- Parse incoming binary TYPED PROTOBUF messages (`ClientRequest` oneof wrapper)
- Execute game logic (move validation, win checks)
- Broadcast state updates to all connected clients
- Broadcast state updates via typed `Response` messages (binary frames)
- Run AI for PVE games
- Manage room lifecycle (create, join, spectate, cleanup)
**Key Classes:**
- `SimpleServer` — Entry point; starts Netty bootstrap for both ports
- `ServerEventListener` — Base for event handlers (in `event/`)
- `ServerEventListener_CODE_*` — Individual handlers for each ServerEventCode
- `ProtobufTransferHandler` — Netty pipeline handler for TCP
- `WebsocketTransferHandler` — Netty pipeline handler for WebSocket (deserializes `Msg` record via gson)
- `SecondProtobufCodec` — Second-pass protobuf codec
- `ProtobufProxy` / `WebsocketProxy` — Bootstrap the TCP and WebSocket server sockets
- `SimpleServer` — Entry point; starts Netty bootstrap for WebSocket only
- `WebsocketTransferHandler` — Netty pipeline handler (decodes binary frame to `Request` protobuf)
- `RequestConverter` — Converts protobuf `Request` oneof to `ClientRequest` sealed records
- `RequestDispatcher` — Pattern-matching switch dispatching `ClientRequest` to typed handlers
- `*Handler` — 14 individual request handlers (SetNicknameHandler, CreateRoomHandler, GameMoveHandler, etc.)
**Event Codes (ServerEventCode)** — sent by clients:
```
@@ -109,7 +105,7 @@ client/src/
- **Event Bus:** Decouples scenes, services, UI components. `emit(event, data)` → listeners respond.
- **Game State Service:** Single source of truth for board, room, players.
- **Connection Service:** Reconnect logic and heartbeat (30-second interval).
- **WebSocket Message Format:** `{ code: "CODE_GAME_MOVE", data: "{...}", info: "" }`
- **Client Connection:** Initiates WebSocket handshake to `ws://localhost:1999/ratel`, then sends typed `Request` messages in binary
---
@@ -118,17 +114,16 @@ client/src/
**File:** `server/src/main/java/com/miti99/caro/common/`
**Responsibilities:**
- Shared entities (Board, Room, GameMove, Msg, ClientSide)
- Shared enums (ServerEventCode, ClientEventCode, PieceType, GameResult, RoomType, RoomStatus)
- Shared entities (Board, Room, GameMove, ClientSide)
- Shared enums (ClientEventCode, PieceType, GameResult, RoomType, RoomStatus)
- Game logic (move validation, win detection, AI)
- Utilities (gson JSON, list, options, stream helpers)
- Utilities (list, options, stream helpers)
**Key Classes:**
- `Board` — 15x15 grid, move validation, win/draw detection
- `Room` — Encapsulates game state, players, spectators
- `GameMove` — Represents a single move (row, col, piece type, playerId, timestamp)
- `Msg`**record** for WebSocket JSON envelope (`code`, `data`, `info`)
- `ServerTransferData` / `ClientTransferData` — Protobuf-generated wire types
- `ClientSide` — Player connection metadata
- `GomokuHelper` — Win detection (4 directions)
- `GomokuAI` — AI move selection (Easy, Medium, Hard)
- Enums under `common/enums/`
@@ -141,19 +136,33 @@ Note: `common` is a sub-package within the single `server/` Gradle project. It i
### Message Format
**WebSocket (JSON, via gson):**
```json
{
"code": "CODE_GAME_MOVE",
"data": "{\"row\": 7, \"col\": 7}",
"info": ""
**WebSocket (Binary Protobuf):**
Client sends `Request` oneof message (defined in `server/src/main/proto/request.proto`):
```
message Request {
oneof payload {
SetNicknameRequest set_nickname = 1;
CreateRoomRequest create_room = 2;
CreatePveRoomRequest create_pve_room = 3;
... 11 more typed requests ...
}
}
```
gson 2.11 serializes the `Msg` record by reading its canonical components (`code`, `data`, `info`). Null components are skipped by default, preserving wire compatibility with the previous setter-based class.
Server replies with `Response` oneof message (defined in `server/src/main/proto/response.proto`):
```
message Response {
oneof payload {
ClientConnectResponse client_connect = 1;
RoomCreateResponse room_create = 2;
GameMoveResponse game_move = 3;
... additional typed responses ...
}
}
```
**TCP (Protobuf):**
Binary format (serialized via Protobuf 3.25.5).
Each message is **binary-encoded** in a WebSocket frame (`binaryType='arraybuffer'`).
### Connection Flow
@@ -268,10 +277,14 @@ HttpObjectAggregator (8 KB)
WebSocketServerProtocolHandler ("/ratel")
WebsocketTransferHandler (decode Msg record via gson, dispatch event listener)
WebsocketTransferHandler (decode binary Request frame, dispatch to RequestDispatcher)
RequestConverter (Request oneof → ClientRequest sealed variant)
RequestDispatcher (pattern-match dispatch to typed *Handler)
```
The pipeline has no static file handler. Non-WS HTTP requests to port 1025 return Netty's default HTTP error.
Outgoing: Handler classes call `ChannelUtils.push(Response)` which encodes to binary and sends `BinaryWebSocketFrame`.
---
@@ -400,7 +413,7 @@ client/ (no dependencies except Phaser 3, Vite dev-only)
│ └─ caro-client (Nginx + dist/) │
└────────────────────────────────────┘
Server listens on `:1024` (TCP) and `:1025` (WebSocket only).
Server listens on `:1999` (WebSocket only, at `/ratel`).
```
Docker Compose runs both services (`caro-server` + `caro-client`) from the single repo context.
@@ -412,23 +425,27 @@ Docker Compose runs both services (`caro-server` + `caro-client`) from the singl
| File | Purpose |
|------|---------|
| `server/src/main/java/com/miti99/caro/server/SimpleServer.java` | Server entry point |
| `server/src/main/proto/request.proto` | Client→server typed message definitions |
| `server/src/main/proto/response.proto` | Server→client typed message definitions |
| `client/src/main.js` | Client entry point (Phaser) |
| `server/src/main/java/com/miti99/caro/common/entity/Board.java` | Game board state + validation |
| `server/src/main/java/com/miti99/caro/common/helper/GomokuHelper.java` | Win detection algorithm |
| `server/src/main/java/com/miti99/caro/common/robot/GomokuAI.java` | AI move selection (3 difficulties) |
| `server/src/main/java/com/miti99/caro/common/entity/Room.java` | Game room state container |
| `server/src/main/java/com/miti99/caro/common/entity/Msg.java` | WebSocket JSON envelope (record) |
| `server/src/main/java/com/miti99/caro/server/event/ServerEventListener_*.java` | Event handlers (game logic) |
| `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java` | WS codec |
| `server/src/main/java/com/miti99/caro/server/event/RequestConverter.java` | Protobuf→ClientRequest record |
| `server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java` | Dispatch ClientRequest→Handler |
| `server/src/main/java/com/miti99/caro/server/event/handler/*.java` | 14 typed request handlers |
| `server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java` | WS binary codec |
| `client/src/scenes/game-scene.js` | Client main gameplay scene |
| `client/src/services/connection-service.js` | WebSocket client |
| `client/src/config/protocol-constants.js` | Event code enums |
| `client/src/services/connection-service.js` | WebSocket client (binary mode) |
| `client/src/config/protocol-constants.js` | ClientEventCode enum (event-bus keys) |
| `client/src/generated/protocol.{js,d.ts}` | Protobuf codegen (protobufjs) |
---
## Future Architectural Improvements
1. **Proto-over-WebSocket** — migrate WS payloads from JSON to Protobuf (the `.proto` files are already staged under `server/src/main/resources/proto/`).
1. **Enhanced request validation** — add server-side schema validation for stricter type safety.
2. **Database integration** — Persist games, leaderboards, accounts.
3. **Virtual threads** — Java 25 has mature virtual-thread support; some blocking code paths (e.g. AI hard-depth search) could be offloaded.
4. **Message broker (Kafka/RabbitMQ)** — Decouple game logic from network I/O.
+3 -3
View File
@@ -21,7 +21,7 @@ WORKDIR /app
COPY --from=build /build/server/build/libs/caro-server-0.0.1.jar app.jar
# TCP (protobuf) and WebSocket ports
EXPOSE 1024 1025
# Single WebSocket port carrying typed protobuf binary frames
EXPOSE 1999
ENTRYPOINT ["java", "-jar", "app.jar", "-p", "1024"]
ENTRYPOINT ["java", "-jar", "app.jar", "-p", "1999"]