Fork me on GitHub

Introduction

  1. Features
  2. Implementations
    1. Web API
    2. Server Queries
    3. Others
  3. Best Practices

Features

  • Simple and easy to use API.
  • All operations are asynchronous. Every request returns a CompletableFuture
  • It's fast and capable of handling large transactions.
  • Resource efficient
    • Uses netty's off-heap pooled direct buffers (Helps reduce GC pressure for high volume/throughput transactions)
    • Built-in thread and connection pooling support. Takes advantage of netty's event loop model.
    • Makes use of native transports (if available) for increased performance (e.g. epoll, kqueue). Java's NIO is used by default.
  • Highly Configurable. Clients can be configured to satisfy your requirements (e.g. providing a custom executor, adjusting rate limit parameters, selecting connection pool strategy etc)
  • Transactions are Failsafe (except web api). Resilience policies have been implemented to guarantee the delivery and receipt of queries. Below are the policies available by default.
    • Retry Policy: A failed query is re-attempted until a response has either been received or the maximum number attempts has been reached.
    • Rate Limiter Policy: This prevents overloading the servers by sending requests too fast causing the requests to timeout due to rate limits being exceeded.
    • Circuit Breaker Policy: When certain number of failures reach the threshold, the library will transition to an “OPEN” state, temporarily rejecting new requests.

Implementations

Web API

A list of supported web service implementations

Vendor Module Supported Interfaces
Supercell Clash of Clans (Deprecated) Clans, Leagues, Locations, Players
Valve Steam Apps, Community, Econ, Economy, Player Service, User, User Stats, Store Front, Game Servers, Store Service, Web API Util
Valve Dota 2 Econ, Fantasy, Match, Stats, Stream, Teams
Valve CS:GO Servers
Server Queries

A list of supported game server query protocols

Vendor Module Description
Valve Source Query Implementations of the A2S_INFO, A2S_PLAYERS, A2S_RULES and A2S_SERVERQUERY_GETCHALLENGE protocols
Valve Source RCON TCP/IP-based communication protocol used by Source Dedicated Server
Others

Other supported protocols

Vendor Module Name Description
Valve Source Log Listener A standalone service which listens to source server log events via UDP
Valve Master Server Query Protocol Legacy protocol to query valve master servers

Best Practices

Keep in mind that you should NEVER BLOCK the thread of your completion handlers. If you have to perform synchronization or a long running operation within your handlers/callbacks, then it is highly recommended that you execute them asynchronously, otherwise your application may hang indefinitely. Java's CompletableFuture provides a convenient way of executing your handlers asynchronously. Refer to the working examples below.

Not Recommended

In this example, a command query cvarlist is sent to the server. The response is then parsed and broken down into a list of ConVar objects by the completion handler parseOutput(). Notice that in parseOutput(), an additional request client.execute() is sent for each entry followed by a call to join(), which blocks the current thread until a response is received. Avoid blocking the event-loop thread as this could cause the application to hang indefinitely.

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

public class NotRecommededExample {

