WIP. Probably need to cherry pick stuff out of here.

Added ClientConnection and server-side handshake.
Added MessageSerialiser and Deserialiser.
#story[1095]
This commit is contained in:
fjc40
2017-08-10 12:13:40 +12:00
parent 3ec87582d3
commit 7cc39abe57
35 changed files with 1837 additions and 793 deletions
@@ -1,21 +1,22 @@
package mock.app;
import mock.enums.ConnectionStateEnum;
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.RaceSnapshot;
import network.Messages.XMLMessage;
import org.mockito.Mock;
import visualiser.gameController.ControllerServer;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Connection acceptor for multiple clients
@@ -31,10 +32,31 @@ public class ConnectionAcceptor implements Runnable {
* Socket used to listen for clients on.
*/
private ServerSocket serverSocket;
//mock outputs
private ArrayBlockingQueue<MockOutput> mockOutputList = new ArrayBlockingQueue<>(16, true);
//latest messages
/**
* List of client connections.
*/
private ArrayBlockingQueue<ClientConnection> 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
@@ -47,14 +69,20 @@ public class ConnectionAcceptor implements Runnable {
/**
* 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.
* @throws IOException if a server socket cannot be instantiated.
*/
public ConnectionAcceptor(LatestMessages latestMessages) throws IOException {
public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator) throws IOException {
this.latestMessages = latestMessages;
this.compositeCommand = compositeCommand;
this.sourceIdAllocator = sourceIdAllocator;
this.serverSocket = new ServerSocket(serverPort);
CheckClientConnection checkClientConnection = new CheckClientConnection(mockOutputList);
CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections);
new Thread(checkClientConnection, "ConnectionAcceptor()->CheckClientConnection thread").start();
}
public String getAddress() throws UnknownHostException {
@@ -71,28 +99,26 @@ public class ConnectionAcceptor implements Runnable {
@Override
public void run() {
while(mockOutputList.remainingCapacity() > 0) {
while(clientConnections.remainingCapacity() > 0) {
try {
System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE
Socket mockSocket = serverSocket.accept();
//TODO at this point we need to assign the connection a boat source ID, if they requested to participate.
DataOutputStream outToVisualiser = new DataOutputStream(mockSocket.getOutputStream());
MockOutput mockOutput = new MockOutput(latestMessages, outToVisualiser);
ControllerServer controllerServer = new ControllerServer(mockSocket); //TODO probably pass assigned boat source ID into ControllerServer.
ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand);
new Thread(mockOutput, "ConnectionAcceptor.run()->MockOutput thread" + mockOutput).start();
new Thread(controllerServer, "ConnectionAcceptor.run()->ControllerServer thread" + controllerServer).start();
clientConnections.add(clientConnection);
mockOutputList.add(mockOutput);
new Thread(clientConnection, "ConnectionAcceptor.run()->ClientConnection thread " + clientConnection).start();
System.out.println(String.format("%d number of Visualisers Connected.", mockOutputList.size()));//TEMP
Logger.getGlobal().log(Level.INFO, String.format("%d number of Visualisers Connected.", clientConnections.size()));
} catch (IOException e) {
e.printStackTrace();//TODO handle this properly
Logger.getGlobal().log(Level.WARNING, "Got an IOException while a client was attempting to connect.", e);
}
@@ -104,14 +130,14 @@ public class ConnectionAcceptor implements Runnable {
*/
class CheckClientConnection implements Runnable{
private ArrayBlockingQueue<MockOutput> mocks;
private ArrayBlockingQueue<ClientConnection> connections;
/**
* Constructor
* @param mocks Mocks "connected"
* @param connections Clients "connected"
*/
public CheckClientConnection(ArrayBlockingQueue<MockOutput> mocks){
this.mocks = mocks;
public CheckClientConnection(ArrayBlockingQueue<ClientConnection> connections){
this.connections = connections;
}
/**
@@ -119,21 +145,27 @@ public class ConnectionAcceptor implements Runnable {
*/
@Override
public void run() {
double timeSinceLastHeartBeat = System.currentTimeMillis();
while(true) {
//System.out.println(mocks.size());//used to see current amount of visualisers connected.
ArrayBlockingQueue<MockOutput> m = new ArrayBlockingQueue<>(16, true, mocks);
for (MockOutput mo : m) {
try {
mo.sendHeartBeat();
} catch (IOException e) {
mocks.remove(mo);
//System.out.println(connections.size());//used to see current amount of visualisers connected.
ArrayBlockingQueue<ClientConnection> clientConnections = new ArrayBlockingQueue<>(16, true, connections);
for (ClientConnection client : clientConnections) {
if (!client.isAlive()) {
connections.remove(client);
Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
Logger.getGlobal().log(Level.WARNING, "CheckClientConnection was interrupted while sleeping.", e);
Thread.currentThread().interrupt();
return;
}
}
}
@@ -1,10 +1,14 @@
package mock.app;
import mock.dataInput.PolarParser;
import mock.exceptions.EventConstructionException;
import mock.model.MockRace;
import mock.model.Polars;
import mock.model.RaceLogic;
import mock.model.SourceIdAllocator;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.LatestMessages;
import network.Messages.RaceSnapshot;
import shared.dataInput.*;
import shared.enums.XMLFileType;
import shared.exceptions.InvalidBoatDataException;
@@ -19,14 +23,18 @@ import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Race Event, this holds all of the race's information as well as handling the connection to its clients.
*/
public class Event {
private static Event theEvent = new Event();
/**
* Contents of the various xml files.
*/
private String raceXML;
private String regattaXML;
private String boatXML;
@@ -35,36 +43,75 @@ public class Event {
private Polars boatPolars;
/**
* Data sources containing data from the xml files.
*/
RaceDataSource raceDataSource;
BoatDataSource boatDataSource;
RegattaDataSource regattaDataSource;
private ConnectionAcceptor connectionAcceptor;
private LatestMessages latestMessages;
private CompositeCommand compositeCommand;
/**
* This is used to allocate source IDs.
*/
private SourceIdAllocator sourceIdAllocator;
/**
* Constructs an event, using various XML files.
* @throws EventConstructionException Thrown if we cannot create an Event for any reason.
*/
private Event() {
public Event() throws EventConstructionException {
//Read XML files.
try {
this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8));
this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8);
this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8);
this.xmlFileType = XMLFileType.Contents;
this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv");
this.latestMessages = new LatestMessages();
this.connectionAcceptor = new ConnectionAcceptor(latestMessages);
} catch (TransformerException | XMLReaderException e) {
throw new EventConstructionException("Could not read XML files.", e);
}
catch (IOException e) {
e.printStackTrace();
} catch (XMLReaderException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
this.xmlFileType = XMLFileType.Contents;
this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv");
//Parse the XML files into data sources.
try {
this.raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType);
this.boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType);
this.regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType);
} catch (XMLReaderException | InvalidRaceDataException | InvalidRegattaDataException | InvalidBoatDataException e) {
throw new EventConstructionException("Could not parse XML files.", e);
}
//Create connection acceptor.
this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants());
this.compositeCommand = new CompositeCommand();
this.latestMessages = new LatestMessages();
try {
this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator);
} catch (IOException e) {
throw new EventConstructionException("Could not create ConnectionAcceptor.", e);
}
}
public static Event getEvent() {
return theEvent;
}
public String getAddress() throws UnknownHostException {
return connectionAcceptor.getAddress();
@@ -76,23 +123,16 @@ public class Event {
/**
* Sends the initial race data and then begins race simulation.
* @throws InvalidRaceDataException Thrown if the race xml file cannot be parsed.
* @throws XMLReaderException Thrown if any of the xml files cannot be parsed.
* @throws InvalidBoatDataException Thrown if the boat xml file cannot be parsed.
* @throws InvalidRegattaDataException Thrown if the regatta xml file cannot be parsed.
*/
public void start() throws InvalidRaceDataException, XMLReaderException, InvalidBoatDataException, InvalidRegattaDataException {
public void start() {
new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start();
sendXMLs();
//Parse the XML files into data sources.
RaceDataSource raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType);
BoatDataSource boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType);
RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType);
//Create and start race.
RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages);
RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages, this.compositeCommand);
new Thread(newRace, "Event.Start()->RaceLogic thread").start();
}
@@ -2,37 +2,25 @@ package mock.app;
import network.BinaryMessageEncoder;
import network.Exceptions.InvalidMessageException;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.*;
import network.Messages.Enums.MessageType;
import shared.model.RunnableWithFramePeriod;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.SocketException;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* TCP server to send race information to connected clients.
*/
public class MockOutput implements Runnable
{
/**
* Timestamp of the last sent heartbeat message.
*/
private long lastHeartbeatTime;
public class MockOutput implements RunnableWithFramePeriod {
/**
* Period for the heartbeat - that is, how often we send it.
* A queue to send messages to client.
*/
private double heartbeatPeriod = 5.0;
/**
* Output stream which wraps around mockSocket outstream.
*/
private DataOutputStream outToVisualiser;
private BlockingQueue<AC35Data> outgoingMessages;
/**
@@ -43,186 +31,20 @@ public class MockOutput implements Runnable
/**
* Ack numbers used in messages.
*/
private int ackNumber = 1;
/**
* Sequence number for heartbeat messages.
*/
private int heartbeatSequenceNum = 1;
/**
* Ctor.
* @param latestMessages Latests Messages that the Mock is to send out
* @param outToVisualiser DataStream to output to Visualisers
* @throws IOException if server socket cannot be opened.
* @param latestMessages Latest Messages that the Mock is to send out
* @param outgoingMessages A queue to place outgoing messages on.
*/
public MockOutput(LatestMessages latestMessages, DataOutputStream outToVisualiser) throws IOException {
this.outToVisualiser = outToVisualiser;
this.lastHeartbeatTime = System.currentTimeMillis();
public MockOutput(LatestMessages latestMessages, BlockingQueue<AC35Data> outgoingMessages) {
this.outgoingMessages = outgoingMessages;
this.latestMessages = latestMessages;
}
/**
* Increments the ackNumber value, and returns it.
* @return Incremented ackNumber.
*/
private int getNextAckNumber(){
this.ackNumber++;
return this.ackNumber;
}
/**
* Calculates the time since last heartbeat message, in seconds.
* @return Time since last heartbeat message, in seconds.
*/
private double timeSinceHeartbeat() {
long now = System.currentTimeMillis();
return (now - lastHeartbeatTime) / 1000.0;
}
/**
* Generates the next heartbeat message and returns it. Increments the heartbeat sequence number.
* @return The next heartbeat message.
*/
private HeartBeat createHeartbeatMessage() {
//Create the heartbeat message.
HeartBeat heartBeat = new HeartBeat(this.heartbeatSequenceNum);
heartbeatSequenceNum++;
return heartBeat;
}
/**
* Serializes a heartbeat message into a packet to be sent, and returns the byte array.
* @param heartBeat The heartbeat message to serialize.
* @return Byte array containing the next heartbeat message.
* @throws InvalidMessageException Thrown if the message cannot be encoded.
*/
private byte[] parseHeartbeat(HeartBeat heartBeat) throws InvalidMessageException {
//Serializes the heartbeat message.
byte[] heartbeatMessage = RaceVisionByteEncoder.encode(heartBeat);
//Places the serialized message in a packet.
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
MessageType.HEARTBEAT,
System.currentTimeMillis(),
getNextAckNumber(),
(short) heartbeatMessage.length,
heartbeatMessage );
return binaryMessageEncoder.getFullMessage();
}
/**
* Encodes/serialises a XMLMessage message, and returns it.
* @param xmlMessage The XMLMessage message to serialise.
* @return The XMLMessage message in a serialised form.
* @throws InvalidMessageException Thrown if the message cannot be encoded.
*/
private synchronized byte[] parseXMLMessage(XMLMessage xmlMessage) throws InvalidMessageException {
//Serialize the xml message.
byte[] encodedXML = RaceVisionByteEncoder.encode(xmlMessage);
//Place the message in a packet.
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
MessageType.XMLMESSAGE,
System.currentTimeMillis(),
xmlMessage.getAckNumber(), //We use the ack number from the xml message.
(short) encodedXML.length,
encodedXML );
return binaryMessageEncoder.getFullMessage();
}
/**
* Encodes/serialises a BoatLocation message, and returns it.
* @param boatLocation The BoatLocation message to serialise.
* @return The BoatLocation message in a serialised form.
* @throws InvalidMessageException If the message cannot be encoded.
*/
private synchronized byte[] parseBoatLocation(BoatLocation boatLocation) throws InvalidMessageException {
//Encodes the message.
byte[] encodedBoatLoc = RaceVisionByteEncoder.encode(boatLocation);
//Encodes the full message with header.
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
MessageType.BOATLOCATION,
System.currentTimeMillis(),
getNextAckNumber(),
(short) encodedBoatLoc.length,
encodedBoatLoc );
return binaryMessageEncoder.getFullMessage();
}
/**
* Encodes/serialises a RaceStatus message, and returns it.
* @param raceStatus The RaceStatus message to serialise.
* @return The RaceStatus message in a serialised form.
* @throws InvalidMessageException Thrown if the message cannot be encoded.
*/
private synchronized byte[] parseRaceStatus(RaceStatus raceStatus) throws InvalidMessageException {
//Encodes the messages.
byte[] encodedRaceStatus = RaceVisionByteEncoder.encode(raceStatus);
//Encodes the full message with header.
BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
MessageType.RACESTATUS,
System.currentTimeMillis(),
getNextAckNumber(),
(short) encodedRaceStatus.length,
encodedRaceStatus );
return binaryMessageEncoder.getFullMessage();
}
/**
* Sends a heartbeat
* @throws IOException if the socket is no longer open at both ends the heartbeat returns an error.
*/
public void sendHeartBeat() throws IOException {
//Sends a heartbeat every so often.
if (timeSinceHeartbeat() >= heartbeatPeriod) {
HeartBeat heartBeat = createHeartbeatMessage();
try {
outToVisualiser.write(parseHeartbeat(heartBeat));
} catch (InvalidMessageException e) {
Logger.getGlobal().log(Level.WARNING, "Could not encode HeartBeat: " + heartBeat, e);
}
lastHeartbeatTime = System.currentTimeMillis();
}
}
/**
* Sending loop of the Server
@@ -251,107 +73,40 @@ public class MockOutput implements Runnable
long previousFrameTime = System.currentTimeMillis();
boolean sentXMLs = false;
try {
while (!Thread.interrupted()) {
try {
long currentFrameTime = System.currentTimeMillis();
//This is the time elapsed, in milliseconds, since the last server "frame".
long framePeriod = currentFrameTime - previousFrameTime;
//We only attempt to send packets every X milliseconds.
long minimumFramePeriod = 16;
if (framePeriod >= minimumFramePeriod) {
//Send XML messages.
if (!sentXMLs) {
//Serialise them.
try {
byte[] raceXMLBlob = parseXMLMessage(latestMessages.getRaceXMLMessage());
byte[] regattaXMLBlob = parseXMLMessage(latestMessages.getRegattaXMLMessage());
byte[] boatsXMLBlob = parseXMLMessage(latestMessages.getBoatXMLMessage());
//Send them.
outToVisualiser.write(raceXMLBlob);
outToVisualiser.write(regattaXMLBlob);
outToVisualiser.write(boatsXMLBlob);
sentXMLs = true;
} catch (InvalidMessageException e) {
Logger.getGlobal().log(Level.WARNING, "Could not encode XMLMessage: " + latestMessages.getRaceXMLMessage(), e);
continue; //Go to next iteration.
}
}
//Sends the RaceStatus message.
if (this.latestMessages.getRaceStatus() != null) {
try {
byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus());
this.outToVisualiser.write(raceStatusBlob);
} catch (InvalidMessageException e) {
Logger.getGlobal().log(Level.WARNING, "Could not encode RaceStatus: " + latestMessages.getRaceStatus(), e);
}
}
//Send all of the BoatLocation messages.
for (int sourceID : this.latestMessages.getBoatLocationMap().keySet()) {
//Get the message.
BoatLocation boatLocation = this.latestMessages.getBoatLocation(sourceID);
if (boatLocation != null) {
try {
//Encode.
byte[] boatLocationBlob = this.parseBoatLocation(boatLocation);
while (!Thread.interrupted()) {
//Write it.
this.outToVisualiser.write(boatLocationBlob);
try {
} catch (InvalidMessageException e) {
Logger.getGlobal().log(Level.WARNING, "Could not encode BoatLocation: " + boatLocation, e);
}
long currentFrameTime = System.currentTimeMillis();
waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
previousFrameTime = currentFrameTime;
//Send XML messages.
if (!sentXMLs) {
}
}
outgoingMessages.put(latestMessages.getRaceXMLMessage());
outgoingMessages.put(latestMessages.getRegattaXMLMessage());
outgoingMessages.put(latestMessages.getBoatXMLMessage());
previousFrameTime = currentFrameTime;
} else {
//Wait until the frame period will be large enough.
long timeToWait = minimumFramePeriod - framePeriod;
try {
Thread.sleep(timeToWait);
} catch (InterruptedException e) {
//If we get interrupted, exit the function.
Logger.getGlobal().log(Level.WARNING, "MockOutput.run().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e);
//Re-set the interrupt flag.
Thread.currentThread().interrupt();
return;
}
}
} catch (SocketException e) {
break;
sentXMLs = true;
}
List<AC35Data> snapshot = latestMessages.getSnapshot();
for (AC35Data message : snapshot) {
outgoingMessages.put(message);
}
} catch (InterruptedException e) {
Logger.getGlobal().log(Level.WARNING, "MockOutput.run() interrupted while putting message in queue.", e);
Thread.currentThread().interrupt();
return;
}
} catch (IOException e) {
e.printStackTrace();
}
}