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:
@@ -144,32 +144,5 @@ public class ConnectionController extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a new host
|
||||
*/
|
||||
public void addLocal() {
|
||||
try {
|
||||
//We don't want to host more than one game.
|
||||
if (!currentlyHostingGame) {
|
||||
Event game = Event.getEvent();
|
||||
urlField.textProperty().set(game.getAddress());
|
||||
portField.textProperty().set(Integer.toString(game.getPort()));
|
||||
|
||||
game.start();
|
||||
addConnection();
|
||||
|
||||
currentlyHostingGame = true;
|
||||
}
|
||||
} catch (InvalidRaceDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (XMLReaderException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvalidBoatDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvalidRegattaDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.stage.Stage;
|
||||
import mock.app.Event;
|
||||
import mock.exceptions.EventConstructionException;
|
||||
import shared.exceptions.InvalidBoatDataException;
|
||||
import shared.exceptions.InvalidRaceDataException;
|
||||
import shared.exceptions.InvalidRegattaDataException;
|
||||
@@ -17,6 +18,8 @@ import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Controller for Hosting a game.
|
||||
@@ -44,17 +47,12 @@ public class HostController extends Controller {
|
||||
*/
|
||||
public void hostGamePressed() throws IOException{
|
||||
try {
|
||||
Event game = Event.getEvent();
|
||||
Event game = new Event();
|
||||
game.start();
|
||||
connectSocket("localhost", 4942);
|
||||
} catch (InvalidRaceDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (XMLReaderException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvalidBoatDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InvalidRegattaDataException e) {
|
||||
e.printStackTrace();
|
||||
} catch (EventConstructionException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "Could not create Event.", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import visualiser.model.*;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Controller used to display a running race.
|
||||
@@ -115,8 +117,9 @@ public class RaceController extends Controller {
|
||||
controllerClient.sendKey(controlKey);
|
||||
controlKey.onAction(); // Change key state if applicable
|
||||
event.consume();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ import shared.exceptions.InvalidRegattaDataException;
|
||||
import shared.exceptions.XMLReaderException;
|
||||
import visualiser.app.VisualiserInput;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.model.ServerConnection;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRace;
|
||||
|
||||
@@ -27,6 +28,8 @@ import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Controller to for waiting for the race to start.
|
||||
@@ -66,18 +69,18 @@ public class StartController extends Controller implements Observer {
|
||||
@FXML private Label raceStatusLabel;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The object used to read packets from the connected server.
|
||||
* Our connection to the server.
|
||||
*/
|
||||
private VisualiserInput visualiserInput;
|
||||
private ServerConnection serverConnection;
|
||||
|
||||
|
||||
/**
|
||||
* The race object which describes the currently occurring race.
|
||||
*/
|
||||
private VisualiserRace visualiserRace;
|
||||
|
||||
private ControllerClient controllerClient;
|
||||
|
||||
|
||||
/**
|
||||
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
|
||||
@@ -309,17 +312,17 @@ public class StartController extends Controller implements Observer {
|
||||
public void enterLobby(Socket socket) {
|
||||
startWrapper.setVisible(true);
|
||||
try {
|
||||
//Begin reading packets from the socket/server.
|
||||
this.visualiserInput = new VisualiserInput(socket);
|
||||
//Send controller input to server
|
||||
this.controllerClient = new ControllerClient(socket);
|
||||
|
||||
LatestMessages latestMessages = new LatestMessages();
|
||||
this.serverConnection = new ServerConnection(socket, latestMessages);
|
||||
|
||||
|
||||
//Store a reference to latestMessages so that we can observe it.
|
||||
LatestMessages latestMessages = this.visualiserInput.getLatestMessages();
|
||||
latestMessages.addObserver(this);
|
||||
new Thread(this.visualiserInput).start();
|
||||
new Thread(this.serverConnection).start();
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Logger.getGlobal().log(Level.WARNING, "Could not connection to server.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ package visualiser.app;
|
||||
import network.BinaryMessageDecoder;
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.Messages.*;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
import static network.Utils.ByteConverter.bytesToShort;
|
||||
|
||||
@@ -15,7 +17,7 @@ import static network.Utils.ByteConverter.bytesToShort;
|
||||
* 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 Runnable {
|
||||
public class VisualiserInput implements RunnableWithFramePeriod {
|
||||
|
||||
/**
|
||||
* Timestamp of the last heartbeat.
|
||||
@@ -27,40 +29,28 @@ public class VisualiserInput implements Runnable {
|
||||
private long lastHeartbeatSequenceNum = -1;
|
||||
|
||||
|
||||
/**
|
||||
* The socket that we have connected to.
|
||||
*/
|
||||
private Socket connectionSocket;
|
||||
|
||||
|
||||
/**
|
||||
* InputStream (from the socket).
|
||||
* Incoming messages from server.
|
||||
*/
|
||||
private DataInputStream inStream;
|
||||
private BlockingQueue<AC35Data> incomingMessages;
|
||||
|
||||
|
||||
/**
|
||||
* An object containing the set of latest messages to write to.
|
||||
* Every server frame, VisualiserInput reads messages from its inputStream, and write them to this.
|
||||
* Every server frame, VisualiserInput reads messages from its incomingMessages, and write them to this.
|
||||
*/
|
||||
private LatestMessages latestMessages;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
* @param socket Socket from which we will receive race data.
|
||||
* @throws IOException If there is something wrong with the socket's input stream.
|
||||
* Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages.
|
||||
* @param latestMessages Object to place messages in.
|
||||
* @param incomingMessages The incoming queue of messages.
|
||||
*/
|
||||
public VisualiserInput(Socket socket) throws IOException {
|
||||
|
||||
this.connectionSocket = socket;
|
||||
|
||||
//We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled.
|
||||
this.inStream = new DataInputStream(connectionSocket.getInputStream());
|
||||
|
||||
this.latestMessages = new LatestMessages();
|
||||
|
||||
public VisualiserInput(LatestMessages latestMessages, BlockingQueue<AC35Data> incomingMessages) {
|
||||
this.latestMessages = latestMessages;
|
||||
this.incomingMessages = incomingMessages;
|
||||
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
@@ -85,279 +75,21 @@ public class VisualiserInput implements Runnable {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead.
|
||||
* @return Encoded binary message bytes.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
*/
|
||||
private byte[] getNextMessageBytes() throws IOException {
|
||||
inStream.mark(0);
|
||||
short CRCLength = 4;
|
||||
short headerLength = 15;
|
||||
|
||||
//Read the header of the next message.
|
||||
byte[] headerBytes = new byte[headerLength];
|
||||
inStream.readFully(headerBytes);
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
//Read the message body length.
|
||||
byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength);
|
||||
short messageBodyLength = bytesToShort(messageBodyLengthBytes);
|
||||
//Handshake.
|
||||
|
||||
//Read the message body.
|
||||
byte[] messageBodyBytes = new byte[messageBodyLength];
|
||||
inStream.readFully(messageBodyBytes);
|
||||
//Main loop.
|
||||
// take message
|
||||
// create command
|
||||
// place in command queue
|
||||
|
||||
//Read the message CRC.
|
||||
byte[] messageCRCBytes = new byte[CRCLength];
|
||||
inStream.readFully(messageCRCBytes);
|
||||
|
||||
//Put the head + body + crc into one large array.
|
||||
ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length);
|
||||
messageBytes.put(headerBytes);
|
||||
messageBytes.put(messageBodyBytes);
|
||||
messageBytes.put(messageCRCBytes);
|
||||
|
||||
return messageBytes.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns the next message object from the socket.
|
||||
* @return The message object. Use instanceof for concrete type.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
* @throws InvalidMessageException Thrown when the message is invalid in some way.
|
||||
*/
|
||||
private AC35Data getNextMessage() throws IOException, InvalidMessageException
|
||||
{
|
||||
//Get the next message from the socket as a block of bytes.
|
||||
byte[] messageBytes = this.getNextMessageBytes();
|
||||
|
||||
//Decode the binary message into an appropriate message object.
|
||||
BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes);
|
||||
|
||||
return decoder.decode();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop which reads messages from the socket, and exposes them.
|
||||
*/
|
||||
public void run(){
|
||||
boolean receiverLoop = true;
|
||||
//receiver loop that gets the input
|
||||
while (receiverLoop) {
|
||||
|
||||
//If no heartbeat has been received in more the heartbeat period
|
||||
//then the connection will need to be restarted.
|
||||
//System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE
|
||||
long heartBeatPeriod = 10 * 1000;
|
||||
if (timeSinceHeartbeat() > heartBeatPeriod) {
|
||||
System.out.println("Connection has stopped, trying to reconnect.");
|
||||
|
||||
//Attempt to reconnect the socket.
|
||||
try {//This attempt doesn't really work. Under what circumstances would
|
||||
this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort());
|
||||
//this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress());
|
||||
//Reset the heartbeat timer.
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Unable to reconnect.");
|
||||
|
||||
//Wait 500ms. Ugly hack, should refactor.
|
||||
long waitPeriod = 500;
|
||||
long waitTimeStart = System.currentTimeMillis() + waitPeriod;
|
||||
|
||||
while (System.currentTimeMillis() < waitTimeStart){
|
||||
//Nothing. Busyloop.
|
||||
}
|
||||
|
||||
//Swallow the exception.
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Reads the next message.
|
||||
AC35Data message;
|
||||
try {
|
||||
message = this.getNextMessage();
|
||||
}
|
||||
catch (InvalidMessageException | IOException e) {
|
||||
//Prints exception to stderr, and iterate loop (that is, read the next message).
|
||||
System.err.println("Unable to read message: " + e.getMessage());
|
||||
try {
|
||||
inStream.reset();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
//Continue to the next loop iteration/message.
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//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: {
|
||||
RaceStatus raceStatus = (RaceStatus) message;
|
||||
|
||||
//System.out.println("Race Status Message");
|
||||
this.latestMessages.setRaceStatus(raceStatus);
|
||||
|
||||
for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
|
||||
this.latestMessages.setBoatStatus(boatStatus);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//DisplayTextMessage.
|
||||
case DISPLAYTEXTMESSAGE: {
|
||||
//System.out.println("Display Text Message");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//XMLMessage.
|
||||
case XMLMESSAGE: {
|
||||
XMLMessage xmlMessage = (XMLMessage) message;
|
||||
|
||||
//System.out.println("XML Message!");
|
||||
|
||||
this.latestMessages.setXMLMessage(xmlMessage);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//RaceStartStatus.
|
||||
case RACESTARTSTATUS: {
|
||||
|
||||
//System.out.println("Race Start Status Message");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//YachtEventCode.
|
||||
case YACHTEVENTCODE: {
|
||||
//YachtEventCode yachtEventCode = (YachtEventCode) message;
|
||||
|
||||
//System.out.println("Yacht Event Code!");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//YachtActionCode.
|
||||
case YACHTACTIONCODE: {
|
||||
//YachtActionCode yachtActionCode = (YachtActionCode) message;
|
||||
|
||||
//System.out.println("Yacht Action Code!");
|
||||
// No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//ChatterText.
|
||||
case CHATTERTEXT: {
|
||||
//ChatterText chatterText = (ChatterText) message;
|
||||
|
||||
//System.out.println("Chatter Text Message!");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//BoatLocation.
|
||||
case BOATLOCATION: {
|
||||
BoatLocation boatLocation = (BoatLocation) message;
|
||||
|
||||
//System.out.println("Boat Location!");
|
||||
|
||||
BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID());
|
||||
if (existingBoatLocation != null) {
|
||||
//If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (boatLocation.getTime() > existingBoatLocation.getTime()) {
|
||||
//If it is, replace the old message.
|
||||
this.latestMessages.setBoatLocation(boatLocation);
|
||||
}
|
||||
} else {
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.latestMessages.setBoatLocation(boatLocation);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//MarkRounding.
|
||||
case MARKROUNDING: {
|
||||
MarkRounding markRounding = (MarkRounding) message;
|
||||
|
||||
//System.out.println("Mark Rounding Message!");
|
||||
|
||||
MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID());
|
||||
if (existingMarkRounding != null) {
|
||||
|
||||
//If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (markRounding.getTime() > existingMarkRounding.getTime()) {
|
||||
//If it is, replace the old message.
|
||||
this.latestMessages.setMarkRounding(markRounding);
|
||||
}
|
||||
|
||||
} else {
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.latestMessages.setMarkRounding(markRounding);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//CourseWinds.
|
||||
case COURSEWIND: {
|
||||
|
||||
//System.out.println("Course Wind Message!");
|
||||
CourseWinds courseWinds = (CourseWinds) message;
|
||||
|
||||
this.latestMessages.setCourseWinds(courseWinds);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//AverageWind.
|
||||
case AVGWIND: {
|
||||
|
||||
//System.out.println("Average Wind Message!");
|
||||
AverageWind averageWind = (AverageWind) message;
|
||||
|
||||
this.latestMessages.setAverageWind(averageWind);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//Unrecognised message.
|
||||
default: {
|
||||
System.out.println("Broken Message!");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package visualiser.gameController;
|
||||
import network.BinaryMessageEncoder;
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.MessageEncoders.RaceVisionByteEncoder;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatAction;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
import network.Messages.Enums.MessageType;
|
||||
@@ -13,6 +14,7 @@ import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -20,28 +22,18 @@ import java.util.logging.Logger;
|
||||
* Basic service for sending key presses to game server
|
||||
*/
|
||||
public class ControllerClient {
|
||||
/**
|
||||
* Socket to server
|
||||
*/
|
||||
Socket socket;
|
||||
|
||||
/**
|
||||
* Output stream wrapper for socket to server
|
||||
* Queue of messages to be sent to server.
|
||||
*/
|
||||
DataOutputStream outputStream;
|
||||
private BlockingQueue<AC35Data> outgoingMessages;
|
||||
|
||||
/**
|
||||
* Initialise controller client with live socket.
|
||||
* @param socket to server
|
||||
* @param outgoingMessages Queue to place messages on to send to server.
|
||||
*/
|
||||
public ControllerClient(Socket socket) {
|
||||
this.socket = socket;
|
||||
|
||||
try {
|
||||
this.outputStream = new DataOutputStream(socket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
public ControllerClient(BlockingQueue<AC35Data> outgoingMessages) {
|
||||
this.outgoingMessages = outgoingMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,27 +41,13 @@ public class ControllerClient {
|
||||
* @param key to send
|
||||
* @throws IOException if socket write fails
|
||||
*/
|
||||
public void sendKey(ControlKey key) throws IOException {
|
||||
public void sendKey(ControlKey key) throws InterruptedException {
|
||||
BoatActionEnum protocolCode = key.getProtocolCode();
|
||||
if(protocolCode != BoatActionEnum.NOT_A_STATUS) {
|
||||
|
||||
BoatAction boatAction = new BoatAction(protocolCode);
|
||||
|
||||
//Encode BoatAction.
|
||||
try {
|
||||
byte[] encodedBoatAction = RaceVisionByteEncoder.encode(boatAction);
|
||||
|
||||
BinaryMessageEncoder binaryMessage = new BinaryMessageEncoder(MessageType.BOATACTION, System.currentTimeMillis(), 0,
|
||||
(short) encodedBoatAction.length, encodedBoatAction);
|
||||
|
||||
System.out.println("Sending out key: " + protocolCode);
|
||||
outputStream.write(binaryMessage.getFullMessage());
|
||||
|
||||
} catch (InvalidMessageException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "Could not encode BoatAction: " + boatAction, e);
|
||||
|
||||
}
|
||||
|
||||
outgoingMessages.put(boatAction);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package visualiser.gameController;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import mock.model.commandFactory.CommandFactory;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import network.BinaryMessageDecoder;
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.MessageDecoders.BoatActionDecoder;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatAction;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
import visualiser.gameController.Keys.ControlKey;
|
||||
import visualiser.gameController.Keys.KeyFactory;
|
||||
import network.Messages.Enums.MessageType;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -18,57 +21,69 @@ import java.util.logging.Logger;
|
||||
* Service for dispatching key press data to race from client
|
||||
*/
|
||||
public class ControllerServer implements Runnable {
|
||||
/**
|
||||
* Socket to client
|
||||
*/
|
||||
private Socket socket;
|
||||
/**
|
||||
* Wrapper for input from client
|
||||
*/
|
||||
private DataInputStream inputStream;
|
||||
|
||||
|
||||
/**
|
||||
* Initialise server-side controller with live client socket
|
||||
* @param socket to client
|
||||
* Queue of incoming messages from client.
|
||||
*/
|
||||
public ControllerServer(Socket socket) {
|
||||
this.socket = socket;
|
||||
try {
|
||||
this.inputStream = new DataInputStream(this.socket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
private BlockingQueue<AC35Data> inputQueue;
|
||||
|
||||
|
||||
/**
|
||||
* Collection of commands from client for race to execute.
|
||||
*/
|
||||
private CompositeCommand compositeCommand;
|
||||
|
||||
/**
|
||||
* This is the source ID associated with the client.
|
||||
*/
|
||||
private int clientSourceID;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Initialise server-side controller with live client socket.
|
||||
* @param compositeCommand Commands for the race to execute.
|
||||
* @param inputQueue The queue of messages to read from.
|
||||
* @param clientSourceID The source ID of the client's boat.
|
||||
*/
|
||||
public ControllerServer(CompositeCommand compositeCommand, BlockingQueue<AC35Data> inputQueue, int clientSourceID) {
|
||||
this.compositeCommand = compositeCommand;
|
||||
this.inputQueue = inputQueue;
|
||||
this.clientSourceID = clientSourceID;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wait for controller key input from client and loop.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
while(true) {
|
||||
byte[] message = new byte[20];
|
||||
while(!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
if (inputStream.available() > 0) {
|
||||
|
||||
inputStream.read(message);
|
||||
AC35Data message = inputQueue.take();
|
||||
|
||||
BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message);
|
||||
BoatActionDecoder boatActionDecoder = new BoatActionDecoder();
|
||||
if (message.getType() == MessageType.BOATACTION) {
|
||||
|
||||
try {
|
||||
boatActionDecoder.decode(encodedMessage.getMessageBody());
|
||||
BoatAction boatAction = boatActionDecoder.getMessage();
|
||||
System.out.println("Received key: " + boatAction.getBoatAction());
|
||||
|
||||
} catch (InvalidMessageException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "Could not decode BoatAction message.", e);
|
||||
}
|
||||
BoatAction boatAction = (BoatAction) message;
|
||||
|
||||
boatAction.setSourceID(clientSourceID);
|
||||
|
||||
Command command = CommandFactory.createCommand(boatAction);
|
||||
compositeCommand.addCommand(command);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ public class KeyFactory {
|
||||
keyState.put("SPACE", new VMGKey("VMG"));
|
||||
keyState.put("SHIFT", new SailsToggleKey("Toggle Sails"));
|
||||
keyState.put("ENTER", new TackGybeKey("Tack/Gybe"));
|
||||
keyState.put("PAGE_UP", new UpWindKey("Upwind"));
|
||||
keyState.put("PAGE_DOWN", new DownWindKey("Downwind"));
|
||||
keyState.put("UP", new UpWindKey("Upwind"));
|
||||
keyState.put("DOWN", new DownWindKey("Downwind"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
package visualiser.model;
|
||||
|
||||
|
||||
import mock.app.MockOutput;
|
||||
import mock.enums.ConnectionStateEnum;
|
||||
import mock.exceptions.SourceIDAllocationException;
|
||||
import mock.model.HeartBeatService;
|
||||
import mock.model.SourceIdAllocator;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
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 visualiser.gameController.ControllerServer;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* 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<AC35Data> outputQueue;
|
||||
|
||||
/**
|
||||
* Used to read messages from socket.
|
||||
*/
|
||||
private MessageDeserialiser messageDeserialiser;
|
||||
|
||||
/**
|
||||
* Stores messages read from socket.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> 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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
sendJoinAcceptanceMessage(RequestToJoinEnum.PARTICIPANT);
|
||||
|
||||
|
||||
JoinAcceptance joinAcceptance = waitForJoinAcceptance();
|
||||
|
||||
int allocatedSourceID = 0;
|
||||
|
||||
//If we join successfully...
|
||||
if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) {
|
||||
|
||||
allocatedSourceID = joinAcceptance.getSourceID();
|
||||
//TODO need to do something with the ID - maybe flag the correct visualiser boat as being the client's boat?
|
||||
|
||||
this.controllerClient = new ControllerClient(inputQueue);
|
||||
//new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start();
|
||||
|
||||
}
|
||||
|
||||
this.visualiserInput = new VisualiserInput(latestMessages, outputQueue);
|
||||
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 sendJoinAcceptanceMessage(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 MessageSerialiser} is still alive.
|
||||
* @return True if it is alive, false otherwise.
|
||||
*/
|
||||
public boolean isAlive() {
|
||||
return messageSerialiser.isRunning();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user