From 89b0aa8b77209f0e7f576b101a4d35bb43bda739 Mon Sep 17 00:00:00 2001 From: fjc40 Date: Sat, 12 Aug 2017 19:58:47 +1200 Subject: [PATCH] Implemented MessageRouter. Added ConnectionToServerCommandFactory, and JoinSuccessfulCommand, RaceParticipantsFullCOmmand, ServerFullCommand. Added IncomingHeartBeatCommandFactory, and IncomingHeartBeatCommand. Added ConnectionToServerState, which represents the client's connection state to server. Renamed VisualiserInput to VisualiserRaceController. Added ConnectionToServer, which tracks the client's connection to server. Added ConnectionToServerController, which accepts JoinAcceptance messages, turns them into commands, and passes them to ConnectionToServer. Added IncomingHeartBeatService, which tracks the heart beat status of the connection. Added IncomingHeartBeatController, which accepts HeartBeat messages, turns them into commands, and passes them to IncomingHeartBeatService. Refactored ServerConnection a bit. #story[1095] --- .../java/mock/model/HeartBeatService.java | 2 +- .../network/MessageRouters/MessageRouter.java | 118 ++++- .../StreamRelated/MessageDeserialiser.java | 8 +- .../StreamRelated/MessageSerialiser.java | 8 + .../ConnectionToServerCommandFactory.java | 41 ++ .../JoinSuccessfulCommand.java | 47 ++ .../RaceParticipantsFullCommand.java | 47 ++ .../ServerFullCommand.java | 47 ++ .../IncomingHeartBeatCommand.java | 47 ++ .../IncomingHeartBeatCommandFactory.java | 33 ++ .../Controllers/MainController.java | 3 +- .../Controllers/RaceController.java | 3 +- .../Controllers/StartController.java | 6 +- .../enums/ConnectionToServerState.java | 96 ++++ .../visualiser/model/ServerConnection.java | 243 ---------- .../VisualiserRaceController.java} | 40 +- .../network/ConnectionToServer.java | 157 ++++++ .../network/ConnectionToServerController.java | 78 +++ .../network/IncomingHeartBeatController.java | 78 +++ .../network/IncomingHeartBeatService.java | 109 +++++ .../visualiser/network/ServerConnection.java | 459 ++++++++++++++++++ .../ConnectionToServerParticipantTest.java | 150 ++++++ .../ConnectionToServerSpectatorTest.java | 109 +++++ 23 files changed, 1640 insertions(+), 289 deletions(-) create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java create mode 100644 racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java create mode 100644 racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java delete mode 100644 racevisionGame/src/main/java/visualiser/model/ServerConnection.java rename racevisionGame/src/main/java/visualiser/{app/VisualiserInput.java => model/VisualiserRaceController.java} (82%) create mode 100644 racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java create mode 100644 racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java create mode 100644 racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java create mode 100644 racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java create mode 100644 racevisionGame/src/main/java/visualiser/network/ServerConnection.java create mode 100644 racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java create mode 100644 racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java diff --git a/racevisionGame/src/main/java/mock/model/HeartBeatService.java b/racevisionGame/src/main/java/mock/model/HeartBeatService.java index 232eb9ad..0028a575 100644 --- a/racevisionGame/src/main/java/mock/model/HeartBeatService.java +++ b/racevisionGame/src/main/java/mock/model/HeartBeatService.java @@ -22,7 +22,7 @@ public class HeartBeatService implements RunnableWithFramePeriod { /** * Period for the heartbeat - that is, how often we send it. Milliseconds. */ - private long heartbeatPeriod = 5000; + private long heartbeatPeriod = 2500; /** diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java index 4eaa6dce..b767b880 100644 --- a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java +++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java @@ -1,11 +1,127 @@ package network.MessageRouters; +import network.Messages.AC35Data; +import network.Messages.Enums.MessageType; +import org.jetbrains.annotations.NotNull; +import shared.model.RunnableWithFramePeriod; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller. */ -public class MessageRouter { +public class MessageRouter implements RunnableWithFramePeriod { + + + /** + * Incoming queue of messages. + */ + private BlockingQueue incomingMessages; + + + /** + * The routing map, which maps from a {@link MessageType} to a message queue. + */ + private Map> routeMap = new HashMap<>(); + + + /** + * The default routing queue. + * Messages without routes are sent here. + * Nothing by default, which means unrouted messages are discarded + */ + private Optional> defaultRoute = Optional.empty(); + + + + /** + * Constructs a {@link MessageRouter} with a given incoming message queue. + * @param incomingMessages Incoming message queue to read from. + */ + public MessageRouter(BlockingQueue incomingMessages) { + this.incomingMessages = incomingMessages; + } + + + /** + * Returns the queue the message router reads from. + * Place messages onto this queue to pass them to the router. + * @return Queue the message router reads from. + */ + public BlockingQueue getIncomingMessageQueue() { + return incomingMessages; + } + + + /** + * Adds a route, which routes a given type of message to a given queue. + * @param messageType The message type to route. + * @param queue The queue to route messages to. + */ + public void addRoute(MessageType messageType, BlockingQueue queue) { + routeMap.put(messageType, queue); + } + + /** + * Removes the route for a given {@link MessageType}. + * @param messageType MessageType to remove route for. + */ + public void removeRoute(MessageType messageType) { + routeMap.remove(messageType); + } + + /** + * Adds a given queue as the default route for any unrouted message types. + * @param queue Queue to use as default route. + */ + public void addDefaultRoute(@NotNull BlockingQueue queue) { + defaultRoute = Optional.of(queue); + } + + /** + * Removes the current default route, if it exists. + */ + public void removeDefaultRoute() { + defaultRoute = Optional.empty(); + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + + AC35Data message = incomingMessages.take(); + + + if (routeMap.containsKey(message.getType())) { + //We have a route. + routeMap.get(message.getType()).put(message); + + } else { + //No route. Use default. + if (defaultRoute.isPresent()) { + defaultRoute.get().put(message); + } + } + + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "MessageRouter: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading message.", e); + Thread.currentThread().interrupt(); + } + } + } } diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java index 028e01a9..2dbbe4e0 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java @@ -50,7 +50,13 @@ public class MessageDeserialiser implements RunnableWithFramePeriod { this.messagesRead = messagesRead; } - + /** + * Returns the queue of messages read from the socket. + * @return Queue of messages read from socket. + */ + public BlockingQueue getMessagesRead() { + return messagesRead; + } /** diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java index 8d2a7038..84bddf78 100644 --- a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java +++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java @@ -54,6 +54,14 @@ public class MessageSerialiser implements RunnableWithFramePeriod { this.messagesToSend = messagesToSend; } + /** + * Returns the queue of messages to write to the socket. + * @return Queue of messages to write to the socket. + */ + public BlockingQueue getMessagesToSend() { + return messagesToSend; + } + /** * Increments the ackNumber value, and returns it. diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java new file mode 100644 index 00000000..bac06db3 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java @@ -0,0 +1,41 @@ +package visualiser.Commands.ConnectionToServerCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.JoinAcceptance; +import visualiser.network.ConnectionToServer; + +/** + * Factory to create ConnectionToServer commands. + */ +public class ConnectionToServerCommandFactory { + + /** + * Generates a command to execute on server connection based on the type of {@link network.Messages.Enums.JoinAcceptanceEnum}. + * @param message The message to turn into a command. + * @param connectionToServer The connection for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(AC35Data message, ConnectionToServer connectionToServer) throws CommandConstructionException { + + if (!(message instanceof JoinAcceptance)) { + throw new CommandConstructionException("Message: " + message + " is not a JoinAcceptance message."); + } + + JoinAcceptance joinAcceptance = (JoinAcceptance) message; + + + switch(joinAcceptance.getAcceptanceType()) { + + case JOIN_SUCCESSFUL: return new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + case RACE_PARTICIPANTS_FULL: return new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + case SERVER_FULL: return new ServerFullCommand(joinAcceptance, connectionToServer); + + default: throw new CommandConstructionException("Could not create command for JoinAcceptance: " + joinAcceptance + ". Unknown JoinAcceptanceEnum."); + } + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java new file mode 100644 index 00000000..8cdfea5b --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessfulCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL} {@link network.Messages.JoinAcceptance} message is received. + */ +public class JoinSuccessfulCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link JoinSuccessfulCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public JoinSuccessfulCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java new file mode 100644 index 00000000..ae631c6e --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/RaceParticipantsFullCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#RACE_PARTICIPANTS_FULL} {@link JoinAcceptance} message is received. + */ +public class RaceParticipantsFullCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link RaceParticipantsFullCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public RaceParticipantsFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.DECLINED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java new file mode 100644 index 00000000..e88d2c16 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.ConnectionToServerCommands; + +import mock.model.commandFactory.Command; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; + +import java.util.Optional; + + +/** + * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#SERVER_FULL} {@link JoinAcceptance} message is received. + */ +public class ServerFullCommand implements Command { + + /** + * The message to operate on. + */ + private JoinAcceptance joinAcceptance; + + /** + * The context to operate on. + */ + private ConnectionToServer connectionToServer; + + + /** + * Creates a new {@link ServerFullCommand}, which operates on a given {@link ConnectionToServer}. + * @param joinAcceptance The message to operate on. + * @param connectionToServer The context to operate on. + */ + public ServerFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) { + this.joinAcceptance = joinAcceptance; + this.connectionToServer = connectionToServer; + } + + + + @Override + public void execute() { + + connectionToServer.setJoinAcceptance(joinAcceptance); + + connectionToServer.setConnectionState(ConnectionToServerState.DECLINED); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java new file mode 100644 index 00000000..d4668ef7 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java @@ -0,0 +1,47 @@ +package visualiser.Commands.IncomingHeartBeatCommands; + +import mock.model.commandFactory.Command; +import network.Messages.HeartBeat; +import network.Messages.JoinAcceptance; +import visualiser.enums.ConnectionToServerState; +import visualiser.network.ConnectionToServer; +import visualiser.network.IncomingHeartBeatService; + + +/** + * Command created when a {@link HeartBeat} message is received. + */ +public class IncomingHeartBeatCommand implements Command { + + /** + * The message to operate on. + */ + private HeartBeat heartBeat; + + /** + * The context to operate on. + */ + private IncomingHeartBeatService incomingHeartBeatService; + + + /** + * Creates a new {@link IncomingHeartBeatCommand}, which operates on a given {@link IncomingHeartBeatService}. + * @param heartBeat The message to operate on. + * @param incomingHeartBeatService The context to operate on. + */ + public IncomingHeartBeatCommand(HeartBeat heartBeat, IncomingHeartBeatService incomingHeartBeatService) { + this.heartBeat = heartBeat; + this.incomingHeartBeatService = incomingHeartBeatService; + } + + + + @Override + public void execute() { + + incomingHeartBeatService.setLastHeartBeatSeqNum(heartBeat.getSequenceNumber()); + + incomingHeartBeatService.setLastHeartbeatTime(System.currentTimeMillis()); + + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java new file mode 100644 index 00000000..bab68c42 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java @@ -0,0 +1,33 @@ +package visualiser.Commands.IncomingHeartBeatCommands; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.HeartBeat; +import visualiser.network.IncomingHeartBeatService; + +/** + * Factory to create IncomingHeartBeatService commands. + */ +public class IncomingHeartBeatCommandFactory { + + /** + * Generates a command on an IncomingHeartBeatService. + * @param message The message to turn into a command. + * @param incomingHeartBeatService The context for the command to operate on. + * @return The command to execute the given action. + * @throws CommandConstructionException Thrown if the command cannot be constructed. + */ + public static Command create(AC35Data message, IncomingHeartBeatService incomingHeartBeatService) throws CommandConstructionException { + + if (!(message instanceof HeartBeat)) { + throw new CommandConstructionException("Message: " + message + " is not a HeartBeat message."); + } + + HeartBeat heartBeat = (HeartBeat) message; + + return new IncomingHeartBeatCommand(heartBeat, incomingHeartBeatService); + } + +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java index 900e43b0..c801f16a 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java @@ -3,9 +3,8 @@ package visualiser.Controllers; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.layout.AnchorPane; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; -import visualiser.model.ServerConnection; +import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java index 0a5fb33a..2e1a811e 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java @@ -17,13 +17,12 @@ import javafx.scene.layout.StackPane; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.model.Leg; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; import visualiser.model.*; +import visualiser.network.ServerConnection; -import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; import java.util.logging.Level; diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java index 82ac39f6..b1c17bb1 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java @@ -11,6 +11,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.paint.Color; import network.Messages.Enums.RaceStatusEnum; +import network.Messages.Enums.RequestToJoinEnum; import network.Messages.LatestMessages; import shared.dataInput.*; import shared.enums.XMLFileType; @@ -18,9 +19,8 @@ import shared.exceptions.InvalidBoatDataException; import shared.exceptions.InvalidRaceDataException; import shared.exceptions.InvalidRegattaDataException; import shared.exceptions.XMLReaderException; -import visualiser.app.VisualiserInput; import visualiser.gameController.ControllerClient; -import visualiser.model.ServerConnection; +import visualiser.network.ServerConnection; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRace; @@ -327,7 +327,7 @@ public class StartController extends Controller implements Observer { try { LatestMessages latestMessages = new LatestMessages(); - this.serverConnection = new ServerConnection(socket, latestMessages); + this.serverConnection = new ServerConnection(socket, latestMessages, RequestToJoinEnum.PARTICIPANT); this.controllerClient = serverConnection.getControllerClient(); diff --git a/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java new file mode 100644 index 00000000..22e1e30f --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java @@ -0,0 +1,96 @@ +package visualiser.enums; + +import java.util.HashMap; +import java.util.Map; + +/** + * The states in which a connection from a client to a server may have. + */ +public enum ConnectionToServerState { + + UNKNOWN(0), + + /** + * We're waiting for the server to complete the joining handshake. + * See {@link network.Messages.RequestToJoin} and {@link network.Messages.JoinAcceptance}. + */ + REQUEST_SENT(1), + + /** + * The client has receved a {@link network.Messages.JoinAcceptance} from the server. + */ + RESPONSE_RECEIVED(2), + + /** + * The server has completed the handshake, and is connected. + * That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}. + */ + CONNECTED(3), + + /** + * The server has timed out, or the connection has been interrupted. + */ + TIMED_OUT(4), + + /** + * The client's connection has been declined. + */ + DECLINED(5); + + + + + private byte value; + + /** + * Ctor. Creates a ConnectionToServerState from a given primitive integer value, cast to a byte. + * @param value Integer, which is cast to byte, to construct from. + */ + private ConnectionToServerState(int value) { + this.value = (byte) value; + } + + /** + * Returns the primitive value of the enum. + * @return Primitive value of the enum. + */ + public byte getValue() { + return value; + } + + + /** + * Stores a mapping between Byte values and ConnectionToServerState values. + */ + private static final Map byteToStateMap = new HashMap<>(); + + + /* + Static initialization block. Initializes the byteToStateMap. + */ + static { + for (ConnectionToServerState type : ConnectionToServerState.values()) { + ConnectionToServerState.byteToStateMap.put(type.value, type); + } + } + + + /** + * Returns the enumeration value which corresponds to a given byte value. + * @param connectionState Byte value to convert to a ConnectionToServerState value. + * @return The ConnectionToServerState value which corresponds to the given byte value. + */ + public static ConnectionToServerState fromByte(byte connectionState) { + //Gets the corresponding ConnectionToServerState from the map. + ConnectionToServerState type = ConnectionToServerState.byteToStateMap.get(connectionState); + + if (type == null) { + //If the byte value wasn't found, return the UNKNOWN ConnectionToServerState. + return ConnectionToServerState.UNKNOWN; + } else { + //Otherwise, return the ConnectionToServerState. + return type; + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java b/racevisionGame/src/main/java/visualiser/model/ServerConnection.java deleted file mode 100644 index 742ab968..00000000 --- a/racevisionGame/src/main/java/visualiser/model/ServerConnection.java +++ /dev/null @@ -1,243 +0,0 @@ -package visualiser.model; - - -import mock.enums.ConnectionStateEnum; -import network.Messages.AC35Data; -import network.Messages.Enums.JoinAcceptanceEnum; -import network.Messages.Enums.MessageType; -import network.Messages.Enums.RequestToJoinEnum; -import network.Messages.JoinAcceptance; -import network.Messages.LatestMessages; -import network.Messages.RequestToJoin; -import network.StreamRelated.MessageDeserialiser; -import network.StreamRelated.MessageSerialiser; -import shared.exceptions.HandshakeException; -import visualiser.app.VisualiserInput; -import visualiser.gameController.ControllerClient; - -import java.io.IOException; -import java.net.Socket; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. - */ -public class ServerConnection implements Runnable { - - /** - * The socket for the connection to server. - */ - private Socket socket; - - /** - * The source ID that has been allocated to the client. - */ - private int allocatedSourceID = 0; - - - /** - * Latest snapshot of the race, received from the server. - */ - private LatestMessages latestMessages; - - - /** - * Used to convert incoming messages into a race snapshot. - */ - private VisualiserInput visualiserInput; - - /** - * Used to send client input to server. - */ - private ControllerClient controllerClient; - - - /** - * Used to write messages to socket. - */ - private MessageSerialiser messageSerialiser; - - /** - * Stores messages to write to socket. - */ - private BlockingQueue outputQueue; - - /** - * Used to read messages from socket. - */ - private MessageDeserialiser messageDeserialiser; - - /** - * Stores messages read from socket. - */ - private BlockingQueue inputQueue; - - /** - * The state of the connection to the client. - */ - private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN; - - - - - - - /** - * Creates a server connection, using a given socket. - * @param socket The socket which connects to the client. - * @param latestMessages Latest race snapshot to send to client. - * @throws IOException Thrown if there is a problem with the client socket. - */ - public ServerConnection(Socket socket, LatestMessages latestMessages) throws IOException { - this.socket = socket; - this.latestMessages = latestMessages; - - this.outputQueue = new LinkedBlockingQueue<>(); - this.inputQueue = new LinkedBlockingQueue<>(); - - - this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); - this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); - - new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start(); - new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start(); - - - this.controllerClient = new ControllerClient(outputQueue); - - } - - - - @Override - public void run() { - try { - handshake(); - - } catch (HandshakeException e) { - Logger.getGlobal().log(Level.WARNING, "Server handshake failed.", e); - Thread.currentThread().interrupt(); - return; - } - - } - - - /** - * Initiates the handshake with the server. - * @throws HandshakeException Thrown if something goes wrong with the handshake. - */ - private void handshake() throws HandshakeException { - - //This function is a bit messy, and could probably be refactored a bit. - - connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE; - - - sendRequestToJoinMessage(RequestToJoinEnum.PARTICIPANT); - - - JoinAcceptance joinAcceptance = waitForJoinAcceptance(); - - - //If we join successfully... - if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) { - - this.allocatedSourceID = joinAcceptance.getSourceID(); - - - //new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start(); - - } - - this.visualiserInput = new VisualiserInput(latestMessages, inputQueue); - new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start(); - - - connectionState = ConnectionStateEnum.CONNECTED; - - } - - - /** - * Waits until the server sends a {@link JoinAcceptance} message, and returns it. - * @return The {@link JoinAcceptance} message. - * @throws HandshakeException Thrown if we get interrupted while waiting. - */ - private JoinAcceptance waitForJoinAcceptance() throws HandshakeException { - - try { - - - while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) { - - AC35Data message = inputQueue.take(); - - //We need to wait until they actually send a join request. - if (message.getType() == MessageType.JOIN_ACCEPTANCE) { - return (JoinAcceptance) message; - } - - } - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e); - - } - - - throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState); - - } - - - /** - * Sends the server a {@link RequestToJoin} message. - * @param requestType The type of request to send - * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue. - */ - private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws HandshakeException { - - //Send them the source ID. - RequestToJoin requestToJoin = new RequestToJoin(requestType); - - try { - outputQueue.put(requestToJoin); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing RequestToJoin message on outgoing message queue.", e); - } - - } - - - /** - * Determines whether or not this connection is still alive. - * This is based off whether the {@link MessageDeserialiser} is still alive. - * @return True if it is alive, false otherwise. - */ - public boolean isAlive() { - return messageDeserialiser.isRunning(); - } - - - /** - * Returns the controller client, which writes BoatAction messages to the outgoing queue. - * @return The ControllerClient. - */ - public ControllerClient getControllerClient() { - return controllerClient; - } - - /** - * Returns the source ID that has been allocated to the client. - * @return Source ID allocated to the client. 0 if it hasn't been allocated. - */ - public int getAllocatedSourceID() { - return allocatedSourceID; - } -} diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java similarity index 82% rename from racevisionGame/src/main/java/visualiser/app/VisualiserInput.java rename to racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java index 0579b623..15adc13b 100644 --- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java @@ -1,4 +1,4 @@ -package visualiser.app; +package visualiser.model; import network.Messages.*; import shared.model.RunnableWithFramePeriod; @@ -11,16 +11,7 @@ import java.util.logging.Logger; * TCP client which receives packets/messages from a race data source * (e.g., mock source, official source), and exposes them to any observers. */ -public class VisualiserInput implements RunnableWithFramePeriod { - - /** - * Timestamp of the last heartbeat. - */ - private long lastHeartbeatTime = -1; - /** - * Sequence number of the last heartbeat. - */ - private long lastHeartbeatSequenceNum = -1; +public class VisualiserRaceController implements RunnableWithFramePeriod { /** @@ -42,11 +33,9 @@ public class VisualiserInput implements RunnableWithFramePeriod { * @param latestMessages Object to place messages in. * @param incomingMessages The incoming queue of messages. */ - public VisualiserInput(LatestMessages latestMessages, BlockingQueue incomingMessages) { + public VisualiserRaceController(LatestMessages latestMessages, BlockingQueue incomingMessages) { this.latestMessages = latestMessages; this.incomingMessages = incomingMessages; - - this.lastHeartbeatTime = System.currentTimeMillis(); } @@ -59,15 +48,7 @@ public class VisualiserInput implements RunnableWithFramePeriod { return latestMessages; } - /** - * Calculates the time since last heartbeat, in milliseconds. - * - * @return Time since last heartbeat, in milliseconds.. - */ - private double timeSinceHeartbeat() { - long now = System.currentTimeMillis(); - return (now - lastHeartbeatTime); - } + @Override @@ -90,19 +71,6 @@ public class VisualiserInput implements RunnableWithFramePeriod { //Checks which message is being received and does what is needed for that message. switch (message.getType()) { - //Heartbeat. - case HEARTBEAT: { - HeartBeat heartBeat = (HeartBeat) message; - - //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time. - if (heartBeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) { - lastHeartbeatTime = System.currentTimeMillis(); - lastHeartbeatSequenceNum = heartBeat.getSequenceNumber(); - //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum); - } - - break; - } //RaceStatus. case RACESTATUS: { diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java new file mode 100644 index 00000000..4d22fc23 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java @@ -0,0 +1,157 @@ +package visualiser.network; + + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.RequestToJoin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import shared.model.RunnableWithFramePeriod; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class tracks the state of the connection to a server. + */ +public class ConnectionToServer implements RunnableWithFramePeriod { + + + /** + * The state of the connection to the client. + */ + private ConnectionToServerState connectionState = ConnectionToServerState.UNKNOWN; + + + /** + * The type of join request to make to server. + */ + private RequestToJoinEnum requestType; + + + /** + * The queue to place outgoing messages on. + */ + private BlockingQueue outgoingMessages; + + + + /** + * The {@link JoinAcceptance} message that has been received, if any. + */ + @Nullable + private JoinAcceptance joinAcceptance; + + + /** + * The incoming commands to execute. + */ + private BlockingQueue incomingCommands; + + + + /** + * Constructs a ConnectionToServer with a given state. + * @param connectionState The state of the connection. + * @param requestType The type of join request to make to server. + * @param incomingCommands The queue of commands to execute. + * @param outgoingMessages The queue to place outgoing messages on. + */ + public ConnectionToServer(ConnectionToServerState connectionState, RequestToJoinEnum requestType, BlockingQueue incomingCommands, BlockingQueue outgoingMessages) { + this.connectionState = connectionState; + this.requestType = requestType; + this.incomingCommands = incomingCommands; + this.outgoingMessages = outgoingMessages; + } + + + /** + * Returns the state of this connection. + * @return The state of this connection. + */ + public ConnectionToServerState getConnectionState() { + return connectionState; + } + + /** + * Sets the state of this connection. + * @param connectionState The new state of this connection. + */ + public void setConnectionState(ConnectionToServerState connectionState) { + this.connectionState = connectionState; + } + + + /** + * Returns the {@link JoinAcceptance} message received from the server, if any. + * @return The JoinAcceptance message from server. Null if no response from server. + */ + @Nullable + public JoinAcceptance getJoinAcceptance() { + return joinAcceptance; + } + + /** + * Sets the {@link JoinAcceptance} message received from the server, if any. + * @param joinAcceptance The new JoinAcceptance message from server. + */ + public void setJoinAcceptance(@NotNull JoinAcceptance joinAcceptance) { + this.joinAcceptance = joinAcceptance; + } + + + + @Override + public void run() { + + try { + sendRequestToJoinMessage(requestType); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending RequestToJoin.", e); + Thread.currentThread().interrupt(); + + } + + while (!Thread.interrupted()) { + + try { + Command command = incomingCommands.take(); + command.execute(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e); + Thread.currentThread().interrupt(); + } + + } + + //If we get interrupted, we consider the connection to have timed-out. + connectionState = ConnectionToServerState.TIMED_OUT; + + } + + + + /** + * Sends the server a {@link RequestToJoin} message. + * @param requestType The type of request to send + * @throws InterruptedException Thrown if the thread is interrupted while placing message on the outgoing message queue. + */ + private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws InterruptedException { + + //Send them the source ID. + RequestToJoin requestToJoin = new RequestToJoin(requestType); + + outgoingMessages.put(requestToJoin); + + connectionState = ConnectionToServerState.REQUEST_SENT; + } + + + +} diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java new file mode 100644 index 00000000..467bbd54 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java @@ -0,0 +1,78 @@ +package visualiser.network; + + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; +import visualiser.Commands.ConnectionToServerCommands.ConnectionToServerCommandFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * The controller for connection related messages, coming from the server to the client. + */ +public class ConnectionToServerController implements RunnableWithFramePeriod { + + + /** + * The incoming queue of messages to act on. + */ + private BlockingQueue incomingMessages; + + + /** + * The connection we are acting on. + */ + private ConnectionToServer connectionToServer; + + + /** + * The queue to place commands on. + */ + private BlockingQueue outgoingCommands; + + + + /** + * Constructs a {@link ConnectionToServer} controller with the given parameters. + * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue. + * @param incomingMessages The message queue to read from. + * @param connectionToServer The ConnectionToServer (context) to act on. + * @param outgoingCommands The queue to place outgoing commands on. + */ + public ConnectionToServerController(BlockingQueue incomingMessages, ConnectionToServer connectionToServer, BlockingQueue outgoingCommands) { + this.incomingMessages = incomingMessages; + this.connectionToServer = connectionToServer; + this.outgoingCommands = outgoingCommands; + } + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + AC35Data message = incomingMessages.take(); + Command command = ConnectionToServerCommandFactory.create(message, connectionToServer); + outgoingCommands.put(command); + + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "ConnectionToServerController: " + this + " could not create command from message.", e); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "ConnectionToServerController: " + this + " was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + + } + + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java new file mode 100644 index 00000000..739a3e64 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java @@ -0,0 +1,78 @@ +package visualiser.network; + +import mock.exceptions.CommandConstructionException; +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import shared.model.RunnableWithFramePeriod; +import visualiser.Commands.IncomingHeartBeatCommands.IncomingHeartBeatCommandFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * The controller for heartbeat related messages, coming from the server to the client. + */ +public class IncomingHeartBeatController implements RunnableWithFramePeriod { + + + /** + * The incoming queue of messages to act on. + */ + private BlockingQueue incomingMessages; + + + /** + * The heart beat service we are acting on. + */ + private IncomingHeartBeatService incomingHeartBeatService; + + + /** + * The queue to place commands on. + */ + private BlockingQueue outgoingCommands; + + + + /** + * Constructs a {@link IncomingHeartBeatService} controller with the given parameters. + * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue. + * @param incomingMessages The message queue to read from. + * @param incomingHeartBeatService The IncomingHeartBeatService (context) to act on. + * @param outgoingCommands The queue to place outgoing commands on. + */ + public IncomingHeartBeatController(BlockingQueue incomingMessages, IncomingHeartBeatService incomingHeartBeatService, BlockingQueue outgoingCommands) { + this.incomingMessages = incomingMessages; + this.incomingHeartBeatService = incomingHeartBeatService; + this.outgoingCommands = outgoingCommands; + } + + + + @Override + public void run() { + + while (!Thread.interrupted()) { + + try { + AC35Data message = incomingMessages.take(); + Command command = IncomingHeartBeatCommandFactory.create(message, incomingHeartBeatService); + outgoingCommands.put(command); + + } catch (CommandConstructionException e) { + Logger.getGlobal().log(Level.WARNING, "IncomingHeartBeatController: " + this + " could not create command from message.", e); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatController: " + this + " was interrupted on thread: " + Thread.currentThread(), e); + Thread.currentThread().interrupt(); + + } + + + } + + } + +} diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java new file mode 100644 index 00000000..e7c5b159 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java @@ -0,0 +1,109 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import shared.model.RunnableWithFramePeriod; + +import java.util.concurrent.BlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * Tracks the heart beat status of a connection. + */ +public class IncomingHeartBeatService implements RunnableWithFramePeriod { + + + /** + * Timestamp of the last sent heartbeat message. + */ + private long lastHeartbeatTime; + + + /** + * Sequence number for heartbeat messages. + */ + private long lastHeartBeatSeqNum; + + + /** + * The incoming commands to execute. + */ + private BlockingQueue incomingCommands; + + + + /** + * Creates an {@link IncomingHeartBeatService} which executes commands from a given queue. + * @param incomingCommands Queue to read and execute commands from. + */ + public IncomingHeartBeatService(BlockingQueue incomingCommands) { + this.incomingCommands = incomingCommands; + + this.lastHeartbeatTime = System.currentTimeMillis(); + this.lastHeartBeatSeqNum = -1; + } + + + /** + * Sets the last heart beat time to a given value. + * @param lastHeartbeatTime Timestamp of heartbeat. + */ + public void setLastHeartbeatTime(long lastHeartbeatTime) { + this.lastHeartbeatTime = lastHeartbeatTime; + } + + /** + * Sets the last heart beat sequence number to a given value. + * @param lastHeartBeatSeqNum Sequence number of heartbeat. + */ + public void setLastHeartBeatSeqNum(long lastHeartBeatSeqNum) { + this.lastHeartBeatSeqNum = lastHeartBeatSeqNum; + } + + + + /** + * Calculates the time since last heartbeat, in milliseconds. + * + * @return Time since last heartbeat, in milliseconds.. + */ + private long timeSinceHeartbeat() { + long now = System.currentTimeMillis(); + return (now - lastHeartbeatTime); + } + + + /** + * Returns whether or not the heartBeat service considers the connection "alive". + * Going 10,000ms without receiving a heartBeat means that the connection is "dead". + * @return True if alive, false if dead. + */ + public boolean isAlive() { + long heartBeatPeriod = 10000; + + return (timeSinceHeartbeat() < heartBeatPeriod); + } + + + + @Override + public void run() { + + + while (!Thread.interrupted()) { + + try { + Command command = incomingCommands.take(); + command.execute(); + + } catch (InterruptedException e) { + Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatService: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e); + Thread.currentThread().interrupt(); + + } + + } + + } +} diff --git a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java new file mode 100644 index 00000000..35b25727 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java @@ -0,0 +1,459 @@ +package visualiser.network; + + +import mock.model.commandFactory.Command; +import network.MessageRouters.MessageRouter; +import network.Messages.AC35Data; +import network.Messages.Enums.MessageType; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import network.Messages.LatestMessages; +import network.StreamRelated.MessageDeserialiser; +import network.StreamRelated.MessageSerialiser; +import shared.model.RunnableWithFramePeriod; +import visualiser.model.VisualiserRaceController; +import visualiser.enums.ConnectionToServerState; +import visualiser.gameController.ControllerClient; + +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient. + */ +public class ServerConnection implements RunnableWithFramePeriod { + + /** + * The socket for the connection to server. + */ + private Socket socket; + + /** + * The source ID that has been allocated to the client. + */ + private int allocatedSourceID = 0; + + + /** + * Latest snapshot of the race, received from the server. + */ + private LatestMessages latestMessages; + + + /** + * Used to send client input to server. + */ + private ControllerClient controllerClient; + + + + /** + * Used to write messages to socket. + */ + private MessageSerialiser messageSerialiser; + /** + * The thread {@link #messageSerialiser} runs on. + */ + private Thread messageSerialiserThread; + + /** + * Used to read messages from socket. + */ + private MessageDeserialiser messageDeserialiser; + /** + * The thread {@link #messageDeserialiser} runs on. + */ + private Thread messageDeserialiserThread; + + + + + /** + * Router to route messages to correct queue. + */ + private MessageRouter messageRouter; + /** + * The thread {@link #messageRouter} runs on. + */ + private Thread messageRouterThread; + + + + /** + * The state of the connection to the client. + */ + private ConnectionToServer connectionToServer; + /** + * The thread {@link #connectionToServer} runs on. + */ + private Thread connectionToServerThread; + + /** + * The controller which handles JoinAcceptance messages. + */ + private ConnectionToServerController connectionToServerController; + /** + * The thread {@link #connectionToServerController} runs on. + */ + private Thread connectionToServerControllerThread; + + + + /** + * Tracks the heartBeat status of the connection. + */ + private IncomingHeartBeatService heartBeatService; + /** + * The thread {@link #heartBeatService} runs on. + */ + private Thread heartBeatServiceThread; + + /** + * Tracks the heartBeat status of the connection. + */ + private IncomingHeartBeatController heartBeatController; + /** + * The thread {@link #heartBeatController} runs on. + */ + private Thread heartBeatControllerThread; + + + + /** + * Used to convert incoming messages into a race snapshot. + */ + private VisualiserRaceController visualiserRaceController; + /** + * The thread {@link #visualiserRaceController} runs on. + */ + private Thread visualiserInputThread; + + + + + /** + * Creates a server connection, using a given socket. + * @param socket The socket which connects to the client. + * @param latestMessages Latest race snapshot to send to client. + * @param requestType The type of join request to make. + * @throws IOException Thrown if there is a problem with the client socket. + */ + public ServerConnection(Socket socket, LatestMessages latestMessages, RequestToJoinEnum requestType) throws IOException { + this.socket = socket; + this.latestMessages = latestMessages; + + createMessageSerialiser(socket); + createMessageDeserialiser(socket); + + createRouter(messageDeserialiser.getMessagesRead()); + + createConnectionToServer(requestType); + + + messageRouterThread.start(); + + + this.controllerClient = new ControllerClient(messageRouter.getIncomingMessageQueue()); + + } + + + + /** + * Creates this connection's {@link MessageRouter}, and gives it a queue to read from. + * Does not start {@link #messageRouterThread}. Start it after setting up any initial routes. + * @param inputQueue Queue for the MessageRouter to read from. + */ + private void createRouter(BlockingQueue inputQueue) { + this.messageRouter = new MessageRouter(inputQueue); + + this.messageRouterThread = new Thread(messageRouter, "ServerConnection()->MessageRouter thread " + messageRouter); + + //Unrouted messages get sent back to the router. Kind of ugly, but we do this to ensure that no messages are lost while initializing (e.g., XML message being received before setting up the route for it). + messageRouter.addDefaultRoute(messageRouter.getIncomingMessageQueue()); + } + + + /** + * Creates the {@link #connectionToServer} and {@link #connectionToServerController}, and starts their threads. + * @param requestType The type of join request to make to server. + */ + private void createConnectionToServer(RequestToJoinEnum requestType) { + + //ConnectionToServer executes these commands. + BlockingQueue commands = new LinkedBlockingQueue<>(); + this.connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, requestType, commands, messageRouter.getIncomingMessageQueue()); + + //ConnectionToServerController receives messages, and places commands on the above command queue. + BlockingQueue incomingJoinMessages = new LinkedBlockingQueue<>(); + this.connectionToServerController = new ConnectionToServerController(incomingJoinMessages, connectionToServer, commands); + + //Route JoinAcceptance messages to the controller, and RequestToJoin to the socket. + this.messageRouter.addRoute(MessageType.JOIN_ACCEPTANCE, incomingJoinMessages); + this.messageRouter.addRoute(MessageType.REQUEST_TO_JOIN, messageSerialiser.getMessagesToSend()); + + + //Start the above on new threads. + this.connectionToServerThread = new Thread(connectionToServer, "ServerConnection()->ConnectionToServer thread " + connectionToServer); + this.connectionToServerThread.start(); + + this.connectionToServerControllerThread = new Thread(connectionToServerController,"ServerConnection()->ConnectionToServerController thread " + connectionToServerController); + this.connectionToServerControllerThread.start(); + } + + + /** + * Creates the {@link #messageSerialiser} and starts its thread. + * @param socket The socket to write to. + * @throws IOException Thrown if we cannot get an outputStream from the socket + */ + private void createMessageSerialiser(Socket socket) throws IOException { + BlockingQueue outputQueue = new LinkedBlockingQueue<>(); + this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue); + + this.messageSerialiserThread = new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser); + this.messageSerialiserThread.start(); + } + + /** + * Creates the {@link #messageDeserialiser} and starts its thread. + * @param socket The socket to read from. + * @throws IOException Thrown if we cannot get an inputStream from the socket + */ + private void createMessageDeserialiser(Socket socket) throws IOException { + BlockingQueue inputQueue = new LinkedBlockingQueue<>(); + this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue); + + this.messageDeserialiserThread = new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser); + this.messageDeserialiserThread.start(); + } + + + /** + * Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts its thread. + */ + private void createHeartBeatService() { + + //IncomingHeartBeatService executes these commands. + BlockingQueue commands = new LinkedBlockingQueue<>(); + this.heartBeatService = new IncomingHeartBeatService(commands); + + //IncomingHeartBeatController receives messages, and places commands on the above command queue. + BlockingQueue incomingHeartBeatMessages = new LinkedBlockingQueue<>(); + this.heartBeatController = new IncomingHeartBeatController(incomingHeartBeatMessages, heartBeatService, commands); + + //Route HeartBeat messages to the controller. + this.messageRouter.addRoute(MessageType.HEARTBEAT, incomingHeartBeatMessages); + + + //Start the above on new threads. + this.heartBeatServiceThread = new Thread(heartBeatService, "ServerConnection()->IncomingHeartBeatService thread " + connectionToServer); + this.heartBeatServiceThread.start(); + + this.heartBeatControllerThread = new Thread(heartBeatController,"ServerConnection()->IncomingHeartBeatController thread " + connectionToServerController); + this.heartBeatControllerThread.start(); + + } + + + + private void createVisualiserRace() { + + BlockingQueue incomingMessages = new LinkedBlockingQueue<>(); + + this.visualiserRaceController = new VisualiserRaceController(latestMessages, incomingMessages); + this.visualiserInputThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserInput thread " + visualiserRaceController); + this.visualiserInputThread.start(); + + //Routes. + this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages); + this.messageRouter.addRoute(MessageType.RACESTATUS, incomingMessages); + this.messageRouter.addRoute(MessageType.RACESTARTSTATUS, incomingMessages); + this.messageRouter.addRoute(MessageType.AVGWIND, incomingMessages); + this.messageRouter.addRoute(MessageType.COURSEWIND, incomingMessages); + this.messageRouter.addRoute(MessageType.CHATTERTEXT, incomingMessages); + this.messageRouter.addRoute(MessageType.DISPLAYTEXTMESSAGE, incomingMessages); + this.messageRouter.addRoute(MessageType.YACHTACTIONCODE, incomingMessages); + this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages); + this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages); + this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages); + this.messageRouter.removeDefaultRoute(); + + //TODO create VisualiserRace here or somewhere else? + + + } + + + + private void createPlayerInputController() { + + this.messageRouter.addRoute(MessageType.BOATACTION, messageSerialiser.getMessagesToSend()); + //TODO routes + } + + + + + @Override + public void run() { + + //Monitor the connection state. + + long previousFrameTime = System.currentTimeMillis(); + + while (!Thread.interrupted()) { + + long currentFrameTime = System.currentTimeMillis(); + waitForFramePeriod(previousFrameTime, currentFrameTime, 100); + previousFrameTime = currentFrameTime; + + + ConnectionToServerState state = connectionToServer.getConnectionState(); + + switch (state) { + + case CONNECTED: + connected(); + break; + + case DECLINED: + declined(); + break; + + case TIMED_OUT: + timedOut(); + break; + + } + + } + + } + + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#CONNECTED}. + */ + private void connected() { + + JoinAcceptance joinAcceptance = connectionToServer.getJoinAcceptance(); + + allocatedSourceID = joinAcceptance.getSourceID(); + + + createHeartBeatService(); + + createVisualiserRace(); + + createPlayerInputController(); + + + //We interrupt as this thread's run() isn't needed anymore. + Thread.currentThread().interrupt(); + } + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#DECLINED}. + */ + private void declined() { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection was declined."); + + terminate(); + + Thread.currentThread().interrupt(); + } + + + /** + * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#TIMED_OUT}. + */ + private void timedOut() { + Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection timed out."); + + terminate(); + + Thread.currentThread().interrupt(); + } + + + + + /** + * Determines whether or not this connection is still alive. + * This is based off whether the {@link #messageDeserialiser}, {@link #messageSerialiser}, and {@link #heartBeatService} are alive. + * @return True if it is alive, false otherwise. + */ + public boolean isAlive() { + return messageDeserialiser.isRunning() && messageSerialiser.isRunning() && heartBeatService.isAlive(); + } + + + /** + * Returns the controller client, which writes BoatAction messages to the outgoing queue. + * @return The ControllerClient. + */ + public ControllerClient getControllerClient() { + return controllerClient; + } + + /** + * Returns the source ID that has been allocated to the client. + * @return Source ID allocated to the client. 0 if it hasn't been allocated. + */ + public int getAllocatedSourceID() { + return allocatedSourceID; + } + + + /** + * Terminates the connection and any running threads. + */ + public void terminate() { + + if (this.messageRouterThread != null) { + this.messageRouterThread.interrupt(); + } + + + if (this.messageSerialiserThread != null) { + this.messageSerialiserThread.interrupt(); + } + if (this.messageDeserialiserThread != null) { + this.messageDeserialiserThread.interrupt(); + } + + + + if (this.connectionToServerThread != null) { + this.connectionToServerThread.interrupt(); + } + if (this.connectionToServerControllerThread != null) { + this.connectionToServerControllerThread.interrupt(); + } + + + if (this.heartBeatServiceThread != null) { + this.heartBeatServiceThread.interrupt(); + } + if (this.heartBeatControllerThread != null) { + this.heartBeatControllerThread.interrupt(); + } + + + if (this.visualiserInputThread != null) { + this.visualiserInputThread.interrupt(); + } + //TODO visualiser race? + //TODO input controller? + + } + + +} diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java new file mode 100644 index 00000000..fae94d1b --- /dev/null +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java @@ -0,0 +1,150 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import org.junit.Before; +import org.junit.Test; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; +import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.Assert.*; + +/** + * Tests the {@link ConnectionToServer} class with a Participant request, and how it reacts to various commands. + */ +public class ConnectionToServerParticipantTest { + + private ConnectionToServer connectionToServer; + private Thread connectionToServerThread; + + private BlockingQueue outgoingMessages; + private BlockingQueue incomingCommands; + + + @Before + public void setUp() throws Exception { + + incomingCommands = new LinkedBlockingQueue<>(); + outgoingMessages = new LinkedBlockingQueue<>(); + + connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.PARTICIPANT, incomingCommands, outgoingMessages); + connectionToServerThread = new Thread(connectionToServer); + connectionToServerThread.start(); + + } + + + /** + * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state. + * @throws Exception On error. + */ + @Test + public void expectRequestSent() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); + } + + + /** + * When the connection to server thread is interrupted, it is expected the connection state will become TimedOut. + * @throws Exception On error. + */ + @Test + public void interruptTimedOut() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + connectionToServerThread.interrupt(); + connectionToServerThread.join(); + + assertEquals(ConnectionToServerState.TIMED_OUT, connectionToServer.getConnectionState()); + } + + + /** + * Sends a join successful command. Expects that the connection becomes Connected. + * @throws Exception On error. + */ + @Test + public void sendJoinSuccessCommand() throws Exception { + int sourceID = 123; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertNotEquals(0, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + + + } + + + /** + * Sends a participants full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendRaceParticipantsFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, sourceID); + + Command command = new RaceParticipantsFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + + /** + * Sends a server full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendServerFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID); + + Command command = new ServerFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + +} + + diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java new file mode 100644 index 00000000..8b69c861 --- /dev/null +++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java @@ -0,0 +1,109 @@ +package visualiser.network; + +import mock.model.commandFactory.Command; +import network.Messages.AC35Data; +import network.Messages.Enums.JoinAcceptanceEnum; +import network.Messages.Enums.RequestToJoinEnum; +import network.Messages.JoinAcceptance; +import org.junit.Before; +import org.junit.Test; +import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand; +import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand; +import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand; +import visualiser.enums.ConnectionToServerState; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.junit.Assert.*; + +/** + * Tests the {@link ConnectionToServer} class with a Spectator request, and how it reacts to various commands. + */ +public class ConnectionToServerSpectatorTest { + + private ConnectionToServer connectionToServer; + private Thread connectionToServerThread; + + private BlockingQueue outgoingMessages; + private BlockingQueue incomingCommands; + + + @Before + public void setUp() throws Exception { + + incomingCommands = new LinkedBlockingQueue<>(); + outgoingMessages = new LinkedBlockingQueue<>(); + + connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.SPECTATOR, incomingCommands, outgoingMessages); + connectionToServerThread = new Thread(connectionToServer); + connectionToServerThread.start(); + + } + + + /** + * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state. + * @throws Exception On error. + */ + @Test + public void expectRequestSent() throws Exception { + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState()); + } + + + /** + * Sends a join successful command. Expects that the connection becomes Connected. + * @throws Exception On error. + */ + @Test + public void sendJoinSuccessCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID); + + Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + + + } + + + /** + * Sends a server full command. Expects that the connection becomes Declined. + * @throws Exception On error. + */ + @Test + public void sendServerFullCommand() throws Exception { + int sourceID = 0; + JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID); + + Command command = new ServerFullCommand(joinAcceptance, connectionToServer); + + incomingCommands.put(command); + + //Need to wait for connection thread to execute commands. + Thread.sleep(20); + + assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState()); + assertTrue(connectionToServer.getJoinAcceptance() != null); + assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID()); + assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType()); + } + + +} + +