Java tutorial
/* * 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."); } }