From 0c01b62d3ce9165c6181a7b31334ca2f59b55866 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Thu, 15 Jan 2026 23:36:25 +0800 Subject: [PATCH 1/2] feat: add docs --- .github/workflows/build_deploy.yml | 55 +++ .github/workflows/links.yml | 50 ++ .gitignore | 11 + CONTRIBUTING.md | 95 ++++ README.adoc | 107 +++-- docs/.lychee.toml | 20 + docs/.lycheeignore | 11 + docs/Gemfile | 13 + docs/_config.yml | 40 ++ docs/assets/logo.svg | 1 + docs/core/buffer-management/index.adoc | 426 +++++++++++++++++ docs/core/byte-order/index.adoc | 190 ++++++++ docs/core/charset-handling/index.adoc | 280 +++++++++++ docs/core/fragment-resolution/index.adoc | 151 ++++++ docs/core/index.adoc | 47 ++ docs/core/packet-protocol/index.adoc | 248 ++++++++++ docs/core/thread-safety/index.adoc | 183 +++++++ docs/getting-started/architecture.adoc | 124 +++++ docs/getting-started/index.adoc | 29 ++ docs/getting-started/installation.adoc | 51 ++ docs/getting-started/quick-start.adoc | 62 +++ .../guides/advanced/async-patterns/index.adoc | 363 ++++++++++++++ .../advanced/custom-exceptions/index.adoc | 452 ++++++++++++++++++ .../advanced/fragment-resolution/index.adoc | 261 ++++++++++ docs/guides/advanced/index.adoc | 204 ++++++++ .../advanced/performance-tuning/index.adoc | 392 +++++++++++++++ docs/guides/basic-usage/index.adoc | 158 ++++++ docs/guides/common-configurations/index.adoc | 241 ++++++++++ docs/guides/index.adoc | 44 ++ docs/index.adoc | 50 ++ docs/reference/index.adoc | 66 +++ docs/reference/javadoc.adoc | 257 ++++++++++ 32 files changed, 4642 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/build_deploy.yml create mode 100644 .github/workflows/links.yml create mode 100644 CONTRIBUTING.md create mode 100644 docs/.lychee.toml create mode 100644 docs/.lycheeignore create mode 100644 docs/Gemfile create mode 100644 docs/_config.yml create mode 100644 docs/assets/logo.svg create mode 100644 docs/core/buffer-management/index.adoc create mode 100644 docs/core/byte-order/index.adoc create mode 100644 docs/core/charset-handling/index.adoc create mode 100644 docs/core/fragment-resolution/index.adoc create mode 100644 docs/core/index.adoc create mode 100644 docs/core/packet-protocol/index.adoc create mode 100644 docs/core/thread-safety/index.adoc create mode 100644 docs/getting-started/architecture.adoc create mode 100644 docs/getting-started/index.adoc create mode 100644 docs/getting-started/installation.adoc create mode 100644 docs/getting-started/quick-start.adoc create mode 100644 docs/guides/advanced/async-patterns/index.adoc create mode 100644 docs/guides/advanced/custom-exceptions/index.adoc create mode 100644 docs/guides/advanced/fragment-resolution/index.adoc create mode 100644 docs/guides/advanced/index.adoc create mode 100644 docs/guides/advanced/performance-tuning/index.adoc create mode 100644 docs/guides/basic-usage/index.adoc create mode 100644 docs/guides/common-configurations/index.adoc create mode 100644 docs/guides/index.adoc create mode 100644 docs/index.adoc create mode 100644 docs/reference/index.adoc create mode 100644 docs/reference/javadoc.adoc diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml new file mode 100644 index 0000000..1f46278 --- /dev/null +++ b/.github/workflows/build_deploy.yml @@ -0,0 +1,55 @@ +name: build_deploy + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + working-directory: docs + + - name: Setup Pages + id: pages + uses: actions/configure-pages@v5 + + - name: Build with Jekyll + run: bundle exec jekyll build --verbose --trace --baseurl "${{ steps.pages.outputs.base_path || '' }}" + working-directory: docs + env: + JEKYLL_ENV: production + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: docs/_site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 0000000..8081419 --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,50 @@ +name: links + +on: + push: + branches: [main] + pull_request: + +jobs: + link_checker: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler-cache: true + working-directory: docs + + - name: Build site + env: + JEKYLL_ENV: production + run: bundle exec jekyll build --trace + working-directory: docs + + - name: Link Checker (Built Site) + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --verbose + --no-progress + --config docs/.lychee.toml + --root-dir "$(pwd)/docs/_site" + --base-url file://${{ github.workspace }}/docs/_site + 'docs/_site/**/*.html' + fail: true + + - name: Comment PR on failure + if: failure() && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '❌ Link checking failed. Please check the workflow logs for details.' + }) diff --git a/.gitignore b/.gitignore index f4d99b6..fe902d7 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,14 @@ build/test-results/ # Project-specific *.bak CLAUDE.md +_site + +# Internal planning docs (not for public repo) +MULTI_PACKET_RESPONSE_PLAN.md +PILAF_TEAM_PROPOSAL.md + +# Docs +docs/_site/ +docs/.lycheecache +docs/Gemfile.lock + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f0852df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing to Rcon + +Thank you for your interest in contributing to Rcon! This document provides guidelines for contributing to the project. + +## Getting Started + +1. Fork the repository on GitHub +2. Clone your fork locally: + ```bash + git clone https://github.com/YOUR_USERNAME/rcon.git + cd rcon + ``` +3. Add the upstream remote: + ```bash + git remote add upstream https://github.com/cavarest/rcon.git + ``` + +## Development + +### Building + +```bash +./gradlew build +``` + +### Running Tests + +```bash +# Unit tests only (fast, no Docker) +./gradlew test + +# Integration tests (requires Docker daemon) +./gradlew integrationTest + +# All tests +./gradlew test integrationTest +``` + +**Note**: Integration tests run against a real Minecraft Paper server using Docker. They can take 10+ minutes due to container overhead. + +### Skip Integration Tests Locally + +```bash +SKIP_INTEGRATION_TESTS=true ./gradlew test +``` + +## Code Style + +- Follow existing code style and conventions +- Use meaningful variable and method names +- Add Javadoc to public APIs +- Keep methods focused and concise + +## Documentation + +Documentation is in the `docs/` directory using Jekyll and AsciiDoc: + +```bash +cd docs +bundle install +bundle exec jekyll serve +``` + +Visit http://localhost:4000 to preview changes. + +## Commit Messages + +Use semantic commit messages: + +``` +(): +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +Example: `fix(auth): resolve login bug` + +## Pull Requests + +1. Create a feature branch from `main` +2. Make your changes with clear commit messages +3. Ensure tests pass +4. Push to your fork and create a pull request + +## Testing Your Changes + +Before submitting a PR, please: + +1. Run all tests: `./gradlew test integrationTest` +2. Build documentation: `cd docs && bundle exec jekyll build` +3. Check links: `cd docs && bundle exec lychee . --config .lychee.toml` + +## Questions? + +Feel free to open an issue for questions or discussion. diff --git a/README.adoc b/README.adoc index 1002a7d..a82a6b5 100644 --- a/README.adoc +++ b/README.adoc @@ -1,8 +1,12 @@ -= Cavarest RCON Library += Cavarest RCON Java Library image:https://img.shields.io/badge/Java-11%2B-blue[Java Version] image:https://img.shields.io/badge/Gradle-8.5-green[Gradle Version] image:https://img.shields.io/badge/License-MIT-yellow[License] +image:https://github.com/cavarest/rcon/actions/workflows/unit-tests.yml/badge.svg["Unit Tests", link="https://github.com/cavarest/pilaf/actions/workflows/unit-tests.yml"] +image:https://github.com/cavarest/rcon/actions/workflows/integration-tests.yml/badge.svg["Integration Tests", link="https://github.com/cavarest/pilaf/actions/workflows/integration-tests.yml"] + +image:https://img.shields.io/badge/Website-RCON_documentation-blue.svg["Documentation site", link="https://cavarest.github.io/rcon"] A Java library for communicating with Minecraft servers using the RCON (Remote Console) protocol. This library provides a simple and efficient API for sending @@ -63,8 +67,8 @@ src/main/java/org/cavarest/rcon/ └────────┬─────────┘ │ ┌────────▼─────────┐ - │ Rcon │ ← Core client - │ (Builder Pattern)│ + │ Rcon │ ← Core client + │ (Builder Pattern)│ └────────┬─────────┘ │ ┌──────────────┼──────────────┐ @@ -130,7 +134,8 @@ try (RconClient client = new RconClient("localhost", 25575, "password")) { === Using the Builder Pattern -The `RconClient.Builder` class provides a fluent API for constructing RCON clients: +The `RconClient.Builder` class provides a fluent API for constructing RCON +clients: [source,java] ---- @@ -269,7 +274,8 @@ All integers are stored in **little-endian** byte order. === Fragmentation and Multi-Packet Responses -The Minecraft server can fragment responses across multiple packets when the response exceeds the maximum payload size. +The Minecraft server can fragment responses across multiple packets when the +response exceeds the maximum payload size. Maximum payload sizes are: @@ -278,9 +284,12 @@ Maximum payload sizes are: ==== Automatic Multi-Packet Handling -The library automatically handles multi-packet responses - no configuration required. +The library automatically handles multi-packet responses - no configuration +required. -When a Minecraft server returns a response exceeding 4096 bytes, the library reads all packets and assembles the complete response transparently. You do not need to configure anything. +When a Minecraft server returns a response exceeding 4096 bytes, the library +reads all packets and assembles the complete response transparently. You do not +need to configure anything. [source,java] ---- @@ -296,16 +305,23 @@ try (RconClient client = new RconClient("localhost", 25575, "password")) { [NOTE] ==== -Multi-packet response handling has been verified with responses exceeding 87,000 bytes across ~22 packets. -The library automatically reads all fragments and returns the complete response regardless of size. +Multi-packet response handling has been verified with responses exceeding 87,000 +bytes across ~22 packets. The library automatically reads all fragments and +returns the complete response regardless of size. ==== ==== Advanced: Fragment Resolution Strategies -The library uses the `ACTIVE_PROBE` strategy by default, which is reliable across all server implementations. For advanced use cases, you can customize the fragment resolution strategy: +The library uses the `ACTIVE_PROBE` strategy by default, which is reliable +across all server implementations. + +For advanced use cases, you can customize the fragment resolution strategy: + +* **ACTIVE_PROBE** (default): Sends an empty command probe to detect the end of +the response. Most reliable across all server implementations. -* **ACTIVE_PROBE** (default): Sends an empty command probe to detect the end of the response. Most reliable across all server implementations. -* **TIMEOUT**: Waits for a fixed timeout period after the last packet. Use this for faster detection when server response time is predictable. +* **TIMEOUT**: Waits for a fixed timeout period after the last packet. Use this +for faster detection when server response time is predictable. [source,java] ---- @@ -335,22 +351,26 @@ Rcon rcon = Rcon.newBuilder() == Testing -This project includes both unit tests and integration tests to ensure code quality and reliability. +This project includes both unit tests and integration tests to ensure code +quality and reliability. === Unit Tests -Unit tests verify the core functionality of the RCON library without requiring external dependencies: +Unit tests verify the core functionality of the RCON library without requiring +external dependencies: [source,shell] ---- ./gradlew test ---- -Unit tests are fast and suitable for local development. Test reports are generated at `build/reports/tests/test/index.html`. +Unit tests are fast and suitable for local development. Test reports are +generated at `build/reports/tests/test/index.html`. === Integration Tests -Integration tests verify the library works correctly against a real Minecraft server running in Docker: +Integration tests verify the library works correctly against a real Minecraft +server running in Docker: [source,shell] ---- @@ -358,6 +378,7 @@ Integration tests verify the library works correctly against a real Minecraft se ---- Integration tests require Docker and take longer to run due to: + * Starting a Minecraft Paper 1.21.8 server in a Docker container * Network initialization and server startup time * Actual RCON communication with the server @@ -385,13 +406,17 @@ SKIP_INTEGRATION_TESTS=true ./gradlew test Tests are organized using JUnit 5 tags: * *Unit tests*: Tagged with `@Tag("unit")` or no tag, run with `./gradlew test` -* *Integration tests*: Tagged with `@Tag("integration")`, run with `./gradlew integrationTest` -The default `test` task excludes integration tests, ensuring fast local test execution. +* *Integration tests*: Tagged with `@Tag("integration")`, run with `./gradlew +integrationTest` + +The default `test` task excludes integration tests, ensuring fast local test +execution. == Development Workflow -This section describes the development workflow, testing procedures, and release process. +This section describes the development workflow, testing procedures, and release +process. === Pre-Release Checklist @@ -422,32 +447,30 @@ Before creating a release, ensure all tests pass: Releases are automated using GitHub Actions. To create a release: -==== Option 1: GitHub Release UI (Recommended) - -. Go to https://github.com/cavarest/rcon/releases/new[new release page] -. Create a new tag (e.g., `v0.2.0`) -. Add release notes (optional) -. Click "Publish release" -. The `Release` workflow will automatically: - - Build the project - - Publish to GitHub Packages - - Upload JAR to GitHub Releases - -==== Option 2: Manual Workflow Trigger +==== Option 1: Manual Workflow Trigger . Navigate to https://github.com/cavarest/rcon/actions/workflows/publish-release.yml[Release workflow] + . Click "Run workflow" + . Enter: - - `release-version`: e.g., `0.2.0` - - `post-release-version`: e.g., `0.2.1-SNAPSHOT` + +** `release-version`: e.g., `0.2.0` + +** `post-release-version`: e.g., `0.2.1-SNAPSHOT` . Click "Run workflow" + . The workflow will: - - Update version.properties - - Commit and push changes (creating the tag) - - Build and publish to GitHub Packages - - Upload JAR to GitHub Releases -==== Option 3: Push a Git Tag +** Update version.properties + +** Commit and push changes (creating the tag) + +** Build and publish to GitHub Packages + +** Upload JAR to GitHub Releases + +==== Option 2: Push a Git Tag [source,shell] ---- @@ -475,7 +498,9 @@ The project includes the following CI/CD workflows: === Version Management -The project version is defined in `version.properties`. To update the version: +The project version is defined in `version.properties`. + +To update the version: . Edit `version.properties`: + @@ -516,10 +541,12 @@ The JAR file will be generated at `build/libs/cavarest-rcon-{version}.jar`. ./gradlew clean ---- -== License +== Copyright and license This project is licensed under the MIT License. +Copyright to the Cavarest project. + == References * https://developer.valvesoftware.com/wiki/RCON[Source RCON Protocol] diff --git a/docs/.lychee.toml b/docs/.lychee.toml new file mode 100644 index 0000000..93a965a --- /dev/null +++ b/docs/.lychee.toml @@ -0,0 +1,20 @@ +include_verbatim = false +max_redirects = 5 +timeout = 30 +max_concurrency = 10 +accept = [200, 201, 204, 206, 301, 302, 303, 304, 307, 308] +max_retries = 3 +user_agent = "lychee/rcon-docs" + +exclude = [ + "http://localhost.*", + "http://127\\.0\\.0\\.1.*", + "mailto:.*", + "^#.*" +] + +cache = true +no_progress = true +insecure = false +scheme = ["https", "http"] +skip_missing = true diff --git a/docs/.lycheeignore b/docs/.lycheeignore new file mode 100644 index 0000000..6d5a7ed --- /dev/null +++ b/docs/.lycheeignore @@ -0,0 +1,11 @@ +# Links to skip during link checking +# Add external links that are known to be problematic but acceptable + +# The site hasn't been deployed yet, so all cavarest.github.io links will 404 +^https://cavarest\.github\.io/rcon + +# GitHub repository doesn't exist yet +^https://github\.com/cavarest/rcon + +# Javadoc package not published yet +^https://javadoc\.io/doc/io\.cavarest/rcon diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..1d9ed72 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "jekyll", "~> 4.4" +gem "jekyll-asciidoc" +gem "jekyll-seo-tag" +gem "just-the-docs" + +group :jekyll_plugins do + gem "jekyll-feed" + gem "jekyll-sitemap" +end diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..88a28a1 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,40 @@ +title: "Rcon documentation" +description: A robust RCON protocol library for Minecraft servers written in pure Java. +url: https://cavarest.github.io +baseurl: /rcon + +theme: just-the-docs +remote_theme: just-the-docs/just-the-docs + +# AsciiDoc support +asciidoc: {} +asciidoctor: + attributes: + - idprefix=_ + - source-highlighter=rouge + - icons=font + - experimental='' + +plugins: + - jekyll-asciidoc + - jekyll-seo-tag + +# Search configuration +search_enabled: true +search: + heading_level: 3 + previews: 3 + button: true + +# Navigation +nav_sort: case_insensitive + +# Logo +logo: "/assets/logo.svg" + +# Aux links +aux_links: + "Rcon on GitHub": + - "https://github.com/cavarest/rcon" + "Report Issue": + - "https://github.com/cavarest/rcon/issues" diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..166b752 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/core/buffer-management/index.adoc b/docs/core/buffer-management/index.adoc new file mode 100644 index 0000000..de7dae4 --- /dev/null +++ b/docs/core/buffer-management/index.adoc @@ -0,0 +1,426 @@ +--- +title: Buffer Management +parent: Core Concepts +nav_order: 6 +--- + +== Buffer Management + +=== Purpose + +Understanding how Rcon manages I/O buffers for efficient network communication. + +=== Overview + +Rcon uses double buffering with `ByteBuffer` for efficient network I/O. Buffers are reused rather than reallocated, reducing memory churn and GC pressure. + +=== Buffer Architecture + +[source] +---- +Application Buffer Socket + │ │ │ + │ write │ │ + ├────────────>│ │ + │ │ flush │ + │ ├────────────>│ + │ │ │ + │ read │ │ + │<────────────┤ │ + │ │ fill │ + │ │<─────────────┤ + │ │ │ + │ compact │ │ + │ │<─────────────┤ (internal) +---- + +=== Buffer Sizes + +==== Send Buffer (PacketWriter) + +[source,java] +---- +private static final int DEFAULT_SEND_BUFFER = 1460; // Typical MTU +---- + +* **Size**: 1460 bytes (typical network MTU) +* **Purpose**: Send packets efficiently +* **Rationale**: Avoids fragmentation at network layer + +==== Receive Buffer (PacketReader) + +[source,java] +---- +private static final int DEFAULT_RECEIVE_BUFFER = 4096; +---- + +* **Size**: 4096 bytes (max RCON packet payload) +* **Purpose**: Receive complete packets in one read +* **Rationale**: Matches protocol max packet size + +=== Double Buffering Pattern + +==== The Problem + +Simple buffer allocation on each read: + +[source,java] +---- +// WRONG - Allocates new buffer on every read +while (true) { + ByteBuffer buffer = ByteBuffer.allocate(4096); // New allocation! + channel.read(buffer); + // ... process packet ... +} +---- + +This causes: +* High memory allocation rate +* Increased GC pressure +* Poor performance + +==== The Solution: Double Buffering + +[source,java] +---- +// CORRECT - Reuse buffer with compact() +ByteBuffer buffer = ByteBuffer.allocate(4096); + +while (true) { + // 1. Fill buffer from channel + channel.read(buffer); + + // 2. Flip for reading + buffer.flip(); + + // 3. Process packet data + processPacket(buffer); + + // 4. Compact for reuse + buffer.compact(); // Move remaining data to beginning +} +---- + +### ByteBuffer Operations + +==== flip() + +Switch from write mode to read mode: + +[source] +---- +Before flip(): ++------------------+------------------+ +| Read Data | Unused | ++------------------+------------------+ +^ ^ ^ +0 position limit + +After flip(): ++------------------+------------------+ +| Read Data | Unused | ++------------------+------------------+ +^ ^ ^ +0 limit capacity + +position = 0 (reset to beginning) +limit = previous position (data length) +---- + +==== compact() + +Move remaining data to beginning, prepare for more writes: + +[source] +---- +Before compact(): ++------------------+------------------+ +| Read Data | Unused | ++------------------+------------------+ +^ ^ ^ ^ +0 position limit capacity + +After compact(): ++------------------+------------------+ +| Remaining Data | Free Space | ++------------------+------------------+ +^ ^ ^ +0 position limit + +- Move data from [position, limit) to [0, remaining) +- Set position = remaining +- Set limit = capacity +- Buffer ready for more writes +---- + +### Implementation: PacketReader + +[source,java] +---- +public class PacketReader implements AutoCloseable { + private static final int BUFFER_SIZE = 4096; + + private final SocketChannel channel; + private final ByteBuffer buffer; + + public PacketReader(SocketChannel channel) { + this.channel = channel; + this.buffer = ByteBuffer.allocate(BUFFER_SIZE); + } + + public Packet readPacket(Duration timeout) throws IOException { + // Read until we have a complete packet + while (true) { + // Check if we have enough data for size field + buffer.flip(); + + if (buffer.remaining() < 4) { + // Need more data + buffer.compact(); + channel.read(buffer); + continue; + } + + // Read packet size + int size = buffer.getInt(); + + // Check if we have complete packet + if (buffer.remaining() < size) { + // Need more data + buffer.compact(); + channel.read(buffer); + continue; + } + + // We have complete packet! + return decodePacket(size); + } + } + + private Packet decodePacket(int size) { + // Read ID, type, payload + int id = buffer.getInt(); + int type = buffer.getInt(); + byte[] payload = new byte[size - 8]; + buffer.get(payload); + + // Compact remaining data for next packet + buffer.compact(); + + return new Packet(id, PacketType.fromValue(type), payload); + } + + @Override + public void close() { + // No resources to release (buffer is stack-allocated) + } +} +---- + +### Implementation: PacketWriter + +[source,java] +---- +public class PacketWriter implements AutoCloseable { + private static final int BUFFER_SIZE = 1460; + + private final SocketChannel channel; + private final ByteBuffer buffer; + + public PacketWriter(SocketChannel channel) { + this.channel = channel; + this.buffer = ByteBuffer.allocate(BUFFER_SIZE); + } + + public void writePacket(Packet packet) throws IOException { + // Encode packet to buffer + buffer.clear(); + encodePacket(packet, buffer); + buffer.flip(); + + // Write buffer to channel + while (buffer.hasRemaining()) { + channel.write(buffer); + } + } + + private void encodePacket(Packet packet, ByteBuffer buffer) { + byte[] payload = packet.getPayload(); + int size = 4 + 4 + payload.length; + + buffer.putInt(size); // Size field + buffer.putInt(packet.getId()); // ID field + buffer.putInt(packet.getType().getValue()); // Type field + buffer.put(payload); // Payload + } + + @Override + public void close() { + // No resources to release (buffer is stack-allocated) + } +} +---- + +### Performance Benefits + +==== Memory Allocation + +Without double buffering: + +[source] +---- +1000 packets × 4096 bytes = 4 MB allocated ++ GC overhead for 1000 objects ++ GC pause time +---- + +With double buffering: + +[source] +---- +1 buffer × 4096 bytes = 4 KB allocated ++ No additional allocations ++ No GC overhead +---- + +**Result**: 1000x less memory allocation + +==== CPU Usage + +* **Less allocation**: Faster execution +* **Better cache locality**: Same buffer reused +* **No GC pauses**: Consistent latency + +### Tuning Buffer Sizes + +==== Large Responses + +If you frequently receive responses >4096 bytes, increase receive buffer: + +[source,java] +---- +// Custom PacketReader with larger buffer +public class LargePacketReader extends PacketReader { + private static final int BUFFER_SIZE = 65536; // 64 KB + + // ... implementation ... +} +---- + +This reduces the number of read syscalls for large responses. + +==== Many Small Commands + +If you send many small commands, smaller send buffer may reduce latency: + +[source,java] +---- +// Custom PacketWriter with smaller buffer +public class LowLatencyPacketWriter extends PacketWriter { + private static final int BUFFER_SIZE = 512; // 512 bytes + + // ... implementation ... +} +---- + +This reduces the time to fill and flush the buffer. + +### Monitoring Buffer Usage + +Add instrumentation to monitor buffer utilization: + +[source,java] +---- +public class InstrumentedPacketReader extends PacketReader { + private long totalReads = 0; + private long totalBytes = 0; + private long maxRemaining = 0; + + @Override + public Packet readPacket(Duration timeout) throws IOException { + Packet packet = super.readPacket(timeout); + + // Track metrics + totalReads++; + totalBytes += packet.getPayload().length + 12; + maxRemaining = Math.max(maxRemaining, buffer.remaining()); + + return packet; + } + + public double getAverageUtilization() { + return (double) totalBytes / (totalReads * BUFFER_SIZE); + } + + public int getMaxRemaining() { + return maxRemaining; + } +} +---- + +### Common Pitfalls + +==== Forgetting to flip() + +[source,java] +---- +// WRONG - Buffer still in write mode +ByteBuffer buffer = ByteBuffer.allocate(1024); +channel.read(buffer); +int data = buffer.getInt(); // Reads from wrong position! +---- + +[source,java] +---- +// CORRECT - Flip before reading +ByteBuffer buffer = ByteBuffer.allocate(1024); +channel.read(buffer); +buffer.flip(); // Switch to read mode +int data = buffer.getInt(); // Correct! +---- + +==== Forgetting to compact() + +[source,java] +---- +// WRONG - Loses remaining data +while (true) { + channel.read(buffer); + buffer.flip(); + processSomeData(buffer); + buffer.clear(); // Clears ALL data, including unread! +} +---- + +[source,java] +---- +// CORRECT - Compact preserves remaining data +while (true) { + channel.read(buffer); + buffer.flip(); + processSomeData(buffer); + buffer.compact(); // Preserves unread data +} +---- + +### Direct Buffers + +For high-performance scenarios, consider direct buffers: + +[source,java] +---- +ByteBuffer buffer = ByteBuffer.allocateDirect(4096); +---- + +Pros: +* May avoid one copy between kernel and userspace +* Better for I/O-heavy workloads + +Cons: +* Slightly slower allocation +* Memory not in JVM heap (harder to debug) + +### See Also + +* link:../core/packet-protocol/index.html[Packet Protocol] - Protocol specification +* link:../guides/advanced/performance-tuning/index.html[Performance Tuning] - Optimization guide diff --git a/docs/core/byte-order/index.adoc b/docs/core/byte-order/index.adoc new file mode 100644 index 0000000..2fba7f0 --- /dev/null +++ b/docs/core/byte-order/index.adoc @@ -0,0 +1,190 @@ +--- +title: Byte Order +parent: Core Concepts +nav_order: 4 +--- + +== Byte Order + +=== Purpose + +Understanding little-endian byte order used in the RCON protocol and why it matters for Java implementations. + +=== The Problem + +Java's default byte order is **big-endian** (most significant byte first), but the RCON protocol uses **little-endian** (least significant byte first). This mismatch can cause protocol failures if not handled correctly. + +=== Endianness Explained + +==== Big-Endian (Network Order, Java Default) + +Most significant byte comes first: + +[source] +---- +Integer value: 0x12345678 +Big-endian bytes: 12 34 56 78 + ↑ ↑ + MSB LSB +---- + +==== Little-Endian (Intel x86, RCON Protocol) + +Least significant byte comes first: + +[source] +---- +Integer value: 0x12345678 +Little-endian bytes: 78 56 34 12 + ↑ ↑ + LSB MSB +---- + +=== Why It Matters + +If you encode integers using Java's default big-endian, the server will interpret the bytes incorrectly: + +[source,java] +---- +// WRONG - Server receives 0x78563412 instead of 0x12345678 +ByteBuffer.allocate(4).putInt(0x12345678).array(); +// Result: 12 34 56 78 (big-endian) + +// CORRECT - Server receives 0x12345678 +ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(0x12345678) + .array(); +// Result: 78 56 34 12 (little-endian) +---- + +=== Correct Encoding + +Always specify `LITTLE_ENDIAN` when encoding packets: + +[source,java] +---- +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class PacketEncoder { + public byte[] encodeInt(int value) { + return ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) // Critical! + .putInt(value) + .array(); + } +} +---- + +=== Correct Decoding + +Also specify `LITTLE_ENDIAN` when decoding packets: + +[source,java] +---- +public class PacketDecoder { + public int decodeInt(byte[] bytes) { + return ByteBuffer.wrap(bytes) + .order(ByteOrder.LITTLE_ENDIAN) // Critical! + .getInt(); + } +} +---- + +=== Real-World Example + +Encoding the packet size field: + +[source,java] +---- +// Packet with 7-byte payload "test\0" +int payloadSize = 7; +int packetSize = payloadSize + 8; // ID(4) + Type(4) + Payload +// packetSize = 15 (0x0000000F) + +// WRONG - Big-endian +ByteBuffer.allocate(4).putInt(15).array(); +// Result: 00 00 00 0F +// Server interprets as: 0x0F000000 = 251,658,240 + +// CORRECT - Little-endian +ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(15) + .array(); +// Result: 0F 00 00 00 +// Server interprets as: 0x0000000F = 15 +---- + +=== Testing Endianness + +Verify your byte order with a simple test: + +[source,java] +---- +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@Test +public void testLittleEndian() { + int value = 0x12345678; + + byte[] littleEndian = ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value) + .array(); + + assertArrayEquals( + new byte[] {0x78, 0x56, 0x34, 0x12}, + littleEndian + ); +} +---- + +=== Common Pitfalls + +==== Using Arrays Directly + +[source,java] +---- +// WRONG - No byte order control +byte[] size = new byte[4]; +size[0] = (byte) (value >> 24); +size[1] = (byte) (value >> 16); +size[2] = (byte) (value >> 8); +size[3] = (byte) value; +// This is big-endian! + +// CORRECT - Use ByteBuffer +byte[] size = ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value) + .array(); +---- + +==== DataOutputStream + +[source,java] +---- +// WRONG - DataOutputStream uses big-endian +ByteArrayOutputStream baos = new ByteArrayOutputStream(); +DataOutputStream dos = new DataOutputStream(baos); +dos.writeInt(value); // Big-endian! + +// CORRECT - Use ByteBuffer +ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value) + .array(); +---- + +=== Performance Note + +`ByteBuffer` with explicit byte order has no performance penalty over other methods. The JIT compiler optimizes it well. + +=== See Also + +* link:packet-protocol/index.html[Packet Protocol] - Binary packet structure +* link:../../internals/protocol-spec.html[Protocol Specification] - Complete protocol docs +* link:charset-handling/index.html[Charset Handling] - Text encoding details diff --git a/docs/core/charset-handling/index.adoc b/docs/core/charset-handling/index.adoc new file mode 100644 index 0000000..ecae1f9 --- /dev/null +++ b/docs/core/charset-handling/index.adoc @@ -0,0 +1,280 @@ +--- +title: Charset Handling +parent: Core Concepts +nav_order: 5 +--- + +== Charset Handling + +=== Purpose + +Understanding how Rcon handles character encoding for commands and responses, especially for Minecraft color codes. + +=== Overview + +RCON is fundamentally a binary protocol, but text payloads are encoded as byte arrays. The choice of character encoding affects how color codes and special characters are preserved. + +=== Default Encoding: UTF-8 + +UTF-8 is the default encoding for all text: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + // UTF-8 is default + .build(); +---- + +UTF-8 works correctly for: +* Standard ASCII text +* Unicode characters (emoji, international characters) +* Most command output + +=== Color Code Encoding: ISO-8859-1 + +Minecraft uses section signs (`§`) for color codes. These color codes are not standard ASCII and require special handling. + +==== The Problem + +In UTF-8, the section sign (`§`) is encoded as two bytes: +* `§` = `0xC2 0xA7` (UTF-8) + +However, Minecraft's internal encoding treats it as: +* `§` = `0xA7` (single byte, ISO-8859-1) + +This mismatch can corrupt color codes in responses. + +==== The Solution + +Use ISO-8859-1 (also called Latin-1) charset to preserve color codes: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .charset(StandardCharsets.ISO_8859_1) + .build(); +---- + +=== Example: Color Code Preservation + +[source,java] +---- +// With UTF-8 (default) +RconClient utf8Client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +RconResponse response = utf8Client.sendCommand("list"); +System.out.println(response.getResponse()); +// Output: ??c[Server] ??fOnline players: 3 +// Color codes are corrupted! + +// With ISO-8859-1 +RconClient latin1Client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .charset(StandardCharsets.ISO_8859_1) + .build(); + +RconResponse response = latin1Client.sendCommand("list"); +System.out.println(response.getResponse()); +// Output: §c[Server] §fOnline players: 3 +// Color codes are preserved! +---- + +=== Minecraft Color Codes + +Standard Minecraft color codes: + +[width="100%",cols="^1,2,6"] +|=== +|Code|Color|Name + +|`§0` +|Black +|BLACK + +|`§1` +|Dark Blue +|DARK_BLUE + +|`§2` +|Dark Green +|DARK_GREEN + +|`§3` +|Dark Aqua +|DARK_AQUA + +|`§4` +|Dark Red +|DARK_RED + +|`§5` +|Dark Purple +|DARK_PURPLE + +|`§6` +|Gold +|GOLD + +|`§7` +|Gray +|GRAY + +|`§8` +|Dark Gray +|DARK_GRAY + +|`§9` +|Blue +|BLUE + +|`§a` +|Green +|GREEN + +|`§b` +|Aqua +|AQUA + +|`§c` +|Red +|RED + +|`§d` +|Light Purple +|LIGHT_PURPLE + +|`§e` +|Yellow +|YELLOW + +|`§f` +|White +|WHITE + +|=== + +Formatting codes: + +[width="100%",cols="^1,2,6"] +|=== +|Code|Effect|Name + +|`§k` +|Obfuscated +|MAGIC + +|`§l` +|Bold +|BOLD + +|`§m` +|Strikethrough +|STRIKETHROUGH + +|`§n` +|Underline +|UNDERLINE + +|`§o` +|Italic +|ITALIC + +|`§r` +|Reset +|RESET + +|=== + +=== When to Use Each Encoding + +==== Use UTF-8 (Default) When: + +* You don't need color codes +* Working with international characters +* Processing log files +* Using modern Minecraft versions without color codes + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + // UTF-8 is default, no need to specify + .build(); +---- + +==== Use ISO-8859-1 When: + +* You need to preserve Minecraft color codes +* Processing chat messages with formatting +* Parsing server responses with `§` codes +* Working with legacy Minecraft versions + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .charset(StandardCharsets.ISO_8859_1) + .build(); +---- + +=== Encoding Comparison + +[source,java] +---- +String text = "§c[Server] §fWelcome!"; + +// UTF-8 encoding +byte[] utf8 = text.getBytes(StandardCharsets.UTF_8); +// Result: C2 A7 63 5B 53 65 72 76 65 72 5D 20 C2 A7 66 57 65 6C 63 6F 6D 65 21 + +// ISO-8859-1 encoding +byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1); +// Result: A7 63 5B 53 65 72 76 65 72 5D 20 A7 66 57 65 6C 63 6F 6D 65 21 + +// UTF-8 uses 2 bytes for § (C2 A7) +// ISO-8859-1 uses 1 byte for § (A7) +---- + +=== Strip Color Codes + +If you don't need color codes, strip them for cleaner output: + +[source,java] +---- +public static String stripColorCodes(String input) { + return input.replaceAll("§[0-9a-fk-or]", ""); +} + +String response = client.sendCommand("list").getResponse(); +String cleanResponse = stripColorCodes(response); +System.out.println(cleanResponse); +// Output: [Server] Online players: 3 +---- + +=== Performance Notes + +* ISO-8859-1 is slightly faster than UTF-8 (single-byte encoding) +* Both are hardware-accelerated on modern JVMs +* Performance difference is negligible for typical RCON usage + +=== See Also + +* link:packet-protocol/index.html[Packet Protocol] - Binary packet structure +* link:../guides/common-configurations/index.html[Common Configurations] - Client setup examples +* link:../../internals/protocol-spec.html[Protocol Specification] - Complete protocol docs diff --git a/docs/core/fragment-resolution/index.adoc b/docs/core/fragment-resolution/index.adoc new file mode 100644 index 0000000..996a713 --- /dev/null +++ b/docs/core/fragment-resolution/index.adoc @@ -0,0 +1,151 @@ +--- +title: Fragment Resolution +parent: Core Concepts +has_children: true +nav_order: 1 +--- + +== Fragment Resolution + +=== Purpose + +Understanding how Rcon handles multi-packet responses when server output exceeds the 4096-byte packet size limit. + +=== The Problem + +Minecraft RCON protocol limits server responses to 4096 bytes per packet. When a command produces more output (like listing 512 entities on a server), the response is split across multiple packets. + +For example, a large response might look like: + +[source] +---- +Packet 1: "There are 512 out of max 20 player..." +Packet 2: "Entity 1: Zombie at 123.5, 64.0, 2" +Packet 3: "Entity 2: Skeleton at 128.1, 64.0, -5" +... +Packet 22: "Entity 512: Enderman at 456.7, 72.0, 123" +---- + +=== The Solution + +Rcon provides two strategies to detect when all packets have been received: + +==== ACTIVE_PROBE Strategy (Default) + +After receiving the last packet, Rcon sends an empty command probe. The server's response to this probe signals that the previous response is complete. + +[source] +---- +Client: SEND "list" (ID=1) +Server: RESPONSE (ID=1, Packet 1) +Server: RESPONSE (ID=1, Packet 2) +Server: END (ID=1, Packet 3) + +Client: SEND "" (ID=2, probe) +Server: RESPONSE (ID=2, "") + +Client: Assemble Packets 1-3 into complete response +---- + +*Advantages*: +- **Explicit detection** - No guessing when response is complete +- **Minimal latency** - Only one extra round-trip +- **High reliability** - Works with all server implementations + +*Use Cases*: +- Most production scenarios (recommended default) +- When response latency is critical +- With servers that handle empty commands properly + +==== TIMEOUT Strategy + +After receiving the last packet, Rcon waits a fixed duration (default: 100ms). If no additional packets arrive within this window, the response is considered complete. + +[source] +---- +Client: SEND "list" (ID=1) +Server: RESPONSE (ID=1, Packet 1) +Server: RESPONSE (ID=1, Packet 2) +Server: END (ID=1, Packet 3) + +[Wait 100ms] + +Client: Assemble Packets 1-3 into complete response +---- + +*Advantages*: +- **Simpler** - No extra probe command needed +- **Works with problematic servers** - When empty commands cause issues + +*Disadvantages*: +- **Added latency** - Must wait full timeout duration on every command +- **Less reliable** - May cut off responses if timeout is too short + +*Use Cases*: +- When server has issues with empty probe commands +- When extra round-trip latency is acceptable +- Testing/debugging scenarios + +=== Configuration + +Select the fragment resolution strategy when building the client: + +[source,java] +---- +// Use ACTIVE_PROBE (default) +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .fragmentStrategy(FragmentResolutionStrategy.ACTIVE_PROBE) + .build(); + +// Use TIMEOUT with custom duration +RconClient timeoutClient = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .timeout(Duration.ofMillis(200)) // Custom timeout + .build(); +---- + +=== Automatic Assembly + +Both strategies handle multi-packet responses **automatically**. You don't need to write any special code: + +[source,java] +---- +// This works for responses of ANY size +RconResponse response = client.sendCommand("list"); + +// Even if the response is 87,000 bytes across 22 packets, +// the complete assembled response is available here +String completeResponse = response.getResponse(); +---- + +=== Packet Assembly Process + +Behind the scenes, Rcon: + +. Sends the command with a unique request ID +. Receives all packets matching that request ID +. Orders packets by their sequence +. Concatenates payloads into complete response +. Signals completion using the chosen strategy + +All of this happens transparently - you simply call `sendCommand()` and get the complete response. + +=== Verified Scale + +The ACTIVE_PROBE strategy has been tested and verified with: + +* **Single packet responses** - < 4096 bytes +* **Large responses** - Up to 87,000 bytes across ~22 packets +* **Concurrent commands** - Multiple threads sending commands simultaneously + +=== See Also + +* link:../thread-safety/index.html[Thread Safety Model] - How concurrent requests are handled +* link:../packet-protocol/index.html[Packet Protocol] - Binary packet structure +* link:../../internals/fragment-algorithms.html[Fragment Algorithms] - Implementation details diff --git a/docs/core/index.adoc b/docs/core/index.adoc new file mode 100644 index 0000000..7410b1c --- /dev/null +++ b/docs/core/index.adoc @@ -0,0 +1,47 @@ +--- +title: Core Concepts +has_children: true +nav_order: 3 +--- + +== Core Concepts + +=== Purpose + +Deep dive into the internal architecture and design decisions of Rcon. + +=== Topics + +* link:fragment-resolution/index.html[Fragment Resolution] - How multi-packet responses are handled +* link:thread-safety/index.html[Thread Safety Model] - Concurrent command execution +* link:packet-protocol/index.html[Packet Protocol] - Binary encoding details +* link:byte-order/index.html[Byte Order] - Little-endian encoding +* link:buffer-management/index.html[Buffer Management] - I/O buffer strategies +* link:charset-handling/index.html[Charset Handling] - Text encoding for color codes + +=== Architecture + +Rcon follows a layered architecture with clear separation of concerns: + +. **High-Level API** (`RconClient`) - User-friendly interface with logging +. **Core API** (`Rcon`) - Thread-safe protocol implementation +. **I/O Layer** (`PacketReader`/`PacketWriter`) - Socket operations +. **Codec Layer** (`PacketCodec`) - Binary encoding/decoding +. **Domain Model** (`Packet`) - Immutable packet representation + +Each layer has a single responsibility and can be used independently if needed. + +=== Design Principles + +* **Zero Dependencies** - Pure Java standard library +* **Immutability** - `Packet` objects are immutable +* **Thread Safety** - Safe concurrent access via synchronized methods +* **Separation of Concerns** - Clear layer boundaries +* **Configurability** - Pluggable strategies and options + +=== Performance Characteristics + +* **Minimal Allocation** - Buffers are reused via `compact()` +* **Lock-Free Counter** - `AtomicInteger` for request IDs +* **Efficient Encoding** - Direct buffer manipulation +* **Smart Fragment Resolution** - Default strategy minimizes round-trips diff --git a/docs/core/packet-protocol/index.adoc b/docs/core/packet-protocol/index.adoc new file mode 100644 index 0000000..80a3d8b --- /dev/null +++ b/docs/core/packet-protocol/index.adoc @@ -0,0 +1,248 @@ +--- +title: Packet Protocol +parent: Core Concepts +nav_order: 3 +--- + +== Packet Protocol + +=== Purpose + +Understanding the binary packet format used by the RCON protocol for communication between client and server. + +=== Overview + +RCON uses a simple binary packet format with four fields: size, ID, type, and payload. All packets follow this structure regardless of direction (client-to-server or server-to-client). + +=== Packet Structure + +[width="100%",cols="^1,^2,4,8"] +|=== +|Field|Size|Type|Description + +|Size +|4 bytes +|int32 (little-endian) +|Length of ID, Type, and Payload fields combined + +|ID +|4 bytes +|int32 (little-endian) +|Request/response identifier for matching + +|Type +|4 bytes +|int32 (little-endian) +|Packet type (see link:#packet-types[Packet Types]) + +|Payload +|Variable +|byte array +|Actual data, null-terminated with `\0` + +|=== + +[source] +---- ++-----------+-----------+-----------+-----------+ +| Size (4) | ID (4) | Type (4) | Payload | ++-----------+-----------+-----------+-----------+ +| Little-endian integers | Variable | ++-----------+-----------+-----------+-----------+ +---- + +=== Packet Types + +[width="100%",cols="^1,2,6"] +|=== +|Value|Constant|Description + +|0 +|`SERVERDATA_AUTH_RESPONSE` +|Authentication response from server + +|2 +|`SERVERDATA_EXECCOMMAND` +|Command response from server + +|3 +|`SERVERDATA_AUTH` +|Authentication request from client + +|=== + +=== Size Field Calculation + +The Size field contains the length of everything **after** itself: + +[source,java] +---- +int size = 4 (ID) + 4 (Type) + payload.length + 1 (null terminator); +---- + +For example, a password "secret" would have: +- Payload: 6 bytes (`s`, `e`, `c`, `r`, `e`, `t`) +- Null terminator: 1 byte +- ID: 4 bytes +- Type: 4 bytes +- **Size = 4 + 4 + 6 + 1 = 15** + +=== ID Field + +The ID field matches requests with responses: + +* **Client-to-server**: Any positive integer (typically auto-incrementing) +* **Server-to-client**: Matches the request ID, or `0` for authentication responses + +[source,java] +---- +// Client generates unique ID +int requestId = requestCounter.getAndIncrement(); + +// Server responds with same ID +// All response packets for this request use the same ID +---- + +=== Payload Encoding + +==== Default: UTF-8 + +Standard text encoding for most commands: + +[source,java] +---- +Charset charset = StandardCharsets.UTF_8; +byte[] payload = command.getBytes(charset); +---- + +==== Color Codes: ISO-8859-1 + +Preserve Minecraft color codes like `§c[Server]`: + +[source,java] +---- +Charset charset = StandardCharsets.ISO_8859_1; +byte[] payload = command.getBytes(charset); +---- + +=== Null Termination + +All payloads must end with a `\0` (null) byte: + +[source,java] +---- +byte[] payload = command.getBytes(charset); +byte[] terminatedPayload = new byte[payload.length + 1]; +System.arraycopy(payload, 0, terminatedPayload, 0, payload.length); +// terminatedPayload[payload.length] is already 0 +---- + +=== Example Packets + +==== Authentication Request + +[source] +---- +Size: 12 +ID: 1 +Type: 3 +Payload: "secret\0" + +Hex (little-endian): +0C 00 00 00 // Size = 12 +01 00 00 00 // ID = 1 +03 00 00 00 // Type = 3 (AUTH) +73 65 63 72 65 74 00 // "secret\0" +---- + +==== Command Request + +[source] +---- +Size: 15 +ID: 2 +Type: 3 +Payload: "list\0" + +Hex (little-endian): +0F 00 00 00 // Size = 15 +02 00 00 00 // ID = 2 +03 00 00 00 // Type = 3 (AUTH) +6C 69 73 74 00 // "list\0" +---- + +==== Authentication Response (Success) + +[source] +---- +Size: 10 +ID: 0 +Type: 2 +Payload: "\0" + +Hex (little-endian): +0A 00 00 00 // Size = 10 +00 00 00 00 // ID = 0 (auth response) +02 00 00 00 // Type = 2 (RESPONSE) +00 // Empty payload (success) +---- + +==== Authentication Response (Failure) + +[source] +---- +Size: 21 +ID: 0 +Type: 2 +Payload: "Authentication failed\0" + +Hex (little-endian): +15 00 00 00 // Size = 21 +00 00 00 00 // ID = 0 +02 00 00 00 // Type = 2 (RESPONSE) +41 75 74 68 65 6E 74 69 63 61 74 69 6F 6E 20 66 61 69 6C 65 64 00 +// "Authentication failed\0" +---- + +=== Size Limits + +[width="100%",cols="^2,^2,6"] +|=== +|Direction|Limit|Notes + +|Client → Server +|1460 bytes total +|Typical MTU, includes headers + +|Server → Client +|4104 bytes total +|4096 bytes payload + headers + +|=== + +Responses exceeding 4096 bytes are split across multiple packets. See link:fragment-resolution/index.html[Fragment Resolution] for details. + +=== Byte Order + +**Critical**: All integer fields use **little-endian** byte order, opposite of Java's default big-endian. + +[source,java] +---- +// WRONG - uses big-endian +byte[] bytes = ByteBuffer.allocate(4).putInt(value).array(); + +// CORRECT - uses little-endian +byte[] bytes = ByteBuffer.allocate(4) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(value) + .array(); +---- + +See link:byte-order/index.html[Byte Order] for more details. + +=== See Also + +* link:fragment-resolution/index.html[Fragment Resolution] - Multi-packet responses +* link:byte-order/index.html[Byte Order] - Little-endian encoding details +* link:charset-handling/index.html[Charset Handling] - Text encoding options +* link:../../internals/protocol-spec.html[Protocol Specification] - Complete protocol docs diff --git a/docs/core/thread-safety/index.adoc b/docs/core/thread-safety/index.adoc new file mode 100644 index 0000000..a5c8b5a --- /dev/null +++ b/docs/core/thread-safety/index.adoc @@ -0,0 +1,183 @@ +--- +title: Thread Safety Model +parent: Core Concepts +nav_order: 2 +--- + +== Thread Safety Model + +=== Purpose + +Understanding how Rcon handles concurrent command execution safely. + +=== Overview + +Rcon is designed for thread-safe concurrent access. Multiple threads can send commands simultaneously without interfering with each other's responses. + +=== Thread Safety Guarantees + +==== Synchronized Methods + +The core `Rcon.writeAndRead()` and `Rcon.read()` methods are **synchronized**, ensuring that: + +. Only one thread can write/read at a time +. Request and response IDs always match correctly +. Responses are never delivered to the wrong thread + +[source,java] +---- +public class Rcon { + public synchronized RconResponse writeAndRead(Packet request) { + // Thread-safe request/response cycle + int requestId = requestCounter.getAndIncrement(); + // ... send and receive logic ... + return response; + } +} +---- + +==== Lock-Free Request IDs + +The `requestCounter` uses `AtomicInteger`, providing thread-safe incrementation without synchronization overhead: + +[source,java] +---- +private final AtomicInteger requestCounter = new AtomicInteger(0); + +// Multiple threads can safely get unique IDs +int id1 = requestCounter.getAndIncrement(); // Thread A +int id2 = requestCounter.getAndIncrement(); // Thread B +int id3 = requestCounter.getAndIncrement(); // Thread C +// All IDs are guaranteed to be unique +---- + +=== Concurrent Usage Example + +Multiple threads can safely share a single `RconClient`: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +// Thread 1 +Thread thread1 = new Thread(() -> { + RconResponse r1 = client.sendCommand("list"); + System.out.println("Thread 1: " + r1.getResponse()); +}); + +// Thread 2 +Thread thread2 = new Thread(() -> { + RconResponse r2 = client.sendCommand("seed"); + System.out.println("Thread 2: " + r2.getResponse()); +}); + +// Thread 3 +Thread thread3 = new Thread(() -> { + RconResponse r3 = client.sendCommand("difficulty"); + System.out.println("Thread 3: " + r3.getResponse()); +}); + +// Start all threads +thread1.start(); +thread2.start(); +thread3.start(); + +// Wait for completion +thread1.join(); +thread2.join(); +thread3.join(); + +client.close(); +---- + +=== Request/Response Matching + +Each thread gets its own response matching its request: + +[source] +---- +Thread A sends request ID=5 → receives response ID=5 +Thread B sends request ID=6 → receives response ID=6 +Thread C sends request ID=7 → receives response ID=7 + +The synchronization ensures no mixing of responses. +---- + +=== Connection-Level Safety + +While methods are synchronized, they share a single TCP connection. This means: + +* **Commands are serialized** - Only one command at a time over the wire +* **Fair ordering** - Threads are served in arrival order at the synchronized block +* **No response mixing** - Each thread waits for its specific response + +=== High-Throughput Scenarios + +For applications requiring high concurrency, consider: + +==== Connection Pooling + +Create multiple client instances: + +[source,java] +---- +List pool = new ArrayList<>(); +for (int i = 0; i < 5; i++) { + pool.add(RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build()); +} + +// Distribute load across connections +// (See Common Configurations for full example) +---- + +==== Async API + +Use the asynchronous API to avoid blocking threads: + +[source,java] +---- +CompletableFuture future1 = client.sendCommandAsync("list"); +CompletableFuture future2 = client.sendCommandAsync("seed"); + +// Both commands execute without blocking the calling thread +future1.thenAccept(r -> System.out.println(r.getResponse())); +future2.thenAccept(r -> System.out.println(r.getResponse())); +---- + +=== CS:GO Quirk Handling + +Some game servers (notably CS:GO) send an empty response immediately before authentication. Rcon handles this transparently: + +[source,java] +---- +// In Rcon.authenticate(): +// If server sends empty response before auth response, +// silently consume it and continue waiting for auth response +---- + +This quirk handling is also thread-safe due to the synchronized methods. + +=== Thread Safety Summary + +| Component | Thread-Safe | Mechanism | +|-----------|--------------|------------| +| `Rcon.writeAndRead()` | Yes | `synchronized` | +| `Rcon.read()` | Yes | `synchronized` | +| `requestCounter` | Yes | `AtomicInteger` | +| `RconClient.sendCommand()` | Yes | Delegates to synchronized methods | +| `Packet` | Yes | Immutable | +| `PacketCodec` | Yes | Stateless | + +=== See Also + +* link:fragment-resolution/index.html[Fragment Resolution] - Multi-packet handling +* link:../guides/advanced/index.html[Advanced Topics] - High-throughput patterns +* link:../../internals/implementation-notes.html[Implementation Notes] - Development guidance diff --git a/docs/getting-started/architecture.adoc b/docs/getting-started/architecture.adoc new file mode 100644 index 0000000..c785774 --- /dev/null +++ b/docs/getting-started/architecture.adoc @@ -0,0 +1,124 @@ +--- +title: Architecture Overview +parent: Getting Started +nav_order: 3 +--- + +== Architecture Overview + +=== Purpose + +Understand the layered architecture of Rcon and how each layer contributes to reliable remote command execution. + +=== Layer Structure + +Rcon is organized into four distinct layers, each with a specific responsibility: + +[source] +---- +┌─────────────────────────────────────────┐ +│ RconClient (High-Level API) │ ← You interact here +│ - Logging and connection management │ +│ - Builder pattern for configuration │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ Rcon (Core API) │ ← Thread-safe operations +│ - Synchronized request/response │ +│ - Fragment resolution strategies │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ PacketReader / PacketWriter │ ← Low-level I/O +│ - NIO socket operations │ +│ - Buffer management │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ PacketCodec │ ← Binary encoding +│ - Protocol encoding/decoding │ +│ - Configurable charset │ +└────────────────┬────────────────────────┘ + │ +┌────────────────▼────────────────────────┐ +│ Packet (Domain Model) │ ← Immutable data +│ - PacketType constants │ +└─────────────────────────────────────────┘ +---- + +=== Layer Responsibilities + +==== RconClient + +The high-level API that wraps `Rcon` with additional functionality: + +* **Logging** - Automatic request/response logging +* **Connection Management** - Handles opening/closing connections +* **Builder Pattern** - Fluent API for configuration +* **Async Support** - `sendCommandAsync()` for non-blocking operations + +==== Rcon + +The core API providing thread-safe RCON operations: + +* **Synchronized Methods** - `writeAndRead()` and `read()` ensure request/response matching +* **AtomicInteger Request Counter** - Thread-safe ID generation without synchronization overhead +* **Fragment Resolution** - Configurable strategies for multi-packet responses + +==== PacketReader / PacketWriter + +Low-level NIO socket I/O operations: + +* **Buffer Management** - Double-buffering with `compact()` after reads +* **Default Buffers** - 4KB receive, 1460-byte send (typical MTU) +* **Blocking I/O** - Uses `SocketChannel` for simplicity + +==== PacketCodec + +Binary protocol encoding/decoding: + +* **Little-Endian** - All integers use little-endian byte order +* **Charset Support** - UTF-8 default, ISO-8859-1 for Minecraft color codes +* **Packet Structure** - Encodes/decodes size, ID, type, and payload + +=== Fragment Resolution Strategies + +When server responses exceed 4096 bytes, they are split across multiple packets. Rcon provides two strategies: + +==== ACTIVE_PROBE (Default) + +Sends an empty command probe to detect the end of response: + +* **Most Reliable** - Explicitly detects response completion +* **Minimal Latency** - Only one extra round-trip +* **Recommended** - Use for most scenarios + +==== TIMEOUT + +Waits a fixed duration after receiving the last packet: + +* **Simpler** - No extra probe command needed +* **Added Latency** - Must wait full timeout duration +* **Use Case** - When probe commands cause issues + +=== Thread Safety Model + +* `Rcon.writeAndRead()` and `read()` are **synchronized** +* `requestCounter` uses `AtomicInteger` for lock-free incrementing +* Multiple threads can safely send commands concurrently +* Each thread gets its own response with matching request ID + +=== Multi-Packet Handling + +Multi-packet responses are **fully automatic** - no configuration needed: + +* Responses of any size are handled transparently +* Tested with responses exceeding 87,000 bytes (~22 packets) +* Packets are assembled in the correct order using the request ID +* The default API requires no special handling for large responses + +=== Next Steps + +* link:../guides/basic-usage/index.html[Basic Usage] - Learn common usage patterns +* link:../core/fragment-resolution/index.html[Fragment Resolution] - Deep dive on multi-packet handling +* link:../core/packet-protocol/index.html[Packet Protocol] - Binary protocol details diff --git a/docs/getting-started/index.adoc b/docs/getting-started/index.adoc new file mode 100644 index 0000000..c4448ff --- /dev/null +++ b/docs/getting-started/index.adoc @@ -0,0 +1,29 @@ +--- +title: Getting Started +has_children: true +nav_order: 1 +--- + +== Getting Started + +=== Purpose + +This section helps you get up and running with Rcon quickly and efficiently. + +=== Overview + +Rcon is designed to be simple to use while providing robust features for production use: + +* **Zero setup** - Add the dependency and you're ready to go +* **Simple API** - Send commands with a single method call +* **Automatic multi-packet handling** - Large responses are assembled transparently +* **Thread-safe operations** - Use from multiple threads without worry + +=== Learning Path + +We recommend following these guides in order: + +. link:installation[Installation] - Set up Rcon in your project +. link:quick-start[Quick Start] - Send your first command in 5 minutes +. link:architecture[Architecture Overview] - Understand how the library works +. link:configuration[Configuration Options] - Customize Rcon behavior diff --git a/docs/getting-started/installation.adoc b/docs/getting-started/installation.adoc new file mode 100644 index 0000000..c47f7d4 --- /dev/null +++ b/docs/getting-started/installation.adoc @@ -0,0 +1,51 @@ +--- +title: Installation +parent: Getting Started +nav_order: 1 +--- + +== Installation + +=== Purpose + +Add Rcon to your Java project using your preferred build system. + +=== Maven + +Add the dependency to your `pom.xml`: + +[source,xml] +---- + + io.cavarest + rcon + 0.2.1 + +---- + +=== Gradle + +Add the dependency to your `build.gradle`: + +[source,gradle] +---- +implementation 'io.cavarest:rcon:0.2.1' +---- + +=== Gradle (Kotlin DSL) + +Add the dependency to your `build.gradle.kts`: + +[source,kotlin] +---- +implementation("io.cavarest:rcon:0.2.1") +---- + +=== Requirements + +* Java 11 or higher +* No additional dependencies required + +=== Version Check + +To verify the installation, you can check the latest release on link:https://github.com/cavarest/rcon/releases[GitHub]. diff --git a/docs/getting-started/quick-start.adoc b/docs/getting-started/quick-start.adoc new file mode 100644 index 0000000..d3b7dbd --- /dev/null +++ b/docs/getting-started/quick-start.adoc @@ -0,0 +1,62 @@ +--- +title: Quick Start +parent: Getting Started +nav_order: 2 +--- + +== Quick Start + +=== Purpose + +Send your first RCON command in under 5 minutes. + +=== Basic Usage + +Create an `RconClient` and send a command: + +[source,java] +---- +import io.cavarest.rcon.RconClient; +import io.cavarest.rcon.RconResponse; + +public class Example { + public static void main(String[] args) { + // Create the client + RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("your_password") + .build(); + + // Send a command + RconResponse response = client.sendCommand("list"); + + // Print the response + System.out.println("Response: " + response.getResponse()); + + // Close when done + client.close(); + } +} +---- + +=== Expected Output + +[source] +---- +Response: There are 5 out of max 20 players online: +Player1, Player2, Player3, Player4, Player5 +---- + +=== What's Happening? + +1. **Client Creation** - The `RconClient.Builder` configures connection parameters +2. **Authentication** - The client automatically authenticates on first use +3. **Command Execution** - The command is sent and response is received +4. **Multi-Packet Handling** - Large responses are automatically assembled (you don't need to do anything!) + +=== Next Steps + +* link:../getting-started/architecture.html[Architecture Overview] - Understand the layers +* link:../guides/basic-usage/index.html[Basic Usage Guide] - Learn common patterns +* link:../reference/index.html[API Reference] - Explore the full API diff --git a/docs/guides/advanced/async-patterns/index.adoc b/docs/guides/advanced/async-patterns/index.adoc new file mode 100644 index 0000000..df3c1a5 --- /dev/null +++ b/docs/guides/advanced/async-patterns/index.adoc @@ -0,0 +1,363 @@ +--- +title: Async Programming Patterns +parent: Advanced Topics +nav_order: 2 +--- + +== Async Programming Patterns + +=== Purpose + +Using Rcon with asynchronous and reactive programming patterns. + +=== Overview + +Rcon provides both synchronous (`sendCommand()`) and asynchronous (`sendCommandAsync()`) APIs. The async API returns `CompletableFuture` for non-blocking operations. + +=== Built-in Async API + +==== Basic Async Command + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +CompletableFuture future = client.sendCommandAsync("list"); + +// Do other work while command executes... + +// Get response when ready +RconResponse response = future.get(); +System.out.println(response.getResponse()); +---- + +==== With Callbacks + +[source,java] +---- +client.sendCommandAsync("seed") + .thenAccept(response -> { + System.out.println("Seed: " + response.getResponse()); + }) + .exceptionally(throwable -> { + System.err.println("Failed: " + throwable.getMessage()); + return null; + }); +---- + +==== Chaining Multiple Commands + +[source,java] +---- +client.sendCommandAsync("list") + .thenCompose(response -> { + System.out.println("Players: " + response.getResponse()); + return client.sendCommandAsync("seed"); + }) + .thenAccept(response -> { + System.out.println("Seed: " + response.getResponse()); + }); +---- + +=== Parallel Execution + +==== Multiple Independent Commands + +[source,java] +---- +CompletableFuture listFuture = client.sendCommandAsync("list"); +CompletableFuture seedFuture = client.sendCommandAsync("seed"); +CompletableFuture diffFuture = client.sendCommandAsync("difficulty"); + +// All commands execute in parallel + +// Wait for all to complete +CompletableFuture.allOf(listFuture, seedFuture, diffFuture) + .thenAccept(v -> { + System.out.println("List: " + listFuture.join().getResponse()); + System.out.println("Seed: " + seedFuture.join().getResponse()); + System.out.println("Difficulty: " + diffFuture.join().getResponse()); + }); +---- + +==== Batch Processing + +[source,java] +---- +List commands = List.of("list", "seed", "difficulty", "weather"); + +List> futures = commands.stream() + .map(client::sendCommandAsync) + .toList(); + +// Collect all responses +CompletableFuture allOf = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0]) +); + +List responses = allOf.thenApply(v -> + futures.stream() + .map(CompletableFuture::join) + .toList() +).join(); +---- + +=== Custom Executor Service + +==== Using Dedicated Thread Pool + +[source,java] +---- +ExecutorService executor = Executors.newFixedThreadPool(10); + +CompletableFuture future = client.sendCommandAsync("list", executor); + +future.thenAcceptAsync(response -> { + // Process response in executor thread + System.out.println("Response: " + response.getResponse()); +}, executor); + +future.whenComplete((result, error) -> { + // Cleanup + executor.shutdown(); +}); +---- + +==== Virtual Threads (Java 21+) + +[source,java] +---- +ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + +CompletableFuture future = client.sendCommandAsync("list", executor); + +future.thenAccept(response -> { + System.out.println("Response: " + response.getResponse()); +}); +---- + +=== Reactive Integration + +==== Project Reactor + +[source,java] +---- +import reactor.core.publisher.Mono; + +public class ReactiveRcon { + private final RconClient client; + + public Mono sendCommand(String command) { + return Mono.fromCallable(() -> + client.sendCommand(command).getResponse() + ); + } + + public Mono> sendCommands(List commands) { + return Flux.fromIterable(commands) + .flatMap(this::sendCommand) + .collectList(); + } +} +---- + +Usage: + +[source,java] +---- +ReactiveRcon reactive = new ReactiveRcon(client); + +reactive.sendCommand("list") + .doOnNext(System.out::println) + .doOnError(e -> System.err.println("Error: " + e)) + .subscribe(); +---- + +==== RxJava + +[source,java] +---- +import io.reactivex.Single; + +public class RxRcon { + private final RconClient client; + + public Single sendCommand(String command) { + return Single.fromCallable(() -> + client.sendCommand(command).getResponse() + ); + } + + public Single> sendCommands(List commands) { + return Observable.fromIterable(commands) + .flatMapSingle(this::sendCommand) + .toList(); + } +} +---- + +=== Coroutine Integration (Kotlin) + +==== Suspend Functions + +[source,kotlin] +---- +import kotlinx.coroutines.* + +class CoroutineRcon(private val client: RconClient) { + suspend fun sendCommand(command: String): String = + withContext(Dispatchers.IO) { + client.sendCommand(command).response + } + + suspend fun sendCommands(commands: List): List = + coroutineScope { + commands.map { async { sendCommand(it) } } + .awaitAll() + } +} +---- + +Usage: + +[source,kotlin] +---- +val rcon = CoroutineRcon(client) + +runBlocking { + val response = rcon.sendCommand("list") + println(response) + + val responses = rcon.sendCommands(listOf("list", "seed", "difficulty")) + responses.forEach { println(it) } +} +---- + +=== Error Handling + +==== Retry Pattern + +[source,java] +---- +public CompletableFuture sendCommandWithRetry( + String command, + int maxRetries +) { + return sendCommandAsync(command) + .exceptionallyCompose(throwable -> { + if (maxRetries > 0) { + // Wait before retry + return CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS) + .execute(() -> sendCommandWithRetry(command, maxRetries - 1)) + .thenCompose(future -> future); + } else { + return CompletableFuture.failedFuture(throwable); + } + }); +} +---- + +==== Circuit Breaker + +[source,java] +---- +public class CircuitBreakerRcon { + private final RconClient client; + private final int threshold; + private final long timeoutMillis; + + private int failureCount = 0; + private long lastFailureTime = 0; + private boolean circuitOpen = false; + + public CompletableFuture sendCommand(String command) { + if (circuitOpen) { + long timeSinceLastFailure = + System.currentTimeMillis() - lastFailureTime; + if (timeSinceLastFailure > timeoutMillis) { + circuitOpen = false; + failureCount = 0; + } else { + return CompletableFuture.failedFuture( + new RconConnectionException("Circuit breaker is open") + ); + } + } + + return client.sendCommandAsync(command) + .whenComplete((response, throwable) -> { + if (throwable != null) { + failureCount++; + lastFailureTime = System.currentTimeMillis(); + if (failureCount >= threshold) { + circuitOpen = true; + } + } else { + failureCount = 0; + } + }); + } +} +---- + +=== Timeout Handling + +==== Per-Command Timeout + +[source,java] +---- +public CompletableFuture sendCommandWithTimeout( + String command, + Duration timeout +) { + CompletableFuture future = client.sendCommandAsync(command); + CompletableFuture timeoutFuture = CompletableFuture.failedFuture( + new TimeoutException("Command timed out") + ); + + return future.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS) + .thenApply(response -> { + if (response == null) { + throw new CompletionException(new TimeoutException()); + } + return response; + }); +} +---- + +=== Metrics and Monitoring + +==== Timing Commands + +[source,java] +---- +public class MetricsRconClient { + private final RconClient delegate; + private final MeterRegistry meterRegistry; + + public CompletableFuture sendCommand(String command) { + Timer.Sample sample = Timer.start(meterRegistry); + + return delegate.sendCommandAsync(command) + .whenComplete((response, throwable) -> { + sample.stop(meterRegistry.timer("rcon.command.latency")); + + if (throwable != null) { + meterRegistry.counter("rcon.command.errors").increment(); + } else { + meterRegistry.counter("rcon.command.success").increment(); + } + }); + } +} +---- + +=== See Also + +* link:../common-configurations/index.html[Common Configurations] - Connection pooling +* link:../advanced/fragment-resolution/index.html[Fragment Resolution] - Multi-packet handling +* link:../advanced/performance-tuning/index.html[Performance Tuning] - Optimization diff --git a/docs/guides/advanced/custom-exceptions/index.adoc b/docs/guides/advanced/custom-exceptions/index.adoc new file mode 100644 index 0000000..733f42e --- /dev/null +++ b/docs/guides/advanced/custom-exceptions/index.adoc @@ -0,0 +1,452 @@ +--- +title: Custom Exception Handling +parent: Advanced Topics +nav_order: 4 +--- + +== Custom Exception Handling + +=== Purpose + +Extending Rcon's exception hierarchy for application-specific error handling. + +=== Overview + +Rcon provides a custom exception hierarchy for clear error handling: + +[source] +---- +java.lang.Exception + └── RconException + ├── RconAuthenticationException + ├── RconConnectionException + └── RconProtocolException +---- + +=== Exception Types + +==== RconException + +Base exception for all RCON errors. + +[source,java] +---- +public class RconException extends Exception { + public RconException(String message) { + super(message); + } + + public RconException(String message, Throwable cause) { + super(message, cause); + } +} +---- + +Usage: + +[source,java] +---- +try { + RconResponse response = client.sendCommand("list"); +} catch (RconException e) { + // Handle any RCON error + System.err.println("RCON error: " + e.getMessage()); +} +---- + +==== RconAuthenticationException + +Thrown when authentication fails. + +[source,java] +---- +try { + RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("wrong") // Wrong password + .build(); +} catch (RconAuthenticationException e) { + // Handle authentication failure + System.err.println("Authentication failed: " + e.getMessage()); +} +---- + +==== RconConnectionException + +Thrown when connection fails or is lost. + +[source,java] +---- +try { + RconClient client = RconClient.builder() + .host("unreachable-host") + .port(25575) + .password("password") + .build(); +} catch (RconConnectionException e) { + // Handle connection failure + System.err.println("Connection failed: " + e.getMessage()); +} +---- + +==== RconProtocolException + +Thrown when protocol violation occurs. + +[source,java] +---- +try { + RconResponse response = client.sendCommand("list"); +} catch (RconProtocolException e) { + // Handle protocol error + System.err.println("Protocol error: " + e.getMessage()); +} +---- + +### Custom Exception Classes + +Create domain-specific exceptions: + +[source,java] +---- +public class RconCommandTimeoutException extends RconException { + private final String command; + private final Duration timeout; + + public RconCommandTimeoutException( + String command, + Duration timeout, + Throwable cause + ) { + super( + String.format( + "Command '%s' timed out after %d ms", + command, + timeout.toMillis() + ), + cause + ); + this.command = command; + this.timeout = timeout; + } + + public String getCommand() { + return command; + } + + public Duration getTimeout() { + return timeout; + } +} +---- + +Usage: + +[source,java] +---- +public RconResponse sendCommandWithTimeout( + String command, + Duration timeout +) throws RconCommandTimeoutException { + CompletableFuture future = + client.sendCommandAsync(command); + + try { + return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + throw new RconCommandTimeoutException(command, timeout, e); + } catch (Exception e) { + throw new RconException("Command failed", e); + } +} +---- + +=== Exception Wrapping + +Wrap Rcon exceptions in application-specific exceptions: + +[source,java] +---- +public class ApplicationRconException extends RuntimeException { + private final String operation; + + public ApplicationRconException( + String operation, + RconException cause + ) { + super( + String.format( + "Failed to execute RCON operation '%s': %s", + operation, + cause.getMessage() + ), + cause + ); + this.operation = operation; + } + + public String getOperation() { + return operation; + } + + public RconException getRconCause() { + return (RconException) getCause(); + } +} +---- + +Usage: + +[source,java] +---- +public String getPlayerList() { + try { + return client.sendCommand("list").getResponse(); + } catch (RconException e) { + throw new ApplicationRconException("getPlayerList", e); + } +} +---- + +### Retry Strategies + +==== Exponential Backoff + +[source,java] +---- +public class RetryableRconClient { + private final RconClient client; + private final int maxRetries; + + public RconResponse sendCommandWithRetry(String command) + throws RconException { + RconException lastException = null; + + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + return client.sendCommand(command); + } catch (RconConnectionException e) { + lastException = e; + + if (attempt < maxRetries) { + long delay = (long) Math.pow(2, attempt) * 100; + try { + Thread.sleep(delay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RconException("Interrupted during retry", ie); + } + } + } + } + + throw lastException; + } +} +---- + +Usage: + +[source,java] +---- +RetryableRconClient retryable = new RetryableRconClient(client, 3); + +try { + RconResponse response = retryable.sendCommandWithRetry("list"); +} catch (RconException e) { + // All retries exhausted + logger.error("Failed after 3 retries", e); +} +---- + +### Circuit Breaker + +[source,java] +---- +public class CircuitBreakerRconClient { + private final RconClient client; + private final int threshold; + private final long timeoutMillis; + + private int failureCount = 0; + private long lastFailureTime = 0; + private boolean circuitOpen = false; + + public RconResponse sendCommand(String command) throws RconException { + if (circuitOpen) { + long timeSinceLastFailure = + System.currentTimeMillis() - lastFailureTime; + + if (timeSinceLastFailure > timeoutMillis) { + // Attempt to close circuit + circuitOpen = false; + failureCount = 0; + } else { + throw new RconConnectionException( + "Circuit breaker is open" + ); + } + } + + try { + RconResponse response = client.sendCommand(command); + failureCount = 0; + return response; + } catch (RconException e) { + failureCount++; + lastFailureTime = System.currentTimeMillis(); + + if (failureCount >= threshold) { + circuitOpen = true; + logger.warn( + "Circuit breaker opened after {} failures", + failureCount + ); + } + + throw e; + } + } +} +---- + +### Fallback Strategies + +==== Default Response + +[source,java] +---- +public class FallbackRconClient { + private final RconClient primary; + private final RconClient fallback; + + public RconResponse sendCommand(String command) throws RconException { + try { + return primary.sendCommand(command); + } catch (RconConnectionException e) { + logger.warn("Primary client failed, using fallback", e); + return fallback.sendCommand(command); + } + } +} +---- + +==== Cached Response + +[source,java] +---- +public class CachedRconClient { + private final RconClient client; + private final Map cache = + new ConcurrentHashMap<>(); + private final Duration cacheTtl; + + public RconResponse sendCommand(String command) throws RconException { + CachedResponse cached = cache.get(command); + + if (cached != null && !cached.isExpired()) { + logger.debug("Cache hit for command: {}", command); + return cached.getResponse(); + } + + RconResponse response = client.sendCommand(command); + cache.put(command, new CachedResponse(response, cacheTtl)); + return response; + } + + private static class CachedResponse { + private final RconResponse response; + private final long expiryTime; + + CachedResponse(RconResponse response, Duration ttl) { + this.response = response; + this.expiryTime = System.currentTimeMillis() + ttl.toMillis(); + } + + boolean isExpired() { + return System.currentTimeMillis() > expiryTime; + } + + RconResponse getResponse() { + return response; + } + } +} +---- + +### Exception Translation + +Translate Rcon exceptions to application errors: + +[source,java] +---- +public class ExceptionTranslator { + public static ApplicationError translate(RconException e) { + if (e instanceof RconAuthenticationException) { + return new ApplicationError( + ErrorCode.AUTHENTICATION_FAILED, + "Invalid RCON password", + e + ); + } else if (e instanceof RconConnectionException) { + return new ApplicationError( + ErrorCode.CONNECTION_FAILED, + "Cannot reach RCON server", + e + ); + } else if (e instanceof RconProtocolException) { + return new ApplicationError( + ErrorCode.PROTOCOL_ERROR, + "RCON protocol violation", + e + ); + } else { + return new ApplicationError( + ErrorCode.UNKNOWN_ERROR, + "Unknown RCON error", + e + ); + } + } +} +---- + +### Logging Best Practices + +==== Structured Logging + +[source,java] +---- +try { + RconResponse response = client.sendCommand(command); +} catch (RconAuthenticationException e) { + logger.error( + "Authentication failed for host={}, port={}", + host, + port, + e + ); +} catch (RconConnectionException e) { + logger.error( + "Connection failed for host={}, port={}", + host, + port, + e + ); +} catch (RconException e) { + logger.error( + "RCON error for command='{}', host={}, port={}", + command, + host, + port, + e + ); +} +---- + +### See Also + +* link:../common-configurations/index.html[Common Configurations] - Retry logic examples +* link:../advanced/async-patterns/index.html[Async Patterns] - Async error handling +* link:../../reference/index.html[API Reference] - Exception documentation diff --git a/docs/guides/advanced/fragment-resolution/index.adoc b/docs/guides/advanced/fragment-resolution/index.adoc new file mode 100644 index 0000000..6c160e8 --- /dev/null +++ b/docs/guides/advanced/fragment-resolution/index.adoc @@ -0,0 +1,261 @@ +--- +title: Fragment Resolution Deep Dive +parent: Advanced Topics +nav_order: 1 +--- + +== Fragment Resolution Deep Dive + +=== Purpose + +Advanced understanding of multi-packet response handling in Rcon. + +=== Overview + +This page provides deeper technical details on fragment resolution beyond the basic link:../../core/fragment-resolution/index.html[Fragment Resolution] documentation. + +=== Protocol Limitation + +The RCON protocol has a fundamental limitation: server responses are limited to 4096 bytes per packet. When a command produces more output, the server splits the response across multiple packets. + +**Critical**: The protocol provides **no explicit marker** for the last packet. The client must use heuristics to detect completion. + +=== Detection Challenge + +Consider this scenario: + +[source] +---- +Client sends: "list" with ID=1 +Server responds with 22 packets, all with ID=1: + Packet 1: "There are 512 out of max 20 players..." + Packet 2: "Entity 1: Zombie at 123.5, 64.0, 2..." + Packet 3: "Entity 2: Skeleton at 128.1, 64.0, -5..." + ... + Packet 22: "Entity 512: Enderman at 456.7, 72.0, 123" + +Question: After receiving Packet 22, how does the client know + no more packets are coming? +---- + +=== Strategy Comparison + +==== ACTIVE_PROBE: Timeline + +[source] +---- +T0: Client sends "list" (ID=1) +T1: Client receives Packet 1 (ID=1) +T2: Client receives Packet 2 (ID=1) +T3: Client receives Packet 3 (ID=1) +... +T22: Client receives Packet 22 (ID=1) +T23: No immediate packet, client suspects end +T24: Client sends "" probe (ID=2) +T25: Client receives probe response (ID=2) +T26: Client confirms response complete, assembles 22 packets + +Total time: ~26 round-trips +Added latency: 1 round-trip (probe) +---- + +==== TIMEOUT: Timeline + +[source] +---- +T0: Client sends "list" (ID=1) +T1: Client receives Packet 1 (ID=1) +T2: Client receives Packet 2 (ID=1) +T3: Client receives Packet 3 (ID=1) +... +T22: Client receives Packet 22 (ID=1) +T23: No immediate packet, client suspects end +T24-T123: Client waits 100ms timeout +T124: Timeout expires, client confirms complete + +Total time: ~22 round-trips + 100ms wait +Added latency: 100ms (always) +---- + +=== Latency Analysis + +Assuming 1ms round-trip: + +[width="100%",cols="^2,^2,^2,6"] +|=== +|Response Size|Packets|ACTIVE_PROBE|TIMEOUT (100ms) + +|Small (1KB) +|1 +|2ms (1 + probe) +|101ms (1 + timeout) + +|Medium (8KB) +|2 +|3ms (2 + probe) +|102ms (2 + timeout) + +|Large (87KB) +|22 +|23ms (22 + probe) +|122ms (22 + timeout) + +|=== + +ACTIVE_PROBE is **4-5x faster** for small responses and **still faster** for large responses. + +=== Network Conditions + +==== High Latency Network + +Round-trip = 100ms: + +[width="100%",cols="^2,^2,6"] +|=== +|Strategy|Small Response|Large Response + +|ACTIVE_PROBE +|200ms (100 + 100) +|2,300ms (2,200 + 100) + +|TIMEOUT +|200ms (100 + 100) +|2,300ms (2,200 + 100) + +|=== + +TIMEOUT becomes competitive on high-latency networks because the timeout overhead is negligible compared to network latency. + +==== Low Latency Network + +Round-trip = 0.1ms: + +[width="100%",cols="^2,^2,6"] +|=== +|Strategy|Small Response|Large Response + +|ACTIVE_PROBE +|0.2ms (0.1 + 0.1) +|2.3ms (2.2 + 0.1) + +|TIMEOUT +|100.1ms (0.1 + 100) +|102.2ms (2.2 + 100) + +|=== + +ACTIVE_PROBE is **500x faster** for small responses on low-latency networks. + +=== Implementation Details + +==== Probe Command + +The probe is an empty command: + +[source,java] +---- +Packet probe = new Packet( + requestCounter.getAndIncrement(), + PacketType.COMMAND, + new byte[0] // Empty payload +); +---- + +Most servers respond to empty commands with an empty response. This is the key insight that makes ACTIVE_PROBE work. + +==== Race Condition Prevention + +[source,java] +---- +// WRONG - Race condition! +if (channel.read(buffer) == 0) { + // No data available, send probe + sendProbe(); + // But data might arrive NOW and be lost! +} + +// CORRECT - Synchronized +synchronized (this) { + if (noDataAvailable()) { + sendProbe(); + readProbeResponse(); + } +} +---- + +The synchronized block prevents concurrent access during the probe. + +=== Custom Timeout Values + +For TIMEOUT strategy, tune based on your network: + +[source,java] +---- +// Fast local network (1ms RTT) +RconClient client = RconClient.builder() + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .timeout(Duration.ofMillis(50)) // 50x RTT + .build(); + +// Internet (100ms RTT) +RconClient client = RconClient.builder() + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .timeout(Duration.ofMillis(500)) // 5x RTT + .build(); + +// Unreliable network +RconClient client = RconClient.builder() + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .timeout(Duration.ofMillis(1000)) // 10x RTT + .build(); +---- + +=== Testing Multi-Packet + +Test with various response sizes: + +[source,java] +---- +// Small response (single packet) +@Test +public void testSmallResponse() { + RconResponse response = client.sendCommand("seed"); + assertTrue(response.getResponse().length() < 4096); +} + +// Medium response (2-3 packets) +@Test +public void testMediumResponse() { + RconResponse response = client.sendCommand("help"); + assertTrue(response.getResponse().length() > 4096); +} + +// Large response (22+ packets) +@Test +public void testLargeResponse() { + RconResponse response = client.sendCommand("list"); + assertTrue(response.getResponse().length() > 80000); +} +---- + +=== Performance Profiling + +Profile fragment resolution performance: + +[source,java] +---- +long start = System.nanoTime(); +RconResponse response = client.sendCommand("list"); +long elapsed = System.nanoTime() - start; + +System.out.printf( + "Received %d bytes in %.2f ms%n", + response.getResponse().length(), + elapsed / 1_000_000.0 +); +---- + +=== See Also + +* link:../../core/fragment-resolution/index.html[Fragment Resolution] - User documentation +* link:../performance-tuning/index.html[Performance Tuning] - Optimization guide diff --git a/docs/guides/advanced/index.adoc b/docs/guides/advanced/index.adoc new file mode 100644 index 0000000..04b3ad9 --- /dev/null +++ b/docs/guides/advanced/index.adoc @@ -0,0 +1,204 @@ +--- +title: Advanced Topics +has_children: true +nav_order: 3 +--- + +== Advanced Topics + +=== Purpose + +Explore advanced usage patterns and customization options for power users. + +=== Topics + +* link:fragment-resolution/index.html[Fragment Resolution Deep Dive] - Understanding multi-packet handling +* link:async-patterns/index.html[Async Programming Patterns] - Non-blocking operations +* link:performance-tuning/index.html[Performance Tuning] - Optimization strategies +* link:custom-exceptions/index.html[Custom Exception Handling] - Extending error handling + +=== Thread Pooling + +For high-throughput scenarios, combine Rcon with thread pools: + +[source,java] +---- +public class HighThroughputRcon { + private final RconClient client; + private final ExecutorService executor; + + public HighThroughputRcon(RconClient client, int threadCount) { + this.client = client; + this.executor = Executors.newFixedThreadPool(threadCount); + } + + public CompletableFuture executeAsync(String command) { + return CompletableFuture.supplyAsync(() -> { + try { + return client.sendCommand(command); + } catch (RconException e) { + throw new CompletionException(e); + } + }, executor); + } + + public void shutdown() { + executor.shutdown(); + try { + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + client.close(); + } +} +---- + +=== Reactive Integration + +Integrate with reactive frameworks like Project Reactor: + +[source,java] +---- +import reactor.core.publisher.Mono; +import io.cavarest.rcon.RconClient; +import io.cavarest.rcon.RconResponse; + +public class ReactiveRcon { + private final RconClient client; + + public ReactiveRcon(RconClient client) { + this.client = client; + } + + public Mono sendCommand(String command) { + return Mono.fromCallable(() -> { + RconResponse response = client.sendCommand(command); + return response.getResponse(); + }); + } + + // Example: Chain multiple commands + public Mono chainCommands(String... commands) { + return Flux.fromArray(commands) + .flatMap(this::sendCommand) + .collectList() + .map(responses -> String.join("\n", responses)); + } +} +---- + +=== Custom Packet Handler + +Extend Rcon with custom packet processing: + +[source,java] +---- +import io.cavarest.rcon.Packet; +import io.cavarest.rcon.PacketType; + +public class CustomPacketHandler { + public void handlePacket(Packet packet) { + if (packet.getType() == PacketType.RESPONSE) { + String payload = new String( + packet.getPayload(), + StandardCharsets.UTF_8 + ); + System.out.println("Received: " + payload); + + // Custom processing logic + if (payload.contains("Warning:")) { + // Handle warning messages specially + } + } + } +} +---- + +=== Monitoring and Metrics + +Add metrics collection to monitor Rcon operations: + +[source,java] +---- +import java.time.Duration; +import java.time.Instant; + +public class MetricsRconClient implements RconClient { + private final RconClient delegate; + private final MetricRegistry metrics; + + public MetricsRconClient(RconClient delegate, MetricRegistry metrics) { + this.delegate = delegate; + this.metrics = metrics; + } + + @Override + public RconResponse sendCommand(String command) { + Instant start = Instant.now(); + try { + RconResponse response = delegate.sendCommand(command); + metrics.counter("rcon.success").increment(); + return response; + } catch (RconException e) { + metrics.counter("rcon.failure").increment(); + throw e; + } finally { + Duration duration = Duration.between(start, Instant.now()); + metrics.timer("rcon.latency").record(duration.toMillis(), TimeUnit.MILLISECONDS); + } + } + + // Delegate other methods... +} +---- + +=== Circuit Breaker Pattern + +Implement circuit breaker for resilience: + +[source,java] +---- +public class CircuitBreakerRconClient { + private final RconClient client; + private final int threshold; + private final long timeoutMillis; + + private int failureCount = 0; + private long lastFailureTime = 0; + private boolean circuitOpen = false; + + public RconResponse sendCommand(String command) { + if (circuitOpen) { + long timeSinceLastFailure = System.currentTimeMillis() - lastFailureTime; + if (timeSinceLastFailure > timeoutMillis) { + circuitOpen = false; + failureCount = 0; + } else { + throw new RconConnectionException("Circuit breaker is open"); + } + } + + try { + RconResponse response = client.sendCommand(command); + failureCount = 0; + return response; + } catch (RconException e) { + failureCount++; + lastFailureTime = System.currentTimeMillis(); + if (failureCount >= threshold) { + circuitOpen = true; + } + throw e; + } + } +} +---- + +=== Next Steps + +* link:../../core/index.html[Core Concepts] - Internal architecture details +* link:../../reference/index.html[API Reference] - Complete API docs diff --git a/docs/guides/advanced/performance-tuning/index.adoc b/docs/guides/advanced/performance-tuning/index.adoc new file mode 100644 index 0000000..977c0ce --- /dev/null +++ b/docs/guides/advanced/performance-tuning/index.adoc @@ -0,0 +1,392 @@ +--- +title: Performance Tuning +parent: Advanced Topics +nav_order: 3 +--- + +== Performance Tuning + +=== Purpose + +Optimization strategies for high-throughput and low-latency Rcon usage. + +=== Overview + +Rcon is designed for simplicity and correctness, but there are several strategies for optimizing performance in demanding scenarios. + +=== Baseline Performance + +Default configuration provides: + +* **Latency**: 1-2 round-trips per command (ACTIVE_PROBE strategy) +* **Throughput**: ~100-1000 commands/second (depending on network) +* **Memory**: ~10KB per client instance +* **CPU**: Minimal, mostly idle waiting for I/O + +=== Bottleneck Analysis + +==== Typical Command Flow + +[source] +---- +Client App → RconClient → Rcon → Socket → Network → Server + ↓ ↓ ↓ ↓ ↓ ↓ + [CPU] [CPU] [CPU] [I/O] [I/O] [I/O] + ↓ ↓ ↓ + [Logger] [Sync] [Buffer] +---- + +**Bottlenecks** (in order of impact): +1. Network latency (unavoidable) +2. Synchronized methods (serializes commands) +3. Logging (especially DEBUG level) +4. Buffer allocation (mitigated by double-buffering) + +=== Optimization Strategies + +==== Strategy 1: Connection Pooling + +**Problem**: Single client serializes all commands via synchronized methods. + +**Solution**: Use multiple clients to parallelize network I/O. + +[source,java] +---- +public class RconConnectionPool { + private final Queue pool = new LinkedList<>(); + private final int maxSize; + + public RconConnectionPool(String host, int port, String password, int size) { + this.maxSize = size; + for (int i = 0; i < size; i++) { + pool.add(RconClient.builder() + .host(host) + .port(port) + .password(password) + .build()); + } + } + + public RconClient acquire() { + synchronized (pool) { + while (pool.isEmpty()) { + try { + pool.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + return pool.remove(); + } + } + + public void release(RconClient client) { + synchronized (pool) { + if (pool.size() < maxSize) { + pool.add(client); + } + pool.notifyAll(); + } + } +} +---- + +Usage: + +[source,java] +---- +RconConnectionPool pool = new RconConnectionPool("localhost", 25575, "password", 5); + +ExecutorService executor = Executors.newFixedThreadPool(10); + +for (int i = 0; i < 100; i++) { + final int cmdNum = i; + executor.submit(() -> { + RconClient client = pool.acquire(); + try { + RconResponse response = client.sendCommand("say Command " + cmdNum); + } finally { + pool.release(client); + } + }); +} +---- + +**Throughput improvement**: 5-10x (depending on pool size) + +==== Strategy 2: Async API + +**Problem**: Synchronous API blocks threads during network I/O. + +**Solution**: Use `sendCommandAsync()` with non-blocking futures. + +[source,java] +---- +List> futures = commands.stream() + .map(client::sendCommandAsync) + .toList(); + +CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .join(); +---- + +**Thread usage improvement**: 10-100x (depends on command count) + +==== Strategy 3: Reduce Logging + +**Problem**: DEBUG logging adds overhead on every command. + +**Solution**: Use INFO or WARN level in production. + +[source,xml] +---- + + + + +---- + +Or disable logging entirely: + +[source,xml] +---- + + + +---- + +**Latency improvement**: 5-10% + +==== Strategy 4: Optimize Fragment Resolution + +**Problem**: TIMEOUT strategy adds 100ms latency to every command. + +**Solution**: Use ACTIVE_PROBE (default) for better latency. + +[source,java] +---- +// Explicitly use ACTIVE_PROBE (default) +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .fragmentStrategy(FragmentResolutionStrategy.ACTIVE_PROBE) + .build(); +---- + +**Latency improvement**: 100ms per command (for TIMEOUT users) + +==== Strategy 5: Tune Timeouts + +**Problem**: Default 10-second timeout is too long for fast-fail scenarios. + +**Solution**: Reduce timeout for faster failure detection. + +[source,java] +---- +// Fast-fail configuration +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .timeout(Duration.ofSeconds(2)) // 2 seconds + .build(); +---- + +**Failure detection improvement**: 8 seconds faster + +=== Memory Optimization + +==== Buffer Sizing + +Default buffers are sized for typical RCON usage: + +[source,java] +---- +// In PacketReader +private static final int RECEIVE_BUFFER = 4096; // 4KB + +// In PacketWriter +private static final int SEND_BUFFER = 1460; // Typical MTU +---- + +For large responses, increase receive buffer: + +[source,java] +---- +// Custom buffer sizes (requires PacketReader/PacketWriter modification) +private static final int RECEIVE_BUFFER = 65536; // 64KB +---- + +**Memory tradeoff**: 16x more memory for potentially fewer syscalls + +=== Network Optimization + +==== TCP_NODELAY + +Disable Nagle's algorithm for lower latency: + +[source,java] +---- +Socket socket = SocketChannel.open().socket(); +socket.setTcpNoDelay(true); // Disable Nagle's algorithm +---- + +**Latency improvement**: 10-40ms on high-latency networks + +==== Keep-Alive + +Enable TCP keep-alive for long-lived connections: + +[source,java] +---- +socket.setKeepAlive(true); +socket.setSoTimeout(60000); // 60 second timeout +---- + +=== Profiling + +==== JMX Monitoring + +Enable JMX for runtime monitoring: + +[source,bash] +---- +java -Dcom.sun.management.jmxremote \ + -Dcom.sun.management.jmxremote.port=9010 \ + -Dcom.sun.management.jmxremote.authenticate=false \ + -Dcom.sun.management.jmxremote.ssl=false \ + -jar rcon.jar +---- + +Connect with JConsole or VisualVM to monitor: +* Thread count +* Memory usage +* GC activity +* CPU usage + +==== Custom Metrics + +Add metrics collection: + +[source,java] +---- +public class MetricsRconClient implements RconClient { + private final RconClient delegate; + private final AtomicLong commandCount = new AtomicLong(0); + private final AtomicLong totalLatencyNanos = new AtomicLong(0); + + @Override + public RconResponse sendCommand(String command) { + long start = System.nanoTime(); + try { + RconResponse response = delegate.sendCommand(command); + commandCount.incrementAndGet(); + return response; + } finally { + totalLatencyNanos.addAndGet(System.nanoTime() - start); + } + } + + public double getAverageLatencyMillis() { + long count = commandCount.get(); + return count == 0 ? 0 : + (totalLatencyNanos.get() / count) / 1_000_000.0; + } +} +---- + +=== Performance Benchmarks + +==== Benchmark Setup + +[source,java] +---- +@State(Scope.Benchmark) +public class RconBenchmark { + private RconClient client; + + @Setup + public void setup() { + client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + } + + @Benchmark + public RconResponse sendCommand() { + return client.sendCommand("seed"); + } + + @TearDown + public void tearDown() { + client.close(); + } +} +---- + +Run with JMH: + +[source,bash] +---- +java -jar rcon.jar benchmarks/target/benchmarks.jar +---- + +==== Expected Results + +Typical performance on local network (1ms RTT): + +[width="100%",cols="^2,^2,^2,6"] +|=== +|Metric|Single Client|Pool of 5|Pool of 10 + +|Throughput +|~500 cmd/s +|~2,000 cmd/s +|~3,500 cmd/s + +|Avg Latency +|2ms +|2ms +|2ms + +|P95 Latency +|5ms +|10ms +|15ms + +|P99 Latency +|10ms +|25ms +|40ms + +|Memory +|10MB +|50MB +|100MB + +|=== + +=== Production Checklist + +Before deploying to production: + +* [ ] Use connection pooling for high-throughput scenarios +* [ ] Enable async API for non-blocking operations +* [ ] Set appropriate timeouts (2-5 seconds) +* [ ] Use ACTIVE_PROBE fragment strategy +* [ ] Set logging to INFO level +* [ ] Enable metrics collection +* [ ] Configure circuit breaker for fault tolerance +* [ ] Test with realistic load +* [ ] Monitor memory usage +* [ ] Profile before optimizing + +=== See Also + +* link:../common-configurations/index.html[Common Configurations] - Connection pool examples +* link:../advanced/async-patterns/index.html[Async Patterns] - Non-blocking operations +* link:../../core/thread-safety/index.html[Thread Safety] - Concurrent access diff --git a/docs/guides/basic-usage/index.adoc b/docs/guides/basic-usage/index.adoc new file mode 100644 index 0000000..0df7e8f --- /dev/null +++ b/docs/guides/basic-usage/index.adoc @@ -0,0 +1,158 @@ +--- +title: Basic Usage +parent: Guides +has_children: true +nav_order: 1 +--- + +== Basic Usage + +=== Purpose + +This section covers fundamental operations for working with Rcon in your applications. + +=== Core Concepts + +All Rcon operations follow a consistent pattern: + +1. **Create a client** - Use the builder pattern for configuration +2. **Send commands** - Use `sendCommand()` or `sendCommandAsync()` +3. **Handle responses** - Process the `RconResponse` object +4. **Clean up** - Close the client when done + +=== The Builder Pattern + +`RconClient` uses a fluent builder for flexible configuration: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("rcon_password") + .timeout(Duration.ofSeconds(5)) + .charset(StandardCharsets.UTF_8) + .build(); +---- + +Available configuration options: + +* `host(String)` - Server hostname (default: "localhost") +* `port(int)` - RCON port (default: 25575) +* `password(String)` - RCON password (required) +* `timeout(Duration)` - Connection timeout (default: 5 seconds) +* `charset(Charset)` - Character encoding (default: UTF_8) +* `fragmentStrategy(FragmentResolutionStrategy)` - Multi-packet strategy + +=== Synchronous Commands + +Use `sendCommand()` for blocking command execution: + +[source,java] +---- +RconResponse response = client.sendCommand("seed"); + +if (response.isSuccess()) { + System.out.println("Seed: " + response.getResponse()); +} else { + System.err.println("Command failed: " + response.getResponse()); +} +---- + +=== Asynchronous Commands + +Use `sendCommandAsync()` for non-blocking operations: + +[source,java] +---- +CompletableFuture future = client.sendCommandAsync("list"); + +future.thenAccept(response -> { + System.out.println("Players: " + response.getResponse()); +}).exceptionally(throwable -> { + System.err.println("Error: " + throwable.getMessage()); + return null; +}); +---- + +=== Error Handling + +Rcon provides a custom exception hierarchy for clear error reporting: + +[source,java] +---- +try { + RconResponse response = client.sendCommand("op Player"); +} catch (RconAuthenticationException e) { + // Invalid password or authentication failed + System.err.println("Authentication failed: " + e.getMessage()); +} catch (RconConnectionException e) { + // Connection refused or timeout + System.err.println("Connection error: " + e.getMessage()); +} catch (RconProtocolException e) { + // Protocol violation or malformed response + System.err.println("Protocol error: " + e.getMessage()); +} catch (RconException e) { + // General Rcon error + System.err.println("Rcon error: " + e.getMessage()); +} +---- + +=== Multiple Commands + +The client maintains an authenticated connection for multiple commands: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +try { + // Send multiple commands using the same connection + RconResponse r1 = client.sendCommand("list"); + RconResponse r2 = client.sendCommand("seed"); + RconResponse r3 = client.sendCommand("difficulty"); + + // Process responses... +} finally { + client.close(); +} +---- + +=== Resource Cleanup + +Always close the client when done to release resources: + +[source,java] +---- +// Try-with-resources (recommended) +try (RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build()) { + RconResponse response = client.sendCommand("list"); + System.out.println(response.getResponse()); +} + +// Or manually close +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); +try { + // Use client... +} finally { + client.close(); +} +---- + +=== Next Steps + +* link:../common-urations/index.html[Common Configurations] - Learn common setup patterns +* link:../advanced/async-patterns.html[Async Patterns] - Deep dive on async usage +* link:../../reference/javadoc.html[API Reference] - Full Javadoc diff --git a/docs/guides/common-configurations/index.adoc b/docs/guides/common-configurations/index.adoc new file mode 100644 index 0000000..7321337 --- /dev/null +++ b/docs/guides/common-configurations/index.adoc @@ -0,0 +1,241 @@ +--- +title: Common Configurations +parent: Guides +has_children: true +nav_order: 2 +--- + +== Common Configurations + +=== Purpose + +Learn common configuration patterns and setups for Rcon in various scenarios. + +=== Single Client Reuse + +Create a single client instance and reuse it for multiple commands: + +[source,java] +---- +public class RconManager { + private final RconClient client; + + public RconManager(String host, int port, String password) { + this.client = RconClient.builder() + .host(host) + .port(port) + .password(password) + .build(); + } + + public String sendCommand(String command) { + RconResponse response = client.sendCommand(command); + return response.getResponse(); + } + + public void close() { + client.close(); + } +} +---- + +=== Connection Pool + +For applications requiring multiple concurrent connections: + +[source,java] +---- +public class RconConnectionPool { + private final Queue pool = new LinkedList<>(); + private final int maxSize; + + public RconConnectionPool(String host, int port, String password, int maxSize) { + this.maxSize = maxSize; + for (int i = 0; i < maxSize; i++) { + pool.add(RconClient.builder() + .host(host) + .port(port) + .password(password) + .build()); + } + } + + public RconClient acquire() { + synchronized (pool) { + while (pool.isEmpty()) { + try { + pool.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for connection", e); + } + } + return pool.remove(); + } + } + + public void release(RconClient client) { + synchronized (pool) { + if (pool.size() < maxSize) { + pool.add(client); + } else { + client.close(); + } + pool.notifyAll(); + } + } +} +---- + +=== Retry Logic + +Implement automatic retry on connection failures: + +[source,java] +---- +public class RetryableRconClient { + private final RconClient client; + private final int maxRetries; + + public RetryableRconClient(RconClient client, int maxRetries) { + this.client = client; + this.maxRetries = maxRetries; + } + + public RconResponse sendCommandWithRetry(String command) { + RconException lastException = null; + for (int attempt = 0; attempt <= maxRetries; attempt++) { + try { + return client.sendCommand(command); + } catch (RconConnectionException e) { + lastException = e; + if (attempt < maxRetries) { + try { + Thread.sleep(1000 * (attempt + 1)); // Exponential backoff + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during retry", ie); + } + } + } + } + throw lastException; + } +} +---- + +=== Custom Timeout + +Set custom connection timeout for slow networks: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("remote-server.example.com") + .port(25575) + .password("password") + .timeout(Duration.ofSeconds(30)) // 30 second timeout + .build(); +---- + +=== Color Code Handling + +Use ISO-8859-1 charset to properly handle Minecraft color codes: + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .charset(StandardCharsets.ISO_8859_1) // For color codes + .build(); + +RconResponse response = client.sendCommand("list"); +String coloredOutput = response.getResponse(); +// Color codes like §c[Server] will be preserved correctly +---- + +=== Fragment Strategy Selection + +Choose the appropriate fragment resolution strategy: + +[source,java] +---- +// Use ACTIVE_PROBE for most cases (default) +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .fragmentStrategy(FragmentResolutionStrategy.ACTIVE_PROBE) + .build(); + +// Use TIMEOUT for servers that don't handle empty commands well +RconClient timeoutClient = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .build(); +---- + +=== Logging + +The `RconClient` automatically logs request/response pairs. Enable debug logging to see details: + +[source,xml] +---- + + + + +---- + +=== Async Batch Processing + +Process multiple commands concurrently using the async API: + +[source,java] +---- +public class BatchCommandProcessor { + private final RconClient client; + private final ExecutorService executor; + + public BatchCommandProcessor(RconClient client) { + this.client = client; + this.executor = Executors.newFixedThreadPool(5); + } + + public List processCommands(List commands) { + List> futures = commands.stream() + .map(cmd -> client.sendCommandAsync(cmd) + .thenApply(RconResponse::getResponse)) + .toList(); + + CompletableFuture allOf = CompletableFuture.allOf( + futures.toArray(new CompletableFuture[0]) + ); + + try { + allOf.get(30, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException("Batch processing failed", e); + } + + return futures.stream() + .map(CompletableFuture::join) + .toList(); + } + + public void shutdown() { + executor.shutdown(); + client.close(); + } +} +---- + +=== Next Steps + +* link:../advanced/index.html[Advanced Topics] - Learn advanced patterns +* link:../../core/fragment-resolution/index.html[Fragment Resolution] - Deep dive on strategies +* link:../../reference/index.html[API Reference] - Complete API documentation diff --git a/docs/guides/index.adoc b/docs/guides/index.adoc new file mode 100644 index 0000000..10e32c7 --- /dev/null +++ b/docs/guides/index.adoc @@ -0,0 +1,44 @@ +--- +title: Guides +has_children: true +nav_order: 2 +--- + +== Guides + +=== Purpose + +This section provides comprehensive guides for using Rcon effectively in various scenarios. + +=== Guide Categories + +==== Basic Usage + +The <> section covers fundamental operations: + +* Sending simple commands +* Handling responses +* Working with multiple commands +* Error handling best practices + +==== Common Patterns + +The <> section shows how to: + +* Create reusable Rcon clients +* Implement command batching +* Handle connection failures gracefully +* Use the async API + +==== Advanced Topics + +The <> section covers: + +* Custom fragment resolution strategies +* Charset configuration for color codes +* Connection pooling strategies +* Performance optimization + +=== Navigation + +Use the left sidebar to navigate through the guides, or continue below to explore each category. diff --git a/docs/index.adoc b/docs/index.adoc new file mode 100644 index 0000000..62c8e3b --- /dev/null +++ b/docs/index.adoc @@ -0,0 +1,50 @@ +--- +title: Home +nav_order: 0 +--- + +== Rcon: A Robust RCON Protocol Library for Java + +=== Purpose + +Rcon is a layered RCON (Remote Console) protocol library for Minecraft servers, written in pure Java with no runtime dependencies. It provides reliable remote command execution with automatic multi-packet response handling. + +=== Quick Start + +Get started with Rcon in minutes: + +* link:getting-started/installation[Add the dependency to your project] +* link:getting-started/quick-start[Send your first RCON command in 5 minutes] +* link:getting-started/architecture[Understand the layered architecture] + +=== Documentation Map + +==== New to Rcon? + +Follow this learning path: + +. Add Rcon to your project +** link:getting-started/installation[Installation Guide] +. Quick Start (5 minutes) +** link:getting-started/quick-start[Quick Start Guide] +. Understand the architecture +** link:getting-started/architecture[Architecture Overview] + +==== Need Specific Information? + +* link:guides[Guides] - Step-by-step tutorials for common tasks +* link:core[Core Concepts] - Deep dive into library internals +* link:reference[API Reference] - Javadoc and API documentation + +=== Key Features + +* **Zero Dependencies** - Pure Java standard library only +* **Thread-Safe** - Synchronized request/response matching +* **Automatic Multi-Packet Handling** - Responses of any size handled transparently +* **Configurable Fragment Resolution** - ACTIVE_PROBE (default) or TIMEOUT strategies +* **Flexible API** - Both synchronous and asynchronous command execution +* **Robust Error Handling** - Custom exception hierarchy for clear error reporting + +=== Version + +Current version: link:https://github.com/cavarest/rcon/releases[0.2.1] diff --git a/docs/reference/index.adoc b/docs/reference/index.adoc new file mode 100644 index 0000000..cb93c1e --- /dev/null +++ b/docs/reference/index.adoc @@ -0,0 +1,66 @@ +--- +title: API Reference +has_children: true +nav_order: 4 +--- + +== API Reference + +=== Purpose + +Complete API documentation for the Rcon library. + +=== Main API + +==== RconClient + +The high-level API for RCON operations: + +* `builder()` - Create a new `RconClient.Builder` +* `sendCommand(String)` - Send a command synchronously +* `sendCommandAsync(String)` - Send a command asynchronously +* `close()` - Close the connection and release resources + +==== RconClient.Builder + +Fluent builder for client configuration: + +* `host(String)` - Set server hostname +* `port(int)` - Set RCON port +* `password(String)` - Set RCON password +* `timeout(Duration)` - Set connection timeout +* `charset(Charset)` - Set character encoding +* `fragmentStrategy(FragmentResolutionStrategy)` - Set fragment resolution strategy +* `build()` - Create the configured client + +==== RconResponse + +Represents a response from the server: + +* `isSuccess()` - Check if the command succeeded +* `getResponse()` - Get the response payload +* `getRequestId()` - Get the matching request ID + +=== Exceptions + +Custom exception hierarchy for clear error handling: + +* `RconException` - Base exception for all Rcon errors +* `RconAuthenticationException` - Authentication failures +* `RconConnectionException` - Connection errors +* `RconProtocolException` - Protocol violations + +=== Fragment Resolution + +Strategies for handling multi-packet responses: + +* `FragmentResolutionStrategy.ACTIVE_PROBE` - Send probe to detect end (default) +* `FragmentResolutionStrategy.TIMEOUT` - Wait fixed duration after last packet + +=== Javadoc + +For complete API documentation, see the link:https://javadoc.io/doc/io.cavarest/rcon/[Javadoc]. + +=== Source Code + +Browse the source code on link:https://github.com/cavarest/rcon[GitHub]. diff --git a/docs/reference/javadoc.adoc b/docs/reference/javadoc.adoc new file mode 100644 index 0000000..ad780c9 --- /dev/null +++ b/docs/reference/javadoc.adoc @@ -0,0 +1,257 @@ +--- +title: Javadoc +parent: API Reference +nav_order: 1 +--- + +== Javadoc + +=== Purpose + +Complete Javadoc API reference for the Rcon library. + +=== Online Javadoc + +The latest Javadoc is available online: + +* link:https://javadoc.io/doc/io.cavarest/rcon/[javadoc.io - Latest Release] + +=== Building Javadoc Locally + +Generate Javadoc from source: + +[source,bash] +---- +./gradlew javadoc +---- + +The Javadoc will be generated in: + +[source,bash] +---- +build/docs/javadoc/index.html +---- + +Open in a browser: + +[source,bash] +---- +open build/docs/javadoc/index.html +---- + +=== Main Packages + +==== io.cavarest.rcon + +Top-level package containing the main API: + +* `RconClient` - High-level client API +* `RconClient.Builder` - Fluent builder pattern +* `RconResponse` - Response wrapper +* `RconException` - Base exception class +* `FragmentResolutionStrategy` - Fragment resolution strategies + +==== io.cavarest.rcon.core + +Core RCON implementation: + +* `Rcon` - Low-level RCON API +* `Rcon.Builder` - Configuration builder + +==== io.cavarest.rcon.protocol + +Protocol implementation: + +* `Packet` - Immutable packet model +* `PacketType` - Packet type enumeration +* `PacketCodec` - Encoding/decoding + +==== io.cavarest.rcon.io + +I/O layer: + +* `PacketReader` - Socket reader with buffering +* `PacketWriter` - Socket writer with buffering + +=== Key Classes + +==== RconClient + +[source,java] +---- +/** + * High-level RCON client with logging and connection management. + * + *

