Skip to content
Alisson Oliveira edited this page Feb 8, 2019 · 5 revisions

Asynchronous Massively Multiplayer Online Core

The Goal

The Async-mmocore is primary designed to Massive Multiplayer Online (MMO) Game Servers. The Goal of the Async-mmocore is to provide a easy way to handle MMO connections to a server abstracting the networking layer complexity.

The Requirements

The Async-mmocore is built on top of Java NIO.2 API using Asyncronous Socket Channels. It requires Java 8+ to build and run.

The ReadablePacket and WritablePacket Classes

These classes, herein referenced as packets, are the abstraction of data send through the network. All packets must have a Header and a optional payload.

The header is composed by a Short number the carries out the size the packet. The payload is the essential information to the server or client. The packet must be composed by at maximum 32767 bytes. Packets greater than this can be lead to unexpected behaviour.

The Basics to Use

  • Define a Client Implementation

The client Class is a representation of a external connection. Thus it's the unique source of incoming packets and the target of the outcome packets.

The Client Class must implement the abstract class Client

public class ClientImpl extends Client<Connection<ClientImpl>> {
    
    public ClientImpl(Connection<ClientImpl> connection) {
        super(connection);
    }
        
    @Override
    public boolean decrypt(byte[] data, int offset, int size) {
        return myCrypter.decrypt(data, offset, size);
    }
    
    @Override
    public int encrypt(byte[] data, int offset, int size) {
        return myCrypter.encrypt(data, offset, size);
    }
    
    @Override
    protected void onDisconnection() {
        saveDataAndReleaseResources();
    }
    
    @Override
    public void onConnected() {
        doTheInitialJob();    
    }
    
    public void sendPacket(WritablePacket<ClientImpl> packet) {
        writePacket(packet);
    }
}
  • Define a Client Factory Implementation

The Client Factory instantiate the new incoming connections.

The Client Factory must implement the interface ClientFactory

public class ClientFactoryImpl implements ClientFactory<ClientImpl> {
    
    @Override
    public ClientImpl create(Connection<ClientImpl> connection) {
        return new ClientImpl(connection);
    }    
}
  • Define a Packet Handler Implementation

The Packet Handler converts the incoming data into a ReadablePacket.

The Packet Handler must implement the interface PacketHandler

public class PacketHandlerImpl implements PacketHandler<ClientImpl> {
    
     @Override
    public ReadablePacket<ClientImpl> handlePacket(ByteBuffer buffer, ClientImpl client) {
        ReadablePacket<ClientImpl> packet = convertToPacket(buffer, client);
        return packet;
    }
}
  • Define a Packet Executor Implementation

The Packet Executor executes the incoming Packets.

Although the packet can be execute in the same Thread, it's highly recommended that the Executors executes the packet on a apart Thread. Thats because the Thread that calls the execute method is the same that process the network I/O operations. Thus these threads must be short-living and execute only no-blocking operations.

The Packet Executor must implement the interface PacketExecutor

public class PacketExecutorImpl implements PacketExecutor<ClientImpl> {
    
    @Override
    public void execute(ReadablePacket<AsyncClient> packet) { 
        threadPoolExecutor.execute(packet);
    }
}
  • Listen Connections

To listen Connections it's necessary to build a ConnectionHandler

public class ServerHandler {
    public void startListen(String host, int port) { 
        ConnectionHandler<ClientImpl> connectionHandler = ConnectionBuilder.create(new InetSocketAddress(host, port), new ClientFactoryImpl(), new PacketHandlerImpl(), new PacketExecutorImpl()).build();
        connectionHandler.start();
    }    
} 
  • Sending a Packet

To send a Packet it's necessary to implement the abstract class WritablePacket

public class ServerInfo implements WritablePacket<ClientImpl> {
    @Override
    protected void write(ClientImpl client, ByteBuffer buffer) {
        buffer.put(this.getServerId());
        writeString(this.getServerName(), buffer);
        buffer.putLong(this.getServerCurrentTime());
        buffer.putInt(this.getServerCurrentUsers());
        
    }
}

and just send it through the client

public class ServerHandler {
    public void sendServerInfoToClient(ClientImpl client) {
        client.sendPacket(new ServerInfo());
    }
}
  • Receiving a Packet

The receiving packet is almost all done by the Async-mmocore. The only part that needs to be implemented to fully read are the steps described in Define a Packet Handler Implementation and Define a Packet Executor Implementation sections.

public class ReceivedServerInfo implements ReadablePacket<ClientImpl> {
    
    @Override
    protected void read(ByteBuffer buffer) {
        this.serverId = buffer.get();
        this.serverName = readString(buffer);
        this.serverCurrentTime = buffer.getLong();
        this.serverCurrentUsers = buffer.getInt();
    }
    
    @Override
    public void run() {
        showServerInfoToClient();
    }
}

Client Side

The class Connector was designed to provides client side asynchronous connection support. It works just like ConnectionBuilder, so you must define the ClientFactory, the PacketHandler and the PacketExecutor implementations.

public class ConnectionFactory {

    public static ClientImpl create(String host, int port) {
        ClientImpl client = Connector.create(clientFactory, packetHandler, packetExecutor).connect(new InetSocketAddress(host, port));
        return client;
    }

}

Tuning Configurations

  • Using Connection Filter

On ConnectionBuilder has a method filter where can be defined a ConnectionFilter to decide if an address can be connected or not.

public class Filter implements ConnectionFilter {

    @Override
    public boolean accept(AsynchronousSocketChannel channel) {
        return acceptIfAddressIsNotBanned(channel.getRemoteAddress());
    }
}
  • Packets Size

There is a method size on WratablePacket where can set the quantity of bytes will be send on the packet. This can reduce the use of resources using only necessary space on memory. But beware that returning a smaller size than which will really send will cause BufferUnderFlowException errors.

Based on the size of packet a buffer will be picked from a group on the resource pool. There are four groups of buffers: small, medium, large and default. The size of which group can be defined on ConnectionBuilder through the methods: bufferSmallSize, bufferMediumSize, bufferLargeSize and bufferDefaultSize.

The ReadablePackets always will use the bufferDefaultSize, because there is no way to know the income packet size previously. So the bufferDefaultSize must be with the bigger ReadablePacket size.

  • BufferPool Size

Each buffer group has a associated pool used to avoid the massive creation and destruction of ByteBuffers that can lead to overhead caused by GC. On ConnectionBuilder can be configured the size of each buffer pool using the methods: bufferSmallPoolSize, bufferMediumPoolSize, bufferLargePoolSize and bufferPoolSize.

The best size for the each group depends on the quantity of simultaneous connections and the frequency of use of each size of buffer i.e. The more used pool must be bigger. However the pool size influences directly on the amount of usage of memory.

  • Thread Pool Size

The method threadPoolSize controls the size of threads used to handle the IO operations. The default size is based on the number of processors. If the network operation is the bottleneck the thread pool size must be increased. If size provided is less than or equal to 0 or bigger than 32767 will be used a cached thread pool strategy which the Threads will be created on demand and cached.

Troubleshooting

  • BufferUnderFlowException

Generally this error is caused when the size of buffer is smaller than necessary, so the size of the buffer group must be increased.