net.marfgamer.jraknet.client.RakNetClient.java Source code

Java tutorial

Introduction

Here is the source code for net.marfgamer.jraknet.client.RakNetClient.java

Source

/*
 *       _   _____            _      _   _          _   
 *      | | |  __ \          | |    | \ | |        | |  
 *      | | | |__) |   __ _  | | __ |  \| |   ___  | |_ 
 *  _   | | |  _  /   / _` | | |/ / | . ` |  / _ \ | __|
 * | |__| | | | \ \  | (_| | |   <  | |\  | |  __/ | |_ 
 *  \____/  |_|  \_\  \__,_| |_|\_\ |_| \_|  \___|  \__|
 *                                                  
 * The MIT License (MIT)
 *
 * Copyright (c) 2016, 2017 Trent "MarfGamer" Summerlin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.  
 */
package net.marfgamer.jraknet.client;

import static net.marfgamer.jraknet.protocol.MessageIdentifier.*;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import net.marfgamer.jraknet.NoListenerException;
import net.marfgamer.jraknet.Packet;
import net.marfgamer.jraknet.RakNet;
import net.marfgamer.jraknet.RakNetException;
import net.marfgamer.jraknet.RakNetLogger;
import net.marfgamer.jraknet.RakNetPacket;
import net.marfgamer.jraknet.client.discovery.DiscoveredServer;
import net.marfgamer.jraknet.client.discovery.DiscoveryMode;
import net.marfgamer.jraknet.client.discovery.DiscoveryThread;
import net.marfgamer.jraknet.protocol.MessageIdentifier;
import net.marfgamer.jraknet.protocol.Reliability;
import net.marfgamer.jraknet.protocol.login.ConnectionRequest;
import net.marfgamer.jraknet.protocol.login.OpenConnectionRequestOne;
import net.marfgamer.jraknet.protocol.login.OpenConnectionRequestTwo;
import net.marfgamer.jraknet.protocol.message.CustomPacket;
import net.marfgamer.jraknet.protocol.message.EncapsulatedPacket;
import net.marfgamer.jraknet.protocol.message.acknowledge.Acknowledge;
import net.marfgamer.jraknet.protocol.status.UnconnectedPing;
import net.marfgamer.jraknet.protocol.status.UnconnectedPingOpenConnections;
import net.marfgamer.jraknet.protocol.status.UnconnectedPong;
import net.marfgamer.jraknet.session.RakNetServerSession;
import net.marfgamer.jraknet.session.RakNetState;
import net.marfgamer.jraknet.session.UnumRakNetPeer;
import net.marfgamer.jraknet.util.RakNetUtils;
import net.marfgamer.jraknet.util.map.IntMap;

/**
 * Used to connect to servers using the RakNet protocol.
 *
 * @author Trent "MarfGamer" Summerlin
 */
public class RakNetClient implements UnumRakNetPeer, RakNetClientListener {

    // Used to discover systems without relying on the main thread
    private static DiscoveryThread discoverySystem = new DiscoveryThread();

    // Client data
    private final long guid;
    private final long pingId;
    private final long timestamp;
    private HashSet<Integer> discoveryPorts;
    private DiscoveryMode discoveryMode;
    /** synchronize this first! (<code>externalServers</code> goes second!) */
    private final ConcurrentHashMap<InetSocketAddress, DiscoveredServer> discovered;
    /** synchronize this second! (<code>discovered</code> goes first!) */
    private final ConcurrentHashMap<InetSocketAddress, DiscoveredServer> externalServers;
    private Thread clientThread;

    // Networking data
    private final Bootstrap bootstrap;
    private final EventLoopGroup group;
    private final RakNetClientHandler handler;
    private final IntMap<MaximumTransferUnit> maximumTransferUnits;

    // Session management
    private Channel channel;
    private SessionPreparation preparation;
    private volatile RakNetServerSession session;
    private volatile RakNetClientListener listener;

