com.l2jfree.network.mmocore.MMOController.java Source code

Java tutorial

Introduction

Here is the source code for com.l2jfree.network.mmocore.MMOController.java

Source

/*
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.l2jfree.network.mmocore;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;

import javolution.util.FastList;

import org.apache.commons.lang3.ObjectUtils;

import com.l2jfree.Startup;
import com.l2jfree.Startup.StartupHook;
import com.l2jfree.network.mmocore.FloodManager.ErrorMode;
import com.l2jfree.network.mmocore.FloodManager.Result;

/**
 * The main class of the MMOCore responsible for controlling basically everything :) The required
 * behaviours should be reached through subclasses.
 * 
 * @see #start()
 * @see #openServerSocket(InetAddress, int)
 * @see #acceptConnectionFrom(SocketChannel)
 * @see #connect(InetSocketAddress, boolean)
 * @see #createClient(SocketChannel)
 * @see #canReceivePacketFrom(MMOConnection, int)
 * @see #report(ErrorMode, MMOConnection, ReceivablePacket, Throwable)
 * @see #shutdown()
 * @param <T> connection
 * @param <RP> receivable packet
 * @param <SP> sendable packet
 * @author KenM (reference)<BR>
 *         Parts of design based on networkcore from WoodenGil
 * @author NB4L1 (l2jfree)
 * @author savormix (l2jfree)
 */
public abstract class MMOController<T extends MMOConnection<T, RP, SP>, RP extends ReceivablePacket<T, RP, SP>, SP extends SendablePacket<T, RP, SP>> {
    protected static final MMOLogger _log = new MMOLogger(MMOController.class, 1000);

    private final MMOConfig _config;
    private final String _name;

    private AcceptorThread<T, RP, SP> _acceptorThread;
    private final FastList<ConnectorThread<T, RP, SP>> _connectorThreads = new FastList<ConnectorThread<T, RP, SP>>();
    private final ReadWriteThread<T, RP, SP>[] _readWriteThreads;

    private volatile boolean _started;

    @SuppressWarnings("unchecked")
    protected MMOController(MMOConfig config, PacketHandler<T, RP, SP> packetHandler) throws IOException {
        _config = config;
        _config.setModifiable(false);
        _name = config.getName();

        _readWriteThreads = new ReadWriteThread[config.getThreadCount()];

        for (int i = 0; i < _readWriteThreads.length; i++) {
            final ReadWriteThread<T, RP, SP> readWriteThread = new ReadWriteThread<T, RP, SP>(this, config,
                    packetHandler);

            readWriteThread.setName(readWriteThread.getName() + "-" + (i + 1));

            _readWriteThreads[i] = readWriteThread;
        }

        _started = false;
    }

    /**
     * Opens a server socket, to accept incoming connections.
     * 
     * @param address the address to listen on
     * @param port the port to listen on
     * @throws IOException if any problem occurs while opening the acceptor
     * @throws UnknownHostException if an invalid address was given
     */
    public final void openServerSocket(String address, int port) throws IOException, UnknownHostException {
        openServerSocket(InetAddress.getByName(address), port);
    }

    /**
     * Opens a server socket, to accept incoming connections.
     * 
     * @param address the address to listen on (should be null in order to listen on all available
     *            addresses)
     * @param port the port to listen on
     * @throws IOException if any problem occurs while opening the acceptor
     */
    public final void openServerSocket(InetAddress address, int port) throws IOException {
        if (_acceptorThread == null) {
            final AcceptorThread<T, RP, SP> acceptor = new AcceptorThread<T, RP, SP>(this, _config);
            if (_started)
                acceptor.start();

            _acceptorThread = acceptor;
        }

        _acceptorThread.openServerSocket(address, port);

        System.out.println(getName() + " ready on " + ObjectUtils.toString(address) + ":" + port);
    }

    /**
     * Connects to the given address and port as a client.
     * 
     * @param address the address to connect to
     * @param port the port to connect to
     * @param persistent keep this connection alive during runtime
     * @throws UnknownHostException if an invalid address was given
     */
    public final void connect(String address, int port, boolean persistent) throws UnknownHostException {
        connect(InetAddress.getByName(address), port, persistent);
    }

    /**
     * Connects to the given address and port as a client.
     * 
     * @param address the address to connect to
     * @param port the port to connect to
     * @param persistent keep this connection alive during runtime
     */
    public final void connect(InetAddress address, int port, boolean persistent) {
        connect(new InetSocketAddress(address, port), persistent);
    }

    /**
     * Connects to the given address and port as a client.
     * 
     * @param socketAddress the socket address (adress and port) to connect to
     * @param persistent keep this connection alive during runtime
     */
    public final void connect(InetSocketAddress socketAddress, boolean persistent) {
        final ConnectorThread<T, RP, SP> connector = new ConnectorThread<T, RP, SP>(this, socketAddress,
                persistent);
        if (_started)
            connector.start();

        _connectorThreads.add(connector);
    }

    /**
     * Removes the non-persistent thread from the list once it's finished.
     * 
     * @param connector
     */
    final void removeConnectorThread(ConnectorThread<T, RP, SP> connector) {
        _connectorThreads.remove(connector);
    }

    /**
     * Returns the name of this controller's configuration.
     * 
     * @return configuration's name
     */
    public final String getName() {
        return _name;
    }

    private int _readWriteThreadIndex;

    final ReadWriteThread<T, RP, SP> getRandomReadWriteThread() {
        return _readWriteThreads[_readWriteThreadIndex++ % _readWriteThreads.length];
    }

