package mock.app; import mock.model.RaceLogic; import mock.model.ClientConnection; import mock.model.SourceIdAllocator; import mock.model.commandFactory.CompositeCommand; import network.Messages.Enums.XMLMessageType; import network.Messages.LatestMessages; import network.Messages.XMLMessage; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * Connection acceptor for multiple clients */ public class ConnectionAcceptor implements Runnable { /** * Port to expose server on. */ private int serverPort = 4942; /** * Socket used to listen for clients on. */ private ServerSocket serverSocket; /** * List of client connections. */ private BlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true); /** * Snapshot of the race. */ private LatestMessages latestMessages; /** * Collection of commands from clients for race to execute. */ private CompositeCommand compositeCommand; /** * Used to allocate source IDs to clients. */ private SourceIdAllocator sourceIdAllocator; //Acknowledgement number for packets private int ackNumber = 0; //race xml sequence number private short raceXMLSequenceNumber; //boat xml sequence number private short boatXMLSequenceNumber; //regatta xml sequence number private short regattaXMLSequenceNumber; // private RaceLogic raceLogic = null; /** * Connection Acceptor Constructor * @param latestMessages Latest messages to be sent * @param compositeCommand Collection of commands for race to execute. * @param sourceIdAllocator Object used to allocate source IDs for clients. * @param raceLogic The race the client will connect to. * @throws IOException if a server socket cannot be instantiated. */ public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator, RaceLogic raceLogic) throws IOException { this.latestMessages = latestMessages; this.compositeCommand = compositeCommand; this.sourceIdAllocator = sourceIdAllocator; this.raceLogic = raceLogic; this.serverSocket = new ServerSocket(serverPort); CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections); new Thread(checkClientConnection, "ConnectionAcceptor()->CheckClientConnection thread").start(); } public String getAddress() throws UnknownHostException { return InetAddress.getLocalHost().getHostAddress(); } public int getServerPort() { return serverPort; } /** * Run the Acceptor */ @Override public void run() { while(clientConnections.remainingCapacity() > 0) { try { Socket mockSocket = serverSocket.accept(); Logger.getGlobal().log(Level.INFO, String.format("Client connected. client ip/port = %s. Local ip/port = %s.", mockSocket.getRemoteSocketAddress(), mockSocket.getLocalSocketAddress())); ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand, raceLogic); clientConnections.add(clientConnection); new Thread(clientConnection, "ConnectionAcceptor.run()->ClientConnection thread " + clientConnection).start(); Logger.getGlobal().log(Level.INFO, String.format("%d number of Visualisers Connected.", clientConnections.size())); } catch (IOException e) { Logger.getGlobal().log(Level.WARNING, "Got an IOException while a client was attempting to connect.", e); } } } /** * Nested class to remove disconnected clients */ class CheckClientConnection implements Runnable{ private BlockingQueue connections; /** * Constructor * @param connections Clients "connected" */ public CheckClientConnection(BlockingQueue connections){ this.connections = connections; } /** * Run the remover. */ @Override public void run() { //We track the number of times each connection fails the !isAlive() test. //This is to give a bit of lee-way in case the connection checker checks a connection before its thread has actually started. Map connectionDeadCount = new HashMap<>(); while(!Thread.interrupted()) { //Make copy of connections. List clientConnections = new ArrayList<>(connections); for (ClientConnection client : clientConnections) { connectionDeadCount.put(client, connectionDeadCount.getOrDefault(client, 0)); if (!client.isAlive()) { //Add one to fail count. connectionDeadCount.put(client, connectionDeadCount.get(client) + 1); } //We only remove them if they fail 5 times. if (connectionDeadCount.get(client) > 5) { connections.remove(client); connectionDeadCount.remove(client); client.terminate(); Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client); } } try { Thread.sleep(100); } catch (InterruptedException e) { Logger.getGlobal().log(Level.WARNING, "CheckClientConnection was interrupted while sleeping.", e); Thread.currentThread().interrupt(); return; } } } } /** * Sets the Race XML to send. * @param raceXml XML to send to the Client. */ public void setRaceXml(String raceXml) { //Create the message. XMLMessage message = this.createXMLMessage(raceXml, XMLMessageType.RACE); //Place it in LatestMessages. this.latestMessages.setRaceXMLMessage(message); } /** * Sets the Regatta XMl to send. * @param regattaXml XML to send to Client. */ public void setRegattaXml(String regattaXml) { //Create the message. XMLMessage message = this.createXMLMessage(regattaXml, XMLMessageType.REGATTA); //Place it in LatestMessages. this.latestMessages.setRegattaXMLMessage(message); } /** * Sets the Boats XML to send. * @param boatsXml XMl to send to the Client. */ public void setBoatsXml(String boatsXml) { //Create the message. XMLMessage message = this.createXMLMessage(boatsXml, XMLMessageType.BOAT); //Place it in LatestMessages. this.latestMessages.setBoatXMLMessage(message); } /** * Creates an XMLMessage of a specified subtype using the xml contents string. * @param xmlString The contents of the xml file. * @param messageType The subtype of xml message (race, regatta, boat). * @return The created XMLMessage object. */ private XMLMessage createXMLMessage(String xmlString, XMLMessageType messageType) { //Get the correct sequence number to use, and increment it. short sequenceNumber = 0; if (messageType == XMLMessageType.RACE) { sequenceNumber = this.raceXMLSequenceNumber; this.raceXMLSequenceNumber++; } else if (messageType == XMLMessageType.BOAT) { sequenceNumber = this.boatXMLSequenceNumber; this.boatXMLSequenceNumber++; } else if (messageType == XMLMessageType.REGATTA) { sequenceNumber = this.regattaXMLSequenceNumber; this.regattaXMLSequenceNumber++; } //Create the message. XMLMessage message = new XMLMessage( XMLMessage.currentVersionNumber, getNextAckNumber(), System.currentTimeMillis(), messageType, sequenceNumber, xmlString); return message; } /** * Increments the ackNumber value, and returns it. * @return Incremented ackNumber. */ private int getNextAckNumber(){ this.ackNumber++; return this.ackNumber; } }