Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,108 @@

**RedisBridge** is a complete rewrite of my previous plugin [JBridge](https://github.com/JossArchived/JBridge), developed for Nukkit and WaterdogPE. It provides automatic server registration, player management, and seamless communication between backend servers and the proxy.

# ⚙️ Available Commands
RedisBridge includes ready-to-use commands for your proxy (WaterdogPE, Velocity, BungeeCord) or backend servers:

- `/lobby`
Teleports the player to an available lobby instance using the `LOWEST_PLAYERS` strategy to avoid overloading a single lobby while keeping activity balanced.

- `/transfer <server>`
Transfers the player to a specific instance if available. Useful for networks with multiple game modes.

- `/whereami`
Displays information to the player showing which instance and group they are currently in, along with the number of players and instance capacity.

# 📡 Instance Management Usage
RedisBridge includes a **distributed instance discovery and selection system** for minigame servers, lobbies, or backend servers using Redis and a low-latency distributed cache.

Each server instance sends **automatic heartbeats** via `InstanceHeartbeatMessage` containing:

- `id` (unique identifier)

- `group` (e.g., `lobby`, `solo_skywars`, `duels`)

- `players` (current online players)

- `maxPlayers` (maximum capacity)

- `host` and `port`

This allows other servers and the proxy to know in real time which instances are available, their capacity, and their status.

The `InstanceManager`:

- Uses a local cache with a 10-second expiration to keep instance state updated efficiently.

- Allows you to:

- Retrieve instances by ID (`getInstanceById`)

- Retrieve all instances in a group (`getGroupInstances`)

- Get total player counts or per group (`getTotalPlayerCount`, `getGroupPlayerCount`)

- Get total maximum player capacity or per group (`getTotalMaxPlayers`, `getGroupMaxPlayers`)

Provides **automatic available instance selection** using different strategies:

- `RANDOM`: Selects a random instance in the group.

- `LOWEST_PLAYERS`: Selects the instance with the fewest players.

- `MOST_PLAYERS_AVAILABLE`: Selects the instance with the most players.

Example:

```java
InstanceInfo instance = InstanceManager.getInstance().selectAvailableInstance("lobby", InstanceManager.SelectionStrategy.LOWEST_PLAYERS);

if (instance != null) {
// Connect player to this instance
}
```

This system enables your network to distribute players dynamically without relying on a heavy centralized matchmaking server.

# 🚀 Communication Usage
RedisBridge simplifies inter-server communication over Redis, enabling you to publish and subscribe to messages seamlessly between your proxy and backend servers.

## How it works?
- Uses Redis Pub/Sub on a single channel (redis-bridge-channel) for all message transmission.

- Messages are serialized in JSON and identified using their type field.

- Each message type can have:

- A registered class (MessageRegistry) for deserialization.

- An optional handler (MessageHandlerRegistry) for automatic processing when received.

- Includes default messages for instance heartbeat and shutdown announcements to enable automatic instance tracking.

## Publishing Messages
To send a message to all connected instances:
```java
YourCustomMessage message = new YourCustomMessage();
// fill your message data here

redisBridge.publish(message, "sender-id");
```

## Handling Incoming Messages
- Register your message type:
```java
MessageRegistry.register("your-message-type", YourCustomMessage.class);
```
- Register your message handler:
```java
MessageHandlerRegistry.register("your-message-type", new MessageHandler<YourCustomMessage>() {
@Override
public void handle(YourCustomMessage message) {
// handle your message here
}
});
```

## License
**RedisBridge** is licensed under the [MIT License](./LICENSE). Feel free to use, modify, and distribute it in your projects.
12 changes: 12 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@
<version>33.4.8-jre</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.19.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
package net.josscoder.redisbridge.core;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.josscoder.redisbridge.core.data.InstanceInfo;
import net.josscoder.redisbridge.core.instance.InstanceInfo;
import net.josscoder.redisbridge.core.instance.InstanceManager;
import net.josscoder.redisbridge.core.message.MessageBase;
import net.josscoder.redisbridge.core.message.MessageHandler;
import net.josscoder.redisbridge.core.message.MessageHandlerRegistry;
import net.josscoder.redisbridge.core.message.MessageRegistry;
import net.josscoder.redisbridge.core.logger.ILogger;
import net.josscoder.redisbridge.core.manager.InstanceManager;
import net.josscoder.redisbridge.core.message.defaults.InstanceHeartbeatMessage;
import net.josscoder.redisbridge.core.message.defaults.InstanceShutdownMessage;
import net.josscoder.redisbridge.core.utils.JsonUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPubSub;

/**
* Part of this code is taken from:
* <a href="https://github.com/theminecoder/DynamicServers/blob/master/dynamicservers-common/src/main/java/me/theminecoder/dynamicservers/DynamicServersCore.java">DynamicServers</a>
*/
public class RedisBridgeCore {

private static final Gson GSON = new GsonBuilder().create();
public static final String INSTANCE_HEARTBEAT_CHANNEL = "instance_heartbeat_channel";
public static final String INSTANCE_REMOVE_CHANNEL = "instance_removed_channel";
public static final String CHANNEL = "redis-bridge-channel";

private JedisPool jedisPool = null;
private Thread listenerThread;
Expand All @@ -37,16 +44,30 @@ public void connect(String host, int port, String password, ILogger logger) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
if (channel.equals(INSTANCE_HEARTBEAT_CHANNEL)) {
InstanceInfo data = GSON.fromJson(message, InstanceInfo.class);
InstanceManager.INSTANCE_CACHE.put(data.getId(), data);
} else if (channel.equals(INSTANCE_REMOVE_CHANNEL)) {
InstanceInfo data = GSON.fromJson(message, InstanceInfo.class);
InstanceManager.INSTANCE_CACHE.invalidate(data.getId());
public void onMessage(String channel, String messageJson) {
try {
String type = JsonUtils.extractType(messageJson);

Class<? extends MessageBase> clazz = MessageRegistry.getClass(type);
if (clazz == null) {
logger.debug("Unregistered message type: " + type);

return;
}

MessageBase message = JsonUtils.fromJson(messageJson, clazz);

MessageHandler<MessageBase> handler = MessageHandlerRegistry.getHandler(type);
if (handler != null) {
handler.handle(message);
} else {
logger.debug("No handler found for message type: " + type);
}
} catch (Exception e) {
logger.error("Error handling message", e);
}
}
}, INSTANCE_REMOVE_CHANNEL, INSTANCE_HEARTBEAT_CHANNEL);
}, CHANNEL);
} catch (Exception e) {
logger.error("RedisBridge encountered an error, will retry in 1 second", e);
try {
Expand All @@ -62,18 +83,33 @@ public void onMessage(String channel, String message) {
listenerThread.start();
}

public void publish(String message, String channel) {
public void publish(MessageBase message, String sender) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(channel, message);
message.setTimestamp(System.currentTimeMillis());
message.setSender(sender);

String json = JsonUtils.toJson(message);
jedis.publish(CHANNEL, json);
}
}

public void publishInstanceInfo(InstanceInfo info) {
publish(GSON.toJson(info), INSTANCE_HEARTBEAT_CHANNEL);
}
public void registerDefaultMessages() {
MessageRegistry.register(InstanceHeartbeatMessage.TYPE, InstanceHeartbeatMessage.class);
MessageHandlerRegistry.register(InstanceHeartbeatMessage.TYPE, new MessageHandler<InstanceHeartbeatMessage>() {
@Override
public void handle(InstanceHeartbeatMessage message) {
InstanceInfo instance = message.getInstance();
InstanceManager.INSTANCE_CACHE.put(instance.getId(), instance);
}
});

public void publishInstanceRemove(InstanceInfo info) {
publish(GSON.toJson(info), INSTANCE_REMOVE_CHANNEL);
MessageRegistry.register(InstanceShutdownMessage.TYPE, InstanceShutdownMessage.class);
MessageHandlerRegistry.register(InstanceShutdownMessage.TYPE, new MessageHandler<InstanceShutdownMessage>() {
@Override
public void handle(InstanceShutdownMessage message) {
InstanceManager.INSTANCE_CACHE.invalidate(message.getSender());
}
});
}

public void close() {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.josscoder.redisbridge.core.instance;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class InstanceInfo {
private String id;
private String host;
private int port;
private String group;
private int maxPlayers;
private int players;

public boolean isFull() {
return players >= maxPlayers;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package net.josscoder.redisbridge.core.manager;
package net.josscoder.redisbridge.core.instance;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Getter;
import net.josscoder.redisbridge.core.data.InstanceInfo;

import java.util.*;
import java.util.concurrent.TimeUnit;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.josscoder.redisbridge.core.message;

import lombok.Data;

@Data
public abstract class MessageBase {

private final String type;
private String sender;
private Long timestamp;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.josscoder.redisbridge.core.message;

public abstract class MessageHandler <T extends MessageBase> {
public abstract void handle(T message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.josscoder.redisbridge.core.message;

import java.util.HashMap;
import java.util.Map;

public class MessageHandlerRegistry {

private static final Map<String, MessageHandler<? extends MessageBase>> handlers = new HashMap<>();

public static void register(String type, MessageHandler<? extends MessageBase> handler) {
handlers.put(type, handler);
}

@SuppressWarnings("unchecked")
public static <T extends MessageBase> MessageHandler<T> getHandler(String type) {
return (MessageHandler<T>) handlers.get(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.josscoder.redisbridge.core.message;

import java.util.HashMap;
import java.util.Map;

public class MessageRegistry {

private static final Map<String, Class<? extends MessageBase>> messages = new HashMap<>();

public static void register(String type, Class<? extends MessageBase> aClass) {
messages.put(type, aClass);
}

public static Class<? extends MessageBase> getClass(String type) {
return messages.get(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.josscoder.redisbridge.core.message.defaults;

import lombok.Getter;
import lombok.Setter;
import net.josscoder.redisbridge.core.instance.InstanceInfo;
import net.josscoder.redisbridge.core.message.MessageBase;

@Setter
@Getter
public class InstanceHeartbeatMessage extends MessageBase {

public static final String TYPE = "instance_heartbeat";

private InstanceInfo instance;

public InstanceHeartbeatMessage() {
super(TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.josscoder.redisbridge.core.message.defaults;

import net.josscoder.redisbridge.core.message.MessageBase;

public class InstanceShutdownMessage extends MessageBase {

public static final String TYPE = "instance_shutdown";

public InstanceShutdownMessage() {
super(TYPE);
}
}
Loading