    /**
     * Starts the mmocore threads.
     */
    public final void start() {
        Startup.addStartupHook(new StartupHook() {
            @Override
            public void onStartup() {
                _started = true;

                if (_acceptorThread != null)
                    _acceptorThread.start();

                for (ConnectorThread<T, RP, SP> connectorThread : _connectorThreads)
                    connectorThread.start();

                for (ReadWriteThread<T, RP, SP> readWriteThread : _readWriteThreads)
                    readWriteThread.start();
            }
        });
    }

    /**
     * @return mmocore threads status
     */
    public final boolean isStarted() {
        return _started;
    }

    /**
     * Initiates the shutdown of the mmocore threads, and waits until they are finished.
     */
    public final void shutdown() {
        try {
            if (_acceptorThread != null)
                _acceptorThread.shutdown();

            for (ConnectorThread<T, RP, SP> connectorThread : _connectorThreads)
                connectorThread.shutdown();

            for (ReadWriteThread<T, RP, SP> readWriteThread : _readWriteThreads)
                readWriteThread.shutdown();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    // ==============================================

    /**
     * Creates a client with any required custom additions.
     * 
     * @param socketChannel the socket channel for read-write calls
     * @return a client backed by the given socket channel
     * @throws ClosedChannelException
     */
    protected abstract T createClient(SocketChannel socketChannel) throws ClosedChannelException;

    // ==============================================

    // TODO fine tune
    private final FloodManager _accepts = initAcceptsFloodManager();
    private final FloodManager _packets = initPacketsFloodManager();
    private final FloodManager _errors = initErrorsFloodManager();

    @SuppressWarnings("static-method")
    protected FloodManager initAcceptsFloodManager() {
        return new FloodManager(1000, // 1000 msec per tick
                new FloodManager.FloodFilter(10, 20, 10), // short period
                new FloodManager.FloodFilter(30, 60, 60)); // long period
    }

    @SuppressWarnings("static-method")
    protected FloodManager initPacketsFloodManager() {
        return new FloodManager(1000, // 1000 msec per tick
                new FloodManager.FloodFilter(250, 300, 2));
    }

    @SuppressWarnings("static-method")
    protected FloodManager initErrorsFloodManager() {
        return new FloodManager(200, // 200 msec per tick
                new FloodManager.FloodFilter(10, 10, 1));
    }

    /**
     * @return a String with informations about this controller for debugging purposes
     */
    protected String getVersionInfo() {
        return getClass().getSimpleName() + " - " + getName();
    }

    /**
     * An easy way to apply any special limitations on incoming connections. At default it contains
     * a flood protection.<br>
     * Overriding implementations should call the super method before anything else gets checked.<br>
     * <br>
     * NOTE: Uses a special way of logging to avoid console flood.
     * 
     * @param sc the inbound connection from a possible client
     * @return true if the connection is valid, and should be allowed, no otherwise
     */
    protected boolean acceptConnectionFrom(SocketChannel sc) {
        final String host = sc.socket().getInetAddress().getHostAddress();

        final Result isFlooding = _accepts.isFlooding(host, true);

        switch (isFlooding) {
        case REJECTED: {
            // TODO punish, warn, log, etc
            _log.warn("Rejected connection from " + host);
            return false;
        }
        case WARNED: {
            // TODO punish, warn, log, etc
            _log.warn("Connection over warn limit from " + host);
            return true;
        }
        default:
            return true;
        }
    }

    /**
     * To report any occurring error.<br>
     * <br>
     * NOTE: Uses a special way of logging to avoid console flood.
     * 
     * @param mode describing the nature of the error
     * @param client the associated client
     * @param packet the associated packet, if there is any
     * @param throwable the associated throwable, if there is any
     */
    public void report(ErrorMode mode, T client, RP packet, Throwable throwable) {
        final Result isFlooding = _errors.isFlooding(client.getValidUID(), true);

        final StringBuilder sb = new StringBuilder();
        if (isFlooding != Result.ACCEPTED) {
            sb.append("Flooding with ");
        }
        sb.append(mode);
        sb.append(": ");
        sb.append(client);
        if (packet != null) {
            sb.append(" - ");
            sb.append(packet.getType());
        }
        final String versionInfo = getVersionInfo();
        if (versionInfo != null && !versionInfo.isEmpty()) {
            sb.append(" - ");
            sb.append(versionInfo);
        }

        if (throwable != null)
            _log.info(sb, throwable);
        else
            _log.info(sb);

        //if (isFlooding != Result.ACCEPTED)
        //{
        //   // TODO punish, warn, log, etc
        //}
    }

    /**
     * An easy way to apply any special limitations on incoming packets. At default it contains a
     * flood protection.<br>
     * Overriding implementations should call the super method before anything else gets checked.<br>
     * <br>
     * NOTE: Uses a special way of logging to avoid console flood.
     * 
     * @param client the associated client
     * @param opcode the opcode of the potential packet (for debugging purposes)
     * @return true if the client can be allowed to receive a packet, no otherwise
     */
    protected boolean canReceivePacketFrom(T client, int opcode) {
        final String key = client.getValidUID();

        switch (Result.max(_packets.isFlooding(key, true), _errors.isFlooding(key, false))) {
        case REJECTED: {
            // TODO punish, warn, log, etc
            _log.warn("Rejected packet (0x" + Integer.toHexString(opcode) + ") from " + client);
            return false;
        }
        case WARNED: {
            // TODO punish, warn, log, etc
            _log.warn("Packet over warn limit (0x" + Integer.toHexString(opcode) + ") from " + client);
            return true;
        }
        default:
            return true;
        }
    }
}