    /**
     * Constructs a <code>RakNetClient</code> with the specified
     * <code>DiscoveryMode</code> and server discovery port.
     * 
     * @param discoveryMode
     *            how the client will discover servers. If this is set to
     *            <code>null</code>, the client will enable set it to
     *            <code>DiscoveryMode.ALL_CONNECTIONS</code> as long as the port
     *            is greater than -1.
     * @param discoveryPorts
     *            the ports the client will attempt to discover servers on.
     */
    public RakNetClient(DiscoveryMode discoveryMode, int... discoveryPorts) {
        // Set client data
        UUID uuid = UUID.randomUUID();
        this.guid = uuid.getMostSignificantBits();
        this.pingId = uuid.getLeastSignificantBits();
        this.timestamp = System.currentTimeMillis();

        // Set discovery data
        this.discoveryPorts = new HashSet<Integer>();
        this.discoveryMode = discoveryMode;
        this.setDiscoveryPorts(discoveryPorts);
        if (discoveryMode == null) {
            this.discoveryMode = (discoveryPorts.length > 0 ? DiscoveryMode.ALL_CONNECTIONS : DiscoveryMode.NONE);
        }
        this.discovered = new ConcurrentHashMap<InetSocketAddress, DiscoveredServer>();
        this.externalServers = new ConcurrentHashMap<InetSocketAddress, DiscoveredServer>();

        // Set networking data
        this.bootstrap = new Bootstrap();
        this.group = new NioEventLoopGroup();
        this.handler = new RakNetClientHandler(this);

        // Add maximum transfer units
        this.maximumTransferUnits = new IntMap<MaximumTransferUnit>();
        MaximumTransferUnit firstTransferUnit = new MaximumTransferUnit(1464, 3);
        if (RakNetUtils.getMaximumTransferUnit() >= firstTransferUnit.getMaximumTransferUnit()) {
            this.addMaximumTransferUnit(new MaximumTransferUnit(RakNetUtils.getMaximumTransferUnit(), 2));
        }
        this.addMaximumTransferUnit(firstTransferUnit);
        this.addMaximumTransferUnit(new MaximumTransferUnit(1172, 4));
        this.addMaximumTransferUnit(new MaximumTransferUnit(RakNet.MINIMUM_TRANSFER_UNIT, 5));

        // Initiate bootstrap data
        try {
            bootstrap.channel(NioDatagramChannel.class).group(group).handler(handler);
            bootstrap.option(ChannelOption.SO_BROADCAST, true).option(ChannelOption.SO_REUSEADDR, false);
            this.channel = bootstrap.bind(0).sync().channel();
            RakNetLogger.debug(this, "Created and bound bootstrap");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Constructs a <code>RakNetClient</code> with the specified server
     * discovery port with the <code>DiscoveryMode</code> set to
     * <code>DiscoveryMode.ALL_CONNECTIONS</code> or
     * <code>DiscoveryMode.NONE</code> if no discovery ports are specified.
     * 
     * @param discoveryPorts
     *            the ports the client will attempt to discover servers on.
     */
    public RakNetClient(int... discoveryPorts) {
        this(null, discoveryPorts);
    }

    /**
     * @return the client's networking protocol version.
     */
    public final int getProtocolVersion() {
        return RakNet.CLIENT_NETWORK_PROTOCOL;
    }

    /**
     * @return the client's globally unique ID.
     */
    public final long getGloballyUniqueId() {
        return this.guid;
    }

    /**
     * @return the client's ping ID.
     */
    public final long getPingId() {
        return this.pingId;
    }

    /**
     * @return the client's timestamp.
     */
    public final long getTimestamp() {
        return (System.currentTimeMillis() - this.timestamp);
    }

    /**
     * @return the client's discovery ports.
     */
    public final int[] getDiscoveryPorts() {
        return discoveryPorts.stream().mapToInt(Integer::intValue).toArray();
    }

    /**
     * Sets the client's discovery ports.
     * 
     * @param discoveryPorts
     *            the new discovery ports.
     */
    public final void setDiscoveryPorts(int... discoveryPorts) {
        // We make a new set to prevent duplicates
        HashSet<Integer> discoverySet = new HashSet<Integer>();
        for (int discoveryPort : discoveryPorts) {
            if (discoveryPort < 0 || discoveryPort > 65535) {
                throw new IllegalArgumentException("Invalid port range for discovery port");
            }
            discoverySet.add(new Integer(discoveryPort));
        }

        // Set discovery ports
        this.discoveryPorts = discoverySet;
        String discoveryString = Arrays.toString(discoveryPorts);
        RakNetLogger.debug(this,
                "Set discovery ports to "
                        + (discoverySet.size() > 0 ? discoveryString.substring(1, discoveryString.length() - 1)
                                : "nothing"));
    }

    /**
     * Adds a discovery port to start broadcasting to.
     * 
     * @param discoveryPort
     *            the discovery port to start broadcasting to.
     */
    public final void addDiscoveryPort(int discoveryPort) {
        discoveryPorts.add(new Integer(discoveryPort));
        RakNetLogger.debug(this, "Added discovery port " + discoveryPort);
    }

    /**
     * Removes a discovery port to stop broadcasting from.
     * 
     * @param discoveryPort
     *            the discovery part to stop broadcasting from.
     */
    public final void removeDiscoveryPort(int discoveryPort) {
        discoveryPorts.remove(new Integer(discoveryPort));
        RakNetLogger.debug(this, "Removed discovery port " + discoveryPort);
    }

    /**
     * @return the client's discovery mode.
     */
    public final DiscoveryMode getDiscoveryMode() {
        return this.discoveryMode;
    }

    /**
     * Sets the client's discovery mode.
     * 
     * @param mode
     *            how the client will discover servers on the local network.
     */
    public final void setDiscoveryMode(DiscoveryMode mode) {
        if (listener == null) {
            throw new NoListenerException();
        }
        this.discoveryMode = (mode != null ? mode : DiscoveryMode.NONE);
        synchronized (discovered) {
            if (this.discoveryMode == DiscoveryMode.NONE) {
                for (InetSocketAddress address : discovered.keySet()) {
                    // Notify API
                    listener.onServerForgotten(address);
                }
                discovered.clear(); // We are not discovering servers anymore!
                RakNetLogger.debug(this,
                        "Cleared discovered servers due to discovery mode being set to " + DiscoveryMode.NONE);
            }
        }
        RakNetLogger.debug(this, "Set discovery mode to " + mode);
    }

    /**
     * @return the thread the server is running on if it was started using
     *         <code>startThreaded()</code>.
     */
    public final Thread getThread() {
        return this.clientThread;
    }

    /**
     * Adds a server to the client's external server discovery list. This
     * functions like the normal discovery system but is not affected by the
     * <code>DiscoveryMode</code> or discovery port set for the client.
     * 
     * @param address
     *            the server address.
     */
    public final void addExternalServer(InetSocketAddress address) {
        synchronized (externalServers) {
            if (!externalServers.contains(address)) {
                // Add newly discovered server
                externalServers.put(address, new DiscoveredServer(address, -1, null));

                // Notify API
                RakNetLogger.debug(this, "Added external server with address " + address);
                listener.onExternalServerAdded(address);
            }
        }
    }

    /**
     * Adds a server to the client's external server discovery list. This
     * functions like the normal discovery system but is not affected by the
     * <code>DiscoveryMode</code> or discovery port set for the client.
     * 
     * @param address
     *            the server address.
     * @param port
     *            the server port.
     */
    public final void addExternalServer(InetAddress address, int port) {
        this.addExternalServer(new InetSocketAddress(address, port));
    }

    /**
     * Adds a server to the client's external server discovery list. This
     * functions like the normal discovery system but is not affected by the
     * <code>DiscoveryMode</code> or discovery port set for the client.
     * 
     * @param address
     *            the server address.
     * @param port
     *            the server port.
     * @throws UnknownHostException
     *             if the specified address is an unknown host.
     */
    public final void addExternalServer(String address, int port) throws UnknownHostException {
        this.addExternalServer(InetAddress.getByName(address), port);
    }

    /**
     * Removes an external server from the client's external server discovery
     * list.
     * 
     * @param address
     *            the server address.
     */
    public final void removeExternalServer(InetSocketAddress address) {
        synchronized (externalServers) {
            if (externalServers.contains(address)) {
                // Remove now forgotten server
                externalServers.remove(address);

                // Notify API
                RakNetLogger.debug(this, "Removed external server with address " + address);
                listener.onExternalServerRemoved(address);
            }
        }
    }

    /**
     * Removes an external server from the client's external server discovery
     * list.
     * 
     * @param address
     *            the server address.
     * @param port
     *            the server port.
     */
    public final void removeExternalServer(InetAddress address, int port) {
        this.removeExternalServer(new InetSocketAddress(address, port));
    }

    /**
     * Removes an external server from the client's external server discovery
     * list.
     * 
     * @param address
     *            the server address.
     * @param port
     *            the server port.
     * @throws UnknownHostException
     *             if the specified address is an unknown host.
     */
    public final void removeExternalServer(String address, int port) throws UnknownHostException {
        this.removeExternalServer(InetAddress.getByName(address), port);
    }

    /**
     * @return the external servers as an array.
     */
    public final DiscoveredServer[] getExternalServers() {
        synchronized (externalServers) {
            return externalServers.values().toArray(new DiscoveredServer[externalServers.size()]);
        }
    }

    /**
     * Adds a <code>MaximumTransferUnit</code> that can be used by the client
     * during connection.
     * 
     * @param maximumTransferUnit
     *            the maximum transfer unit.
     */
    public final void addMaximumTransferUnit(MaximumTransferUnit maximumTransferUnit) {
        maximumTransferUnits.put(maximumTransferUnit.getMaximumTransferUnit(), maximumTransferUnit);
        RakNetLogger.debug(this,
                "Added maximum transfer unit with size of " + maximumTransferUnit.getMaximumTransferUnit() + " ("
                        + (maximumTransferUnit.getMaximumTransferUnit() * 8) + " bits) to use during client login");
    }

    /**
     * Adds a <code>MaximumTransferUnit</code> that can be used by the client
     * during connection.
     * 
     * @param maximumTransferUnit
     *            the maximum transfer unit.
     * @param retries
     *            the amount of retries before the client moves on to the lower
     *            maximum transfer unit.
     */
    public final void addMaximumTransferUnit(int maximumTransferUnit, int retries) {
        this.addMaximumTransferUnit(new MaximumTransferUnit(maximumTransferUnit, retries));
    }

    /**
     * Removes a <code>MaximumTransferUnit</code> that was being used by the
     * client based on it's maximum transfer unit.
     * 
     * @param maximumTransferUnit
     *            the maximum transfer unit to remove.
     */
    public final void removeMaximumTransferUnit(int maximumTransferUnit) {
        maximumTransferUnits.remove(maximumTransferUnit);
        RakNetLogger.debug(this, "Remove maximum transfer unit with size of " + maximumTransferUnit + " ("
                + (maximumTransferUnit * 8) + " bits) that would be used during client login");
    }

    /**
     * Removes a <code>MaximumTransferUnit</code> that was being used by the
     * client.
     * 
     * @param maximumTransferUnit
     *            the maximum transfer unit to remove.
     */
    public final void removeMaximumTransferUnit(MaximumTransferUnit maximumTransferUnit) {
        this.removeMaximumTransferUnit(maximumTransferUnit.getMaximumTransferUnit());
    }

    /**
     * @return the <code>MaximumTransferUnit</code>'s the client uses during
     *         login.
     */
    public final MaximumTransferUnit[] getMaximumTransferUnits() {
        return maximumTransferUnits.values().toArray(new MaximumTransferUnit[maximumTransferUnits.size()]);
    }

    /**
     * @return the session the client is connected to.
     */
    public final RakNetServerSession getSession() {
        return this.session;
    }

    /**
     * @return the client's listener.
     */
    public final RakNetClientListener getListener() {
        return this.listener;
    }

    /**
     * Sets the client's listener.
     * 
     * @param listener
     *            the client's new listener.
     * @return the client.
     */
    public final RakNetClient setListener(RakNetClientListener listener) {
        // Set listener
        if (listener == null) {
            throw new NullPointerException("Listener must not be null");
        }
        this.listener = listener;
        RakNetLogger.info(this, "Set listener to " + listener.getClass().getName());

        // Initiate discovery system if it is not yet started
        if (discoverySystem.isRunning() == false) {
            discoverySystem.start();
        }
        discoverySystem.addClient(this);

        return this;
    }

    /**
     * Sets the client's listener to itself, normally used for when a client is
     * an all-in-one class.
     * 
     * @return the client.
     */
    public final RakNetClient setListenerSelf() {
        return this.setListener(this);
    }

    /**
     * @return <code>true</code> if the client is currently connected to a
     *         server.
     */
    public final boolean isConnected() {
        if (session == null) {
            return false;
        }
        return session.getState().equals(RakNetState.CONNECTED);
    }

    /**
     * Returns whether or not the client is doing something on a thread. This
     * can mean multiple things, with being connected to a server or looking for
     * servers on the local network to name a few.
     * 
     * @return <code>true</code> if the client is running.
     */
    public final boolean isRunning() {
        if (channel == null) {
            return false;
        }
        return channel.isOpen();
    }

    /**
     * Called whenever the handler catches an exception in Netty.
     * 
     * @param address
     *            the address that caused the exception.
     * @param cause
     *            the exception caught by the handler.
     */
    protected final void handleHandlerException(InetSocketAddress address, Throwable cause) {
        // Handle exception based on connection state
        if (address.equals(preparation.address)) {
            if (preparation != null) {
                preparation.cancelReason = new NettyHandlerException(this, handler, cause);
            } else if (session != null) {
                this.disconnect(cause.getClass().getName() + ": " + cause.getLocalizedMessage());
            }
        }

        // Notify API
        RakNetLogger.warn(this,
                "Handled exception " + cause.getClass().getName() + " caused by address " + address);
        listener.onHandlerException(address, cause);
    }

    /**
     * Handles a packet received by the handler.
     * 
     * @param packet
     *            the packet to handle.
     * @param sender
     *            the address of the sender.
     */
    public final void handleMessage(RakNetPacket packet, InetSocketAddress sender) {
        short packetId = packet.getId();

        // This packet has to do with server discovery so it isn't handled here
        if (packetId == ID_UNCONNECTED_PONG) {
            UnconnectedPong pong = new UnconnectedPong(packet);
            pong.decode();
            if (pong.identifier != null) {
                this.updateDiscoveryData(sender, pong);
            }
        }

        // Are we still logging in?
        if (preparation != null) {
            if (sender.equals(preparation.address)) {
                preparation.handleMessage(packet);
                return;
            }
        }

        // Only handle these from the server we're connected to!
        if (session != null) {
            if (sender.equals(session.getAddress())) {
                if (packetId >= ID_CUSTOM_0 && packetId <= ID_CUSTOM_F) {
                    CustomPacket custom = new CustomPacket(packet);
                    custom.decode();

                    session.handleCustom(custom);
                } else if (packetId == Acknowledge.ACKNOWLEDGED || packetId == Acknowledge.NOT_ACKNOWLEDGED) {
                    Acknowledge acknowledge = new Acknowledge(packet);
                    acknowledge.decode();

                    session.handleAcknowledge(acknowledge);
                }
            }
        }

        if (MessageIdentifier.hasPacket(packet.getId())) {
            RakNetLogger.debug(this, "Handled internal packet with ID " + MessageIdentifier.getName(packet.getId())
                    + " (" + packet.getId() + ")");
        } else {
            RakNetLogger.debug(this, "Sent packet with ID " + packet.getId() + " to session handler");
        }
    }

    /**
     * Sends a raw message to the specified address. Be careful when using this
     * method, because if it is used incorrectly it could break server sessions
     * entirely! If you are wanting to send a message to a session, you are
     * probably looking for the
     * {@link net.marfgamer.jraknet.session.RakNetSession#sendMessage(net.marfgamer.jraknet.protocol.Reliability, net.marfgamer.jraknet.Packet)
     * sendMessage} method.
     * 
     * @param buf
     *            the buffer to send.
     * @param address
     *            the address to send the buffer to.
     */
    public final void sendNettyMessage(ByteBuf buf, InetSocketAddress address) {
        channel.writeAndFlush(new DatagramPacket(buf, address));
        RakNetLogger.debug(this, "Sent netty message with size of " + buf.capacity() + " bytes ("
                + (buf.capacity() * 8) + " bits) to " + address);
    }

    /**
     * Sends a raw message to the specified address. Be careful when using this
     * method, because if it is used incorrectly it could break server sessions
     * entirely! If you are wanting to send a message to a session, you are
     * probably looking for the
     * {@link net.marfgamer.jraknet.session.RakNetSession#sendMessage(net.marfgamer.jraknet.protocol.Reliability, net.marfgamer.jraknet.Packet)
     * sendMessage} method.
     * 
     * @param packet
     *            the packet to send.
     * @param address
     *            the address to send the packet to.
     */
    public final void sendNettyMessage(Packet packet, InetSocketAddress address) {
        this.sendNettyMessage(packet.buffer(), address);
    }

    /**
     * Sends a raw message to the specified address. Be careful when using this
     * method, because if it is used incorrectly it could break server sessions
     * entirely! If you are wanting to send a message to a session, you are
     * probably looking for the
     * {@link net.marfgamer.jraknet.session.RakNetSession#sendMessage(net.marfgamer.jraknet.protocol.Reliability, int)
     * sendMessage} method.
     * 
     * @param packetId
     *            the ID of the packet to send.
     * @param address
     *            the address to send the packet to.
     */
    public final void sendNettyMessage(int packetId, InetSocketAddress address) {
        this.sendNettyMessage(new RakNetPacket(packetId), address);
    }

    /**
     * Updates the discovery data in the client by sending pings and removing
     * servers that have taken too long to respond to a ping.
     */
    public final void updateDiscoveryData() {
        // Remove all servers that have timed out
        synchronized (discovered) {
            synchronized (externalServers) {
                ArrayList<InetSocketAddress> forgottenServers = new ArrayList<InetSocketAddress>();
                for (InetSocketAddress discoveredServerAddress : discovered.keySet()) {
                    DiscoveredServer discoveredServer = discovered.get(discoveredServerAddress);
                    if (System.currentTimeMillis()
                            - discoveredServer.getDiscoveryTimestamp() >= DiscoveredServer.SERVER_TIMEOUT_MILLI) {
                        forgottenServers.add(discoveredServerAddress);
                        listener.onServerForgotten(discoveredServerAddress);
                    }
                }
                discovered.keySet().removeAll(forgottenServers);
                if (forgottenServers.size() > 0) {
                    RakNetLogger.debug(this, "Forgot " + forgottenServers.size() + " servers");
                }

                // Broadcast ping to local network
                if (discoveryMode != DiscoveryMode.NONE) {
                    Iterator<Integer> discoveryIterator = discoveryPorts.iterator();
                    while (discoveryIterator.hasNext()) {
                        int discoveryPort = discoveryIterator.next().intValue();
                        UnconnectedPing ping = new UnconnectedPing();
                        if (discoveryMode == DiscoveryMode.OPEN_CONNECTIONS) {
                            ping = new UnconnectedPingOpenConnections();
                        }
                        ping.timestamp = this.getTimestamp();
                        ping.pingId = this.pingId;
                        ping.encode();

                        this.sendNettyMessage(ping, new InetSocketAddress("255.255.255.255", discoveryPort));
                        RakNetLogger.debug(this, "Broadcasted unconnected ping to port " + discoveryPort);
                    }
                }

                // Send ping to external servers
                synchronized (externalServers) {
                    if (!externalServers.isEmpty()) {
                        UnconnectedPing ping = new UnconnectedPing();
                        ping.timestamp = this.getTimestamp();
                        ping.pingId = this.pingId;
                        ping.encode();

                        for (InetSocketAddress externalAddress : externalServers.keySet()) {
                            this.sendNettyMessage(ping, externalAddress);
                            RakNetLogger.debug(this, "Broadcasting ping to server with address " + externalAddress);
                        }
                    }
                }
            }
        }
    }

    /**
     * This method handles the specified <code>UnconnectedPong</code> packet and
     * updates the discovery data accordingly.
     * 
     * @param sender
     *            the sender of the <code>UnconnectedPong</code> packet.
     * @param pong
     *            the <code>UnconnectedPong</code> packet to handle.
     */
    public final void updateDiscoveryData(InetSocketAddress sender, UnconnectedPong pong) {
        // Is this a local or an external server?
        synchronized (discovered) {
            synchronized (externalServers) {
                if (sender.getAddress().isSiteLocalAddress() && !externalServers.containsKey(sender)) {
                    // This is a local server
                    if (!discovered.containsKey(sender)) {
                        // Add newly discovered server
                        discovered.put(sender,
                                new DiscoveredServer(sender, System.currentTimeMillis(), pong.identifier));

                        // Notify API
                        RakNetLogger.info(this, "Discovered local server with address " + sender);
                        listener.onServerDiscovered(sender, pong.identifier);
                    } else {
                        // Server already discovered, but data has changed
                        DiscoveredServer server = discovered.get(sender);
                        server.setDiscoveryTimestamp(System.currentTimeMillis());
                        if (!pong.identifier.equals(server.getIdentifier())) {
                            // Update server identifier
                            server.setIdentifier(pong.identifier);

                            // Notify API
                            RakNetLogger.debug(this, "Updated local server with address " + sender
                                    + " identifier to \"" + pong.identifier + "\"");
                            listener.onServerIdentifierUpdate(sender, pong.identifier);
                        }
                    }
                } else if (externalServers.containsKey(sender)) {
                    DiscoveredServer server = externalServers.get(sender);
                    server.setDiscoveryTimestamp(System.currentTimeMillis());
                    if (!pong.identifier.equals(server.getIdentifier())) {
                        // Update server identifier
                        server.setIdentifier(pong.identifier);

                        // Notify API
                        RakNetLogger.debug(this, "Updated local server with address " + sender + " identifier to \""
                                + pong.identifier + "\"");
                        listener.onExternalServerIdentifierUpdate(sender, pong.identifier);
                    }
                }
            }
        }
    }

    /**
     * Connects the client to a server with the specified address.
     * 
     * @param address
     *            the address of the server to connect to.
     * @throws RakNetException
     *             if an error occurs during connection or login.
     */
    public final void connect(InetSocketAddress address) throws RakNetException {
        // Make sure we have a listener
        if (this.listener == null) {
            throw new NoListenerException();
        }

        // Reset client data
        if (this.isConnected()) {
            this.disconnect("Disconnected");
        }
        MaximumTransferUnit[] units = MaximumTransferUnit.sort(this.getMaximumTransferUnits());
        this.preparation = new SessionPreparation(this, units[0].getMaximumTransferUnit());
        preparation.address = address;

        // Send OPEN_CONNECTION_REQUEST_ONE with a decreasing MTU
        int retriesLeft = 0;
        for (MaximumTransferUnit unit : units) {
            retriesLeft += unit.getRetries();
            while (unit.retry() > 0 && preparation.loginPackets[0] == false && preparation.cancelReason == null) {
                OpenConnectionRequestOne connectionRequestOne = new OpenConnectionRequestOne();
                connectionRequestOne.maximumTransferUnit = unit.getMaximumTransferUnit();
                connectionRequestOne.protocolVersion = this.getProtocolVersion();
                connectionRequestOne.encode();
                this.sendNettyMessage(connectionRequestOne, address);

                RakNetUtils.threadLock(500);
            }
        }

        // Reset MaximumTransferUnit's so they can be used again
        for (MaximumTransferUnit unit : maximumTransferUnits.values()) {
            unit.reset();
        }

        // If the server didn't respond then it is offline
        if (preparation.loginPackets[0] == false && preparation.cancelReason == null) {
            preparation.cancelReason = new ServerOfflineException(this, preparation.address);
        }

        // Send OPEN_CONNECTION_REQUEST_TWO until a response is received
        while (retriesLeft > 0 && preparation.loginPackets[1] == false && preparation.cancelReason == null) {
            OpenConnectionRequestTwo connectionRequestTwo = new OpenConnectionRequestTwo();
            connectionRequestTwo.clientGuid = this.guid;
            connectionRequestTwo.address = preparation.address;
            connectionRequestTwo.maximumTransferUnit = preparation.maximumTransferUnit;
            connectionRequestTwo.encode();

            if (!connectionRequestTwo.failed()) {
                this.sendNettyMessage(connectionRequestTwo, address);
                RakNetUtils.threadLock(500);
            } else {
                preparation.cancelReason = new PacketBufferException(this, connectionRequestTwo);
            }
        }

        // If the server didn't respond then it is offline
        if (preparation.loginPackets[1] == false && preparation.cancelReason == null) {
            preparation.cancelReason = new ServerOfflineException(this, preparation.address);
        }

        // If the session was set we are connected
        if (preparation.readyForSession()) {
            // Set session and delete preparation data
            this.session = preparation.createSession(channel);
            this.preparation = null;

            // Send connection packet
            ConnectionRequest connectionRequest = new ConnectionRequest();
            connectionRequest.clientGuid = this.guid;
            connectionRequest.timestamp = (System.currentTimeMillis() - this.timestamp);
            connectionRequest.encode();
            session.sendMessage(Reliability.RELIABLE_ORDERED, connectionRequest);
            RakNetLogger.debug(this, "Sent connection packet to server");

            // Initiate connection loop required for the session to function
            this.initConnection();
        } else {
            // Reset the connection data, it failed
            RakNetException cancelReason = preparation.cancelReason;
            this.preparation = null;
            this.session = null;
            throw cancelReason;
        }
    }

    /**
     * Connects the client to a server with the specified address.
     * 
     * @param address
     *            the address of the server to connect to.
     * @param port
     *            the port of the server to connect to.
     * @throws RakNetException
     *             if an error occurs during connection or login.
     */
    public final void connect(InetAddress address, int port) throws RakNetException {
        this.connect(new InetSocketAddress(address, port));
    }

    /**
     * Connects the client to a server with the specified address.
     * 
     * @param address
     *            the address of the server to connect to.
     * @param port
     *            the port of the server to connect to.
     * @throws RakNetException
     *             if an error occurs during connection or login.
     * @throws UnknownHostException
     *             if the specified address is an unknown host.
     */
    public final void connect(String address, int port) throws RakNetException, UnknownHostException {
        this.connect(InetAddress.getByName(address), port);
    }

    /**
     * Connects the the client to the specified discovered server.
     * 
     * @param server
     *            the discovered server to connect to.
     * @throws RakNetException
     *             if an error occurs during connection or login.
     */
    public final void connect(DiscoveredServer server) throws RakNetException {
        this.connect(server.getAddress());
    }

    /**
     * Connects the client to a server with the specified address on it's own
     * <code>Thread</code>.
     * 
     * @param address
     *            the address of the server to connect to.
     * @return the Thread the client is running on.
     */
    public final synchronized Thread connectThreaded(InetSocketAddress address) {
        // Give the thread a reference
        RakNetClient client = this;

        // Create and start the thread
        Thread thread = new Thread() {
            @Override
            public synchronized void run() {
                try {
                    client.connect(address);
                } catch (Throwable throwable) {
                    if (client.getListener() != null) {
                        client.getListener().onThreadException(throwable);
                    } else {
                        throwable.printStackTrace();
                    }
                }
            }
        };
        thread.setName("JRAKNET_CLIENT_" + client.getGloballyUniqueId());
        thread.start();
        this.clientThread = thread;
        RakNetLogger.info(this, "Started on thread with name " + thread.getName());

        // Return the thread so it can be modified
        return thread;
    }

    /**
     * Connects the client to a server with the specified address on it's own
     * <code>Thread</code>.
     * 
     * @param address
     *            the address of the server to connect to.
     * @param port
     *            the port of the server to connect to.
     * @return the Thread the client is running on.
     */
    public final Thread connectThreaded(InetAddress address, int port) {
        return this.connectThreaded(new InetSocketAddress(address, port));
    }

    /**
     * Connects the client to a server with the specified address on it's own
     * <code>Thread</code>.
     * 
     * @param address
     *            the address of the server to connect to.
     * @param port
     *            the port of the server to connect to.
     * @throws UnknownHostException
     *             if the specified address is an unknown host.
     * @return the Thread the client is running on.
     */
    public final Thread connectThreaded(String address, int port) throws UnknownHostException {
        return this.connectThreaded(InetAddress.getByName(address), port);
    }

    /**
     * Connects the the client to the specified discovered server on it's own
     * <code>Thread</code>.
     * 
     * @param server
     *            the discovered server to connect to.
     * @return the Thread the client is running on.
     */
    public final Thread connectThreaded(DiscoveredServer server) {
        return this.connectThreaded(server.getAddress());
    }

    /**
     * Starts the loop needed for the client to stay connected to the server.
     * 
     * @throws RakNetException
     *             if any problems occur during connection.
     */
    private final void initConnection() throws RakNetException {
        if (session != null) {
            RakNetLogger.debug(this, "Initiated connected with server");
            while (session != null) {
                session.update();
            }
        } else {
            throw new RakNetClientException(this, "Attempted to initiate connection without session");
        }
    }

    @Override
    public final EncapsulatedPacket sendMessage(Reliability reliability, int channel, Packet packet) {
        if (this.isConnected()) {
            return session.sendMessage(reliability, channel, packet);
        }
        return null;
    }

    /**
     * Disconnects the client from the server if it is connected to one.
     * 
     * @param reason
     *            the reason the client disconnected from the server.
     */
    public final void disconnect(String reason) {
        if (session != null) {
            // Disconnect session
            session.closeConnection();

            // Interrupt it's thread if it owns one
            if (this.clientThread != null) {
                clientThread.interrupt();
            }

            // Notify API
            RakNetLogger.info(this, "Disconnected from server with address " + session.getAddress()
                    + " with reason \"" + reason + "\"");
            listener.onDisconnect(session, reason);

            // Destroy session
            this.session = null;
        } else {
            RakNetLogger.warn(this,
                    "Attempted to disconnect from server even though it was not connected to as server in the first place");
        }
    }

    /**
     * Disconnects the client from the server if it is connected to one.
     */
    public final void disconnect() {
        this.disconnect("Disconnected");
    }

    /**
     * Shuts down the client for good, once this is called the client can no
     * longer connect to servers.
     */
    public final void shutdown() {
        // Close channel
        if (this.isRunning()) {
            channel.close();
            group.shutdownGracefully();

            // Shutdown discovery system if needed
            discoverySystem.removeClient(this);
            if (discoverySystem.getClients().length <= 0) {
                discoverySystem.shutdown();
                discoverySystem = new DiscoveryThread();
            }

            // Notify API
            RakNetLogger.info(this, "Shutdown client");
            listener.onClientShutdown();
        } else {
            RakNetLogger.warn(this, "Client attempted to shutdown after it was already shutdown");
        }
    }

    /**
     * Disconnects from the server and shuts down the client for good, once this
     * is called the client can no longer connect to servers.
     * 
     * @param reason
     *            the reason the client shutdown.
     */
    public final void disconnectAndShutdown(String reason) {
        // Disconnect from server
        if (this.isConnected()) {
            this.disconnect(reason);
        }
        this.shutdown();
    }

    /**
     * Disconnects from the server and shuts down the client for good, once this
     * is called the client can no longer connect to servers.
     */
    public final void disconnectAndShutdown() {
        this.disconnectAndShutdown("Client shutdown");
    }

    @Override
    public final void finalize() {
        this.shutdown();
        RakNetLogger.debug(this, "Finalized and collected by garbage heap");
    }

}