ca.wumbo.doommanager.server.ServerManager.java Source code

Java tutorial

Introduction

Here is the source code for ca.wumbo.doommanager.server.ServerManager.java

Source

/*
 * DoomManager
 * Copyright (C) 2014  Chris K
 * 
 * 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 ca.wumbo.doommanager.server;

import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;

import ca.wumbo.doommanager.Start;
import ca.wumbo.doommanager.core.config.Config;
import ca.wumbo.doommanager.file.dxml.DXML;
import ca.wumbo.doommanager.file.dxml.DXMLException;
import ca.wumbo.doommanager.file.entry.Entry;

/**
 * Manages all the aspect of a live server. This class is not thread safe.
 */
public class ServerManager implements Runnable {

    /**
     * The logger for this class.
     */
    private static final Logger log = LogManager.getLogger(ServerManager.class);

    /**
     * Check every 10 seconds for a timed out connection.
     */
    private static final long TIMEOUT_FREQUENCY_CHECK_MILLIS = 10 * 1000;

    /**
     * Not getting a message for this many seconds means the client is likely
     * dead.
     */
    private static final long TIMEOUT_CLIENT_MILLIS = 30 * 1000;

    /**
     * The port being listened on for incoming connections.
     */
    private int listenPort;

    /**
     * The selector for all the clients.
     */
    private Selector selector;

    /**
     * The socket channel for listening to incoming connections, is null if not
     * initialized.
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * The last time a check was done.
     */
    private long lastTimeoutCheckMillis;

    /**
     * If the object has been initialized.
     */
    private boolean isInitialized;

    /**
     * If the main thread is running.
     */
    private boolean isRunning;

    /**
     * The DoomFileManager that controls the server project.
     */
    private ServerDoomFileManager serverDoomFileManager;

    @Autowired
    private Config config;

    /**
     * Creates a non-initialized server manager.
     */
    public ServerManager() {
    }

    /**
     * Starts up the channels and selectors for net connections. This must be
     * called before executing the run() method. This is synchronized, though
     * multiple threads should not be calling initialize on this anyways.
     * 
     * @throws IOException
     *       If any errors occur while setting up the connection handling.
     * 
     * @throws IllegalArgumentException
     *       If the IP is empty upon   extraction from the socket channel.
     * 
     * @throws RuntimeException
     *       If there are multiple attempts to initialize the server.
     */
    public void initialize() throws IOException {
        if (isInitialized)
            throw new RuntimeException("Attempting to initialize a server when it already has been.");

        log.trace("Initializing Server Manager.");
        listenPort = config.getServerConfig().getPort();
        selector = Selector.open();

        // Attempt to open a channel.
        log.debug("Attempting to bind server to port {}.", listenPort);
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(listenPort));
        log.info("Successfully bound server to port {}.", listenPort);