    private static final Pattern cvarlistParser = Pattern.compile("(?<name>\\S+)\\s*:\\s?(?<value>\\S+)?\\s*:(?<group>.*):(?<description>.*)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

    private static class ConVar {

        private final String name;

        private final String value;

        private final String[] types;

        private final String description;

        private ConVar(String name, String value, String[] types, String description) {
            this.name = name;
            this.value = value;
            this.types = types;
            this.description = description;
        }

        private String getName() {
            return name;
        }

        private String getValue() {
            return value;
        }

        private String[] getTypes() {
            return types;
        }

        private String getDescription() {
            return description;
        }
    }

    public static void main(String[] args) throws Exception {
        new RconTest().run();
    }

    private void run() throws Exception {
        InetSocketAddress address = new InetSocketAddress("192.168.1.10", 27016);
        try (SourceRconClient client = new SourceRconClient()) {
            //authenticate
            SourceRconAuthResponse authResponse = client.authenticate(address, "<password_here>".getBytes()).join();
            if (!authResponse.isAuthenticated())
                throw new Exception(authResponse.getReason());

            //Example
            //1. execute 'cvarlist'
            //2. parse 'cvarlist' output with regex
            //3. transform each convar into ConVar class and add to list
            //4. return list
            List<ConVar> cvarList = client.execute(address, "cvarlist") //get cvarlist from server
                                          .thenApply(out -> parseOutput(client, out)) //parse the cvarlist output and transform it to a List of ConVar objects
                                          .join(); //block until the operation is complete

            System.out.printf("Processed a total of %d cvars%n", cvarList.size());
        }
    }

    private List<ConVar> parseOutput(SourceRconClient client, SourceRconCmdResponse response) {
        String cvarlistOutput = response.getResult();
        Matcher matcher = cvarlistParser.matcher(cvarlistOutput);
        List<ConVar> conVarList = new ArrayList<>();

        while (matcher.find()) {
            String name = matcher.group("name");
            String value = matcher.group("value");
            String[] types = StringUtils.splitPreserveAllTokens(matcher.group("group"), ",");
            String description = matcher.group("description");
            ConVar conVar = new ConVar(name, value, types, description);
            conVarList.add(conVar);
            Console.colorize(true).green("[%s]", Thread.currentThread().getName()).white(": %s = %s", conVar.getName(), conVar.getValue()).println();

            //Obtain additional information about the cvar via calling 'help <cvar name>' (Calling join() will block the current event loop thread) 
            SourceRconCmdResponse helpResponse = client.execute(response.getAddress(), String.format("help %s", name.trim())).join();

            //parse cvarHelpRes and update ConVar
            String helpOutput = helpResponse.getResult();
            //parse helpOutput here....
        }
        return conVarList;
    }
}

Recommended

Solution 1: The easiest solution would be to swap thenApply with thenApplyAsync. This will ensure that parseOutput will be executed on a different thread and most importantly will not block the current event-loop thread. You also can specify a custom executor if needed.

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

public class RecommendedExampleOne {

    private static final Pattern cvarlistParser = Pattern.compile("(?<name>\\S+)\\s*:\\s?(?<value>\\S+)?\\s*:(?<group>.*):(?<description>.*)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

    private static class ConVar {

        private final String name;

        private final String value;

        private final String[] types;

        private final String description;

        private ConVar(String name, String value, String[] types, String description) {
            this.name = name;
            this.value = value;
            this.types = types;
            this.description = description;
        }

        private String getName() {
            return name;
        }

        private String getValue() {
            return value;
        }

        private String[] getTypes() {
            return types;
        }

        private String getDescription() {
            return description;
        }
    }

    public static void main(String[] args) throws Exception {
        new RconTest().run();
    }

    private void run() throws Exception {
        InetSocketAddress address = new InetSocketAddress("192.168.1.10", 27016);
        try (SourceRconClient client = new SourceRconClient()) {
            //authenticate
            SourceRconAuthResponse authResponse = client.authenticate(address, "<password_here>".getBytes()).join();
            if (!authResponse.isAuthenticated())
                throw new Exception(authResponse.getReason());

            //Example
            //1. execute 'cvarlist'
            //2. parse 'cvarlist' output with regex
            //3. transform each convar into ConVar class and add to list
            //4. return list
            List<ConVar> cvarList = client.execute(address, "cvarlist")
                                          .thenApplyAsync(out -> parseOutput(client, out)) //THIS IS A LONG RUNNING TASK. Call thenApplyAsync() so it wont block the event-loop.
                                          .join(); //block until the operation is complete

            System.out.printf("Processed a total of %d cvars%n", cvarList.size());
        }
    }

    private List<ConVar> parseOutput(SourceRconClient client, SourceRconCmdResponse response) {
        String cvarlistOutput = response.getResult();
        Matcher matcher = cvarlistParser.matcher(cvarlistOutput);
        List<ConVar> conVarList = new ArrayList<>();

        while (matcher.find()) {
            String name = matcher.group("name");
            String value = matcher.group("value");
            String[] types = StringUtils.splitPreserveAllTokens(matcher.group("group"), ",");
            String description = matcher.group("description");
            ConVar conVar = new ConVar(name, value, types, description);
            conVarList.add(conVar);
            Console.colorize(true).green("[%s]", Thread.currentThread().getName()).white(": %s = %s", conVar.getName(), conVar.getValue()).println();

            //Obtain additional information about the cvar via calling 'help <cvar name>' (Calling join() will block the current event loop thread) 
            SourceRconCmdResponse helpResponse = client.execute(response.getAddress(), String.format("help %s", name.trim())).join();

            //parse cvarHelpRes and update ConVar
            String helpOutput = helpResponse.getResult();
            //parse helpOutput here....
        }
        return conVarList;
    }
}

The problem with this approach is that it's not efficient. A closer look at the parseOutput implementation, shows that it is calling client.execute(...).join() synchronously and will block the current thread until the method completes. The call to join() blocks the current thread until a response is received. A slightly better approach would be to execute all these additional requests asynchronously then use a synchronization barrier (e.g. CountDownLatch or Phaser) to block until all requests have been fulfilled. In this example, we will use Phaser since we do not yet know how many cvars we are going to process.

Solution 2: Update parseOutput implementation so that the requests for help <cvar> are all executed asynchronously then wait until all requests have been fulfilled.

import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

public class RecommendedExampleTwo {

