From cbad6905657c9163d2137ca95d8aeea5fca8d16d Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 11 Apr 2026 08:33:46 +0700 Subject: [PATCH] docs,chore: single-port 1999 websocket protobuf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 53 ++++++++++++++------ docker-compose.yml | 3 +- docs/code-standards.md | 16 +++--- docs/codebase-summary.md | 98 ++++++++++++++++++------------------ docs/deployment-guide.md | 88 ++++++++++++++------------------- docs/project-overview.md | 35 +++++++------ docs/system-architecture.md | 99 ++++++++++++++++++++++--------------- server/Dockerfile | 6 +-- 8 files changed, 214 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index 6a2dcd6..70c8693 100644 --- a/README.md +++ b/README.md @@ -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 + 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. diff --git a/docker-compose.yml b/docker-compose.yml index ff32146..8aafffc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/docs/code-standards.md b/docs/code-standards.md index db63b54..a51b679 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -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} Promise resolving to the connected WebSocket * @throws {Error} If connection fails or timeout occurs */ diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index 4c6dbed..130e152 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -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 diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 3e290dc..e2cd5de 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -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 -# 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. diff --git a/docs/project-overview.md b/docs/project-overview.md index ac4b21e..339d451 100644 --- a/docs/project-overview.md +++ b/docs/project-overview.md @@ -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 | diff --git a/docs/system-architecture.md b/docs/system-architecture.md index f13409a..cf4a746 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -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. diff --git a/server/Dockerfile b/server/Dockerfile index 3fef2a6..804daea 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -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"]