This is the main entry point for RCON operations. It wraps the + * low-level {@link Rcon} class with logging and simplified API.

+ * + *

Example usage:

+ *
{@code
+ * RconClient client = RconClient.builder()
+ *     .host("localhost")
+ *     .port(25575)
+ *     .password("password")
+ *     .build();
+ *
+ * RconResponse response = client.sendCommand("list");
+ * System.out.println(response.getResponse());
+ *
+ * client.close();
+ * }
+ * + * @see RconClient.Builder + */ +public class RconClient implements AutoCloseable { + // ... +} +---- + +==== Rcon + +[source,java] +---- +/** + * Low-level RCON protocol implementation. + * + *

This class provides direct access to RCON operations with full + * configuration options. Most users should use {@link RconClient} instead.

+ * + *

All methods are thread-safe and may be called from multiple threads + * concurrently.

+ * + * @see Rcon.Builder + */ +public class Rcon implements AutoCloseable { + /** + * Sends a command to the server and returns the response. + * + * @param command the command to send + * @return the server's response + * @throws RconException if the command fails + */ + public synchronized RconResponse sendCommand(String command) { + // ... + } +} +---- + +==== Packet + +[source,java] +---- +/** + * Immutable RCON packet. + * + *

Packets are thread-safe and can be freely shared between threads.

+ * + * @param id the request/response ID + * @param type the packet type + * @param payload the packet payload + */ +public class Packet { + /** + * Creates a new packet. + * + * @param id the request/response ID + * @param type the packet type + * @param payload the packet payload (will be copied) + */ + public Packet(int id, PacketType type, byte[] payload) { + // ... + } +} +---- + +=== Exception Hierarchy + +[source,java] +---- +java.lang.Exception + └── RconException + ├── RconAuthenticationException + ├── RconConnectionException + └── RconProtocolException +---- + +==== RconException + +Base exception for all RCON errors. + +[source,java] +---- +/** + * Base exception for all RCON-related errors. + * + *

Specific error types:

+ *
    + *
  • {@link RconAuthenticationException} - Authentication failures
  • + *
  • {@link RconConnectionException} - Connection errors
  • + *
  • {@link RconProtocolException} - Protocol violations
  • + *
+ */ +public class RconException extends Exception { + // ... +} +---- + +=== API Examples + +==== Synchronous Command + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +try { + RconResponse response = client.sendCommand("list"); + System.out.println(response.getResponse()); +} catch (RconException e) { + System.err.println("Command failed: " + e.getMessage()); +} finally { + client.close(); +} +---- + +==== Asynchronous Command + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .build(); + +CompletableFuture future = client.sendCommandAsync("seed"); + +future.thenAccept(response -> { + System.out.println("Response: " + response.getResponse()); +}).exceptionally(throwable -> { + System.err.println("Command failed: " + throwable.getMessage()); + return null; +}); +---- + +==== Custom Configuration + +[source,java] +---- +RconClient client = RconClient.builder() + .host("localhost") + .port(25575) + .password("password") + .timeout(Duration.ofSeconds(30)) + .charset(StandardCharsets.ISO_8859_1) + .fragmentStrategy(FragmentResolutionStrategy.TIMEOUT) + .build(); +---- + +=== See Also + +* link:../getting-started/index.html[Getting Started] - Setup and usage +* link:../guides/basic-usage/index.html[Basic Usage] - Common operations +* link:../guides/advanced/index.html[Advanced Topics] - Advanced patterns From 53cb7fdc39541fff74e8f8c13fc42cd20d758d28 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Fri, 16 Jan 2026 11:29:11 +0800 Subject: [PATCH 2/2] fix: add pr permissions and update lychee excludes --- .github/workflows/links.yml | 4 ++++ docs/.lychee.toml | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml index 8081419..ecabe93 100644 --- a/.github/workflows/links.yml +++ b/.github/workflows/links.yml @@ -5,6 +5,10 @@ on: branches: [main] pull_request: +permissions: + contents: read + pull-requests: write + jobs: link_checker: runs-on: ubuntu-latest diff --git a/docs/.lychee.toml b/docs/.lychee.toml index 93a965a..1913a0c 100644 --- a/docs/.lychee.toml +++ b/docs/.lychee.toml @@ -10,7 +10,10 @@ exclude = [ "http://localhost.*", "http://127\\.0\\.0\\.1.*", "mailto:.*", - "^#.*" + "^#.*", + "^https://cavarest\\.github\\.io/rcon", + "^https://github\\.com/cavarest/rcon", + "^https://javadoc\\.io/doc/io\\.cavarest/rcon" ] cache = true