    private static final Pattern cvarlistParser = Pattern.compile("(?<name>\\S+)\\s*:\\s?(?<value>\\S+)?\\s*:(?<group>.*):(?<description>.*)$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);

    private static class ConVar {

        private final String name;

        private final String value;

        private final String[] types;

        private final String description;

        private ConVar(String name, String value, String[] types, String description) {
            this.name = name;
            this.value = value;
            this.types = types;
            this.description = description;
        }

        private String getName() {
            return name;
        }

        private String getValue() {
            return value;
        }

        private String[] getTypes() {
            return types;
        }

        private String getDescription() {
            return description;
        }
    }

    public static void main(String[] args) throws Exception {
        new RconTest().run();
    }

    private void run() throws Exception {
        InetSocketAddress address = new InetSocketAddress("192.168.50.6", 27016);
        try (SourceRconClient client = new SourceRconClient()) {
            //authenticate
            SourceRconAuthResponse authResponse = client.authenticate(address, "AzCgBF6E7ddY6wE5XFrfdv3Rsw9XcQoY".getBytes()).join();//<password_here>
            if (!authResponse.isAuthenticated())
                throw new Exception(authResponse.getReason());

            //Example
            //1. execute 'cvarlist'
            //2. parse 'cvarlist' output with regex
            //3. transform each convar into ConVar class and add to list
            //4. return list
            List<ConVar> cvarList = client.execute(address, "cvarlist").thenApplyAsync(out -> parseOutput(client, out)).join();
            System.out.printf("Processed a total of %d cvars%n", cvarList.size());
            System.out.println("Exiting application");
        }
    }

    //Here is an updated implementation using Phaser sync barrier
    private List<ConVar> parseOutput(SourceRconClient client, SourceRconCmdResponse response) {
        String cvarlistOutput = response.getResult();
        Matcher matcher = cvarlistParser.matcher(cvarlistOutput);
        List<ConVar> conVarList = new ArrayList<>();
        Phaser phaser = new Phaser();
        phaser.register();
        while (matcher.find()) {
            String name = matcher.group("name");
            String value = matcher.group("value");
            String[] types = StringUtils.splitPreserveAllTokens(matcher.group("group"), ",");
            String description = matcher.group("description");
            ConVar conVar = new ConVar(name, value, types, description);
            conVarList.add(conVar);
            phaser.register();
            //Obtain cvar information via 'help'
            client.execute(response.getAddress(), String.format("help %s", name.trim())).thenCombine(CompletableFuture.completedFuture(conVar), (res, cvar) -> {
                String helpOutput = res.getResult();
                //TODO: parse helpOutput and update cvar
                return cvar;
            }).whenComplete((conVar1, throwable) -> phaser.arriveAndDeregister());
        }
        phaser.arriveAndAwaitAdvance();
        return conVarList;
    }
}