package seng302.gameServer; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.ArrayList; import java.util.Random; import java.util.zip.CRC32; import java.util.zip.Checksum; import org.apache.commons.io.IOUtils; import seng302.models.Player; import seng302.models.Yacht; import seng302.models.stream.packets.PacketType; import seng302.models.stream.packets.StreamPacket; import seng302.models.xml.Race; import seng302.models.xml.Regatta; import seng302.models.xml.XMLGenerator; import seng302.server.messages.BoatActionType; import seng302.server.messages.BoatLocationMessage; import seng302.server.messages.Message; import java.io.*; import java.net.Socket; import java.util.zip.CRC32; import java.util.zip.Checksum; import seng302.server.messages.XMLMessage; import seng302.server.messages.XMLMessageSubType; import seng302.server.messages.XMLMessage; import seng302.server.messages.XMLMessageSubType; import seng302.utilities.GeoPoint; /** * A class describing a single connection to a Client for the purposes of sending and receiving on its own thread. * All server threads created and owned by the server thread handler which can trigger client updates on its threads * Created by wmu16 on 13/07/17. */ public class ServerToClientThread implements Runnable { private static final Integer LOG_LEVEL = 1; private static final Integer MAX_ID_ATTEMPTS = 10; private Thread thread; private InputStream is; private OutputStream os; private Socket socket; private ByteArrayOutputStream crcBuffer; private Boolean userIdentified = false; private Boolean connected = true; private Boolean updateClient = true; private Boolean initialisedRace = true; private Integer seqNo; private Integer sourceId; private XMLGenerator xml; public ServerToClientThread(Socket socket) { this.socket = socket; try { is = socket.getInputStream(); os = socket.getOutputStream(); } catch (IOException e) { System.out.println("IO error in server thread upon grabbing streams"); } //Attempt threeway handshake with connection sourceId = GameState.getUniquePlayerID(); if (threeWayHandshake(sourceId)) { serverLog("Successful handshake. Client allocated id: " + sourceId, 1); Yacht yacht = new Yacht("Yacht", sourceId, sourceId.toString(), "Kap", "Kappa", "NZ"); // Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0); GameState.addYacht(sourceId, yacht); GameState.addPlayer(new Player(socket, yacht)); } else { serverLog("Unsuccessful handshake. Connection rejected", 1); closeSocket(); return; } seqNo = 0; thread = new Thread(this); thread.start(); } static void serverLog(String message, int logLevel){ if(logLevel <= LOG_LEVEL){ System.out.println("[SERVER] " + message); } } public void run() { int sync1; int sync2; // TODO: 14/07/17 wmu16 - Work out how to fix this while loop // used by ryan to simulate sending boats.xml // InputStream inputStream = getClass().getResourceAsStream("/server_config/boats1.xml"); // StringWriter writer = new StringWriter(); // try { // IOUtils.copy(inputStream, writer); // } catch (IOException e) { // e.printStackTrace(); // } // String xml = writer.toString(); // Message message = new XMLMessage(xml, XMLMessageSubType.BOAT, 0); // sendMessage(message); // System.out.println("[server] send message 1 " + message); // // try { // Thread.sleep(3000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // // inputStream = getClass().getResourceAsStream("/server_config/boats.xml"); // writer = new StringWriter(); // try { // IOUtils.copy(inputStream, writer); // } catch (IOException e) { // e.printStackTrace(); // } // xml = writer.toString(); // message = new XMLMessage(xml, XMLMessageSubType.BOAT, 0); // sendMessage(message); // System.out.println("[server] send message 2 " + message); // // try { // Thread.sleep(3000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // // inputStream = getClass().getResourceAsStream("/server_config/boats2.xml"); // writer = new StringWriter(); // try { // IOUtils.copy(inputStream, writer); // } catch (IOException e) { // e.printStackTrace(); // } // xml = writer.toString(); // message = new XMLMessage(xml, XMLMessageSubType.BOAT, 0); // sendMessage(message); // System.out.println("[server] send message 3 " + message); // // try { // Thread.sleep(3000); // } catch (InterruptedException e) { // e.printStackTrace(); // } // // inputStream = getClass().getResourceAsStream("/server_config/boats.xml"); // writer = new StringWriter(); // try { // IOUtils.copy(inputStream, writer); // } catch (IOException e) { // e.printStackTrace(); // } // xml = writer.toString(); // message = new XMLMessage(xml, XMLMessageSubType.BOAT, 0); // sendMessage(message); // System.out.println("[server] send message 4 " + message); //------- while(true) { try { if (initialisedRace) { sendSetupMessages(); initialisedRace = false; } //Perform a write if it is time to as delegated by the MainServerThread if (updateClient) { // TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream // ChatterMessage chatterMessage = new ChatterMessage(4, 14, "Hello, it's me"); // sendMessage(chatterMessage); // try { // GameState.outputState(os); // } catch (IOException e) { // System.out.println("IO error in server thread upon writing to output stream"); // } sendBoatLocationPackets(); updateClient = false; } crcBuffer = new ByteArrayOutputStream(); sync1 = readByte(); sync2 = readByte(); //checking if it is the start of the packet if(sync1 == 0x47 && sync2 == 0x83) { int type = readByte(); //No. of milliseconds since Jan 1st 1970 long timeStamp = Message.bytesToLong(getBytes(6)); skipBytes(4); long payloadLength = Message.bytesToLong(getBytes(2)); byte[] payload = getBytes((int) payloadLength); Checksum checksum = new CRC32(); checksum.update(crcBuffer.toByteArray(), 0, crcBuffer.size()); long computedCrc = checksum.getValue(); long packetCrc = Message.bytesToLong(getBytes(4)); if (computedCrc == packetCrc) { //System.out.println("RECEIVED A PACKET"); switch (PacketType.assignPacketType(type)) { case BOAT_ACTION: BoatActionType actionType = ServerPacketParser .extractBoatAction( new StreamPacket(type, payloadLength, timeStamp, payload)); GameState.updateBoat(sourceId, actionType); break; } } else { System.err.println("Packet has been dropped"); } } } catch (Exception e) { serverLog("ERROR OCCURRED, CLOSING SERVER CONNECTION: " + socket.getRemoteSocketAddress().toString(), 1); e.printStackTrace(); closeSocket(); return; } } } private void sendSetupMessages() { xml = new XMLGenerator(); Race race = new Race(); for (Player player : GameState.getPlayers()){ race.addBoat(player.getYacht()); } //@TODO calculate lat/lng values xml.setRegatta(new Regatta("RaceVision Test Game", 57.6679590, 11.8503233)); xml.setRace(race); XMLMessage xmlMessage = new XMLMessage(xml.getRegattaAsXml(), XMLMessageSubType.REGATTA, xml.getRegattaAsXml().length()); sendMessage(xmlMessage); xmlMessage = new XMLMessage(xml.getBoatsAsXml(), XMLMessageSubType.BOAT, xml.getBoatsAsXml().length()); sendMessage(xmlMessage); xmlMessage = new XMLMessage(xml.getRaceAsXml(), XMLMessageSubType.RACE, xml.getRaceAsXml().length()); sendMessage(xmlMessage); } public void updateClient() { updateClient = true; } /** * Tries to confirm the connection just accepted. * Sends ID, expects that ID echoed for confirmation, * if so, sends a confirmation packet back to that connection * Creates a player instance with that ID and this thread and adds it to the GameState * If not, close the socket and end the threads execution * @param id the id to try and assign to the connection * @return A boolean indicating if it was a successful handshake */ private Boolean threeWayHandshake(Integer id) { Integer confirmationID = null; Integer identificationAttempt = 0; while (!userIdentified) { try { os.write(id); //Send out new ID looking for echo confirmationID = is.read(); } catch (IOException e) { e.printStackTrace(); } if (id.equals(confirmationID)) { //ID is echoed back. Connection is a client return true; } else if (identificationAttempt > MAX_ID_ATTEMPTS) { //No response. not a client. tidy up and go home. return false; } identificationAttempt++; } return true; } private void closeSocket() { try { socket.close(); } catch (IOException e) { System.out.println("IO error in server thread upon trying to close socket"); } } public void initialiseRace(){ initialisedRace = true; } private int readByte() throws Exception { int currentByte = -1; try { // @TODO @FIX ConnectionReset Exception when a client disconnects before it is garbage collected currentByte = is.read(); crcBuffer.write(currentByte); } catch (IOException e) { e.printStackTrace(); } if (currentByte == -1){ throw new Exception(); } return currentByte; } private byte[] getBytes(int n) throws Exception{ byte[] bytes = new byte[n]; for (int i = 0; i < n; i++){ bytes[i] = (byte) readByte(); } return bytes; } private void skipBytes(long n) throws Exception{ for (int i=0; i < n; i++){ readByte(); } } public void sendMessage(Message message){ try { os.write(message.getBuffer()); } catch (IOException e) { e.printStackTrace(); } } private int getSeqNo(){ seqNo++; return seqNo; } private void sendBoatLocationPackets(){ ArrayList yachts = new ArrayList<>(GameState.getYachts().values()); for (Yacht yacht: yachts){ // System.out.println("[SERVER] Lat: " + yacht.getLocation().getLat() + " Lon: " + yacht.getLocation().getLng()); BoatLocationMessage boatLocationMessage = new BoatLocationMessage(sourceId, getSeqNo(), yacht.getLocation().getLat(), yacht.getLocation().getLng(), yacht.getHeading(), (long) yacht.getVelocity()); sendMessage(boatLocationMessage); } } public Thread getThread() { return thread; } }