        isInitialized = true;
    }

    /**
     * If the server manager has been initialized.
     * 
     * @return
     *       True if it has, false if not.
     */
    public boolean isInitialized() {
        return isInitialized;
    }

    /**
     * Returns if the server is running.
     * 
     * @return
     *       True if it's running, false if not.
     */
    public boolean isRunning() {
        return isRunning;
    }

    /**
     * Requests server termination. This will not stop it immediately, and it
     * will do nothing if the server has not been initialized and run. This can
     * be checked with isInitialized() and isRunning(). If both are true, this
     * method can be run, otherwise it will do nothing.
     */
    public void requestServerTermination() {
        isRunning = false;
    }

    /**
     * Attempts to accept incoming new connections.
     * 
     * @return
     *       True if there was no error, false if an error occured.
     */
    private boolean acceptConnections() {
        SocketChannel socketChannel = null;

        // Only bother if the connection is open.
        if (serverSocketChannel.isOpen()) {

            // Attempt to get a connection.
            try {
                socketChannel = serverSocketChannel.accept();
            } catch (Exception e) {
                log.error("Unexpected IO exception when accepting a connection.", e);
                return false;
            }

            // If there was a client connecting, take care of them.
            // Make sure to attach a new ClientInfo that we will fill out later upon validation.
            if (socketChannel != null) {
                try {
                    log.info("Incoming new client connection from {}.",
                            socketChannel.getRemoteAddress().toString());
                    ClientInfo clientInfo = new ClientInfo(socketChannel.getRemoteAddress().toString());
                    socketChannel.configureBlocking(false);
                    socketChannel.socket().setTcpNoDelay(true);
                    socketChannel.register(selector, SelectionKey.OP_READ, clientInfo);
                    clientInfo.markMessageReceived(); // Prevent timing out from a connection.

                    //               // TEST
                    //               int amount = 1024 * 16;
                    //               byte[] hi = new byte[amount];
                    //               for (int i = 0; i < amount; i++)
                    //                  hi[i] = (byte)(Math.random() * 255);
                    //               ByteBuffer bb = ByteBuffer.allocate(amount);
                    //               bb.put(hi);
                    //               bb.flip();
                    //               
                    //               int wrote = 0;
                    //               while (bb.hasRemaining()) {
                    //                  wrote = socketChannel.write(bb);
                    //                  System.out.println("Server wrote: " + wrote + " bytes to a client");
                    //               }
                    //               
                    //               Thread.sleep(4000);
                    //               
                    //               bb = ByteBuffer.allocate(5);
                    //               bb.put(new byte[] { 1, 2, 3, 4, 5 });
                    //               bb.flip();
                    //               wrote = 0;
                    //               while (bb.hasRemaining()) {
                    //                  wrote = socketChannel.write(bb);
                    //                  System.out.println("2) Server wrote: " + wrote + " bytes to a client");
                    //               }

                    // TODO - send global version of the file.
                } catch (ClosedChannelException e) {
                    log.error("Channel closed exception when registering a connected client.", e);
                    return false;
                } catch (IOException e) {
                    log.error("IO exception when registering a new client connection.", e);
                    return false;
                } catch (Exception e) {
                    log.error("Unexpected exception when registering a new client connection.", e);
                    return false;
                }
            }
        }

        // Signal all is good.
        return true;
    }

    /**
     * Processes incoming data from all clients if any is present.
     * 
     * @return
     *       True if there was no error, false if an error occured.
     */
    private boolean processIncomingData() {
        // If the selector is open, look for data.
        if (selector.isOpen()) {
            try {
                int availableKeys = selector.selectNow();

                // If there are available keys, get the key set and handle them.
                if (availableKeys > 0) {
                    // TODO
                }
            } catch (Exception e) {
                log.error("Unexpected exception when selecting now from a selector.", e);
            }
        }

        // Signal all is good.
        return true;
    }

    /**
     * Kills any connections that have not sent any messages for a period of
     * time.
     * 
     * @return
     *       True if there was no error, false if an error occured.
     */
    private boolean killInactiveConnections() {
        // Go through each connection every so often to check for dead connections.
        if (System.currentTimeMillis() - lastTimeoutCheckMillis > TIMEOUT_FREQUENCY_CHECK_MILLIS) {
            lastTimeoutCheckMillis = System.currentTimeMillis();
            log.trace("Checking for timeouts... {}", System.currentTimeMillis());

            // We cannot do this if the selector is closed.
            if (selector.isOpen()) {
                // Go through each connection...
                for (SelectionKey key : selector.keys()) {
                    // If the attachment has client info (which it always should)...
                    if (key.attachment() instanceof ClientInfo) {
                        ClientInfo clientInfo = (ClientInfo) key.attachment();
                        // If the client hasn't responded for a certain amount of time, kill the connection.
                        if (clientInfo.hasReceivedMessageSince(TIMEOUT_CLIENT_MILLIS)) {
                            log.info("Client {} timed out, requesting connection termination.",
                                    clientInfo.getIPAddress());
                            Channel channel = key.channel();
                            key.cancel();
                            try {
                                channel.close();
                            } catch (IOException e) {
                                log.error("Error closing a timed out client's channel: {}", e.getMessage());
                            }
                        }
                    }
                }
            }
        }

        // Signal all is good.
        return true;
    }

    /**
     * Closes the connection listener.
     */
    private void closeConnectionListener() {
        if (serverSocketChannel != null && serverSocketChannel.isOpen()) {
            try {
                serverSocketChannel.close();
                log.debug("Successful connection listener closure at server termination.");
            } catch (IOException e) {
                log.error("Connection listening channel errored out when shutting down.", e);
            }
        } else {
            log.debug("Connection listener already closed pre-termination.");
        }
    }

    /**
     * Closes all active connections (does not send out a kill message).
     */
    private void closeConnections() {
        if (selector.isOpen()) {
            for (SelectionKey key : selector.keys()) {
                key.cancel();
                try {
                    key.channel().close();
                } catch (IOException e) {
                    log.error("Error closing a timed out client's channel.");
                    log.error(e);
                }
            }
        }
    }

    /**
     * Closes the selector.
     */
    private void closeSelector() {
        if (selector != null && selector.isOpen()) {
            try {
                selector.close();
            } catch (Exception e) {
                log.error("Error shutting down selector at exit.");
            }
        }
    }

    /**
     * Loads a project from the command line argument that should have been
     * verified at the very start of the program.
     * 
     * @return
     *       True on successful loading, false if there was any problems.
     */
    private boolean loadProject() {
        File dxmlFile = new File(Start.getParsedRuntimeArgs().getFile());
        DXML dxml = null;

        // Since client/server mode doesn't run immediately (whereas the -server instance does), we should check one last time...
        if (!dxmlFile.exists() || !dxmlFile.canRead()) {
            log.error("The .dxml file either doesn't exist, or cannot be read.");
            return false;
        }

        // Read in the DXML file.
        try {
            dxml = new DXML(dxmlFile);
        } catch (DXMLException e) {
            log.error("Error loading .dxml file.", e);
            return false;
        }

        // Make sure it's valid.
        if (!dxml.isValidDXMLFile()) {
            log.error("Loaded .dxml file has invalid properties and cannot be safely loaded.");
            log.error("You may be able to fix or recover your file easily with a text editor.");
            return false;
        }

        // Prepare the project. Now that we have the DXML file, we can designate it and start the server.
        long startNs = 0;
        long endNs = 0;
        //      try {
        //         log.info("Loading .dxml project...");
        //         startNs = System.nanoTime();
        //         serverDoomFileManager = new ServerDoomFileManager(Designator.designateDXML(dxmlFile, dxml));
        //         endNs = System.nanoTime();
        //      } catch (IOException e) {
        //         log.error("Error reading data from hard drive when loading .dxml components.");
        //         return false;
        //      }

        // Signal everything was loaded properly.
        log.info("Successfully loaded .dxml file (took {} sec).", ((endNs - startNs) / 1000000000.0));
        return true;
    }

    /**
     * Gets the manager. This will be null if the object has not been started
     * on a thread yet.
     * 
     * @return
     *       The manager, or null if it hasn't been loaded.
     */
    public ServerDoomFileManager getServerDoomFileManager() {
        return serverDoomFileManager;
    }

    /**
     * Gets the root node. If the DoomFileManager has not been created, or it
     * has no root node, this will return null.
     * 
     * @return
     *       The root node, or null.
     */
    public Entry getRootNode() {
        return serverDoomFileManager != null ? serverDoomFileManager.getRootNode() : null;
    }

    @Override
    public void run() {
        // If it is not initialized or is already running, error out.
        if (!isInitialized)
            throw new RuntimeException("Did not call initialize() on the ServerManager.");
        if (isRunning)
            throw new RuntimeException("Attempting to run the server twice.");

        // Load up the files to be run.
        if (!loadProject()) {
            log.fatal("DXML file provided for running the server could not be properly loaded.");
            log.fatal(
                    "Please check your file and ensure it is not corrupt (or that your directory is okay to read).");
            return;
        }

        // Run until we're told to stop.
        isRunning = true;
        boolean noError = false;
        log.info("Server is online and accepting connections.");
        while (isRunning) {
            // Listen for any new connections.
            noError = acceptConnections();
            if (!noError) {
                log.error("Error encountered when accepting connections.");
                break;
            }

            // Check for any incoming data and process it.
            noError = processIncomingData();
            if (!noError) {
                log.error("Error encountered when processing incoming data.");
                break;
            }

            // Kill any timed out connections.
            // Note that this must be after processIncomingData() because .select() is called.
            // This removes dead clients that we have set to be killed.
            noError = killInactiveConnections();
            if (!noError) {
                log.error("Error encountered when killing inactive connections.");
                break;
            }

            // Sleep so we don't choke the OS.
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                log.error("Server thread interrupted while sleeping, stopping server.");
                break;
            }
        }

        // Shut down the connections.
        closeConnectionListener();
        closeConnections();
        closeSelector(); // Should go last because the keys are used to kill the active connections.

        isRunning = false;
        log.info("DoomManager server terminated.");
    }
}