From c19f66a6a4002c170bed39942929232d1445ce3a Mon Sep 17 00:00:00 2001 From: Michael Rausch Date: Thu, 13 Jul 2017 22:07:03 +1200 Subject: [PATCH] Added garbage collection for disconnected players - Heartbeat messages are sent out from their own thread to each player - If a heartbeat message can't be sent to a player, they are removed from the list of players - Added equals method for players Tags: #story[1047] --- .../gameServer/ClientConnectionDelegate.java | 10 +++ .../seng302/gameServer/GameServerThread.java | 68 ++++------------ .../seng302/gameServer/HeartbeatThread.java | 81 +++++++++++++++++++ .../gameServer/ServerListenThread.java | 3 +- src/main/java/seng302/models/Player.java | 24 +++++- .../messages/RaceStartStatusMessage.java | 11 ++- 6 files changed, 141 insertions(+), 56 deletions(-) create mode 100644 src/main/java/seng302/gameServer/HeartbeatThread.java diff --git a/src/main/java/seng302/gameServer/ClientConnectionDelegate.java b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java index dc0827fe..35d839bc 100644 --- a/src/main/java/seng302/gameServer/ClientConnectionDelegate.java +++ b/src/main/java/seng302/gameServer/ClientConnectionDelegate.java @@ -3,5 +3,15 @@ package seng302.gameServer; import seng302.models.Player; public interface ClientConnectionDelegate { + /** + * A player has connected to the server + * @param player The player that has connected + */ void clientConnected(Player player); + + /** + * A player has disconnected from the server + * @param player The player that has disconnected + */ + void clientDisconnected(Player player); } diff --git a/src/main/java/seng302/gameServer/GameServerThread.java b/src/main/java/seng302/gameServer/GameServerThread.java index d4e1f5a3..91d17cc2 100644 --- a/src/main/java/seng302/gameServer/GameServerThread.java +++ b/src/main/java/seng302/gameServer/GameServerThread.java @@ -26,7 +26,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel private long startTime; private short seqNum; - private final int HEARTBEAT_PERIOD = 5000; private final int RACE_STATUS_PERIOD = 1000/2; private final int RACE_START_STATUS_PERIOD = 1000; private final int BOAT_LOCATION_PERIOD = 1000/5; @@ -128,28 +127,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel 100, GameState.getPlayers().size(), RaceType.MATCH_RACE, 1, boatSubMessages); } - - /** - * Starts sending heartbeat messages to the client - */ - private void startSendingHeartbeats() { - Timer t = new Timer(); - - t.schedule(new TimerTask() { - @Override - public void run() { - Message heartbeat = new Heartbeat(seqNum); - - try { - System.out.println("Sending heartbeat"); - broadcast(heartbeat); - } catch (IOException e) { - e.printStackTrace(); - } - } - }, 0, HEARTBEAT_PERIOD); - } - /** * Start sending race start status messages until race starts */ @@ -164,7 +141,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel if (startTime < System.currentTimeMillis() && GameState.getCurrentStage() != GameStages.RACING){ } else{ - System.out.println("Sending race start status"); broadcast(raceStartStatusMessage); } @@ -204,15 +180,12 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel Message regatta = getXmlMessage("/server_config/regatta.xml", XMLMessageSubType.REGATTA); if (raceData != null){ - System.out.println("Sending RaceXML"); broadcast(raceData); } if (boatData != null){ - System.out.println("Sending boatsXML"); broadcast(boatData); } if (regatta != null){ - System.out.println("Sending regattaXML"); broadcast(regatta); } } catch (IOException e) { @@ -231,7 +204,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel try { Message raceData = getXmlMessage("/server_config/courseLimits.xml", XMLMessageSubType.RACE); if (raceData != null) { - System.out.println("Sending courseLimitsXML"); broadcast(raceData); } }catch (IOException e) { @@ -244,12 +216,17 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel public void run() { ServerListenThread serverListenThread; + HeartbeatThread heartbeatThread; Boolean serverIsSendingMessages = false; try{ server = ServerSocketChannel.open(); server.socket().bind(new InetSocketAddress("localhost", PORT_NUMBER)); + serverListenThread = new ServerListenThread(server, this); + heartbeatThread = new HeartbeatThread(this); + + heartbeatThread.start(); serverListenThread.start(); } catch (IOException e){ @@ -266,7 +243,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel if (GameState.getCurrentStage() == GameStages.RACING && !serverIsSendingMessages) { serverLog("Race Started", 0); - startSendingHeartbeats(); sendXml(); startSendingRaceStartStatusMessages(); //startSendingRaceStatusMessages(); @@ -279,7 +255,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel } startTime = System.currentTimeMillis() + TIME_TILL_RACE_START; - } } @@ -314,44 +289,35 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel @Override public void clientConnected(Player player) { if (GameState.getPlayers().size() < MAX_NUM_PLAYERS && GameState.getCurrentStage() == GameStages.LOBBYING) { - System.out.println(""); serverLog("Player Connected", 0); GameState.addPlayer(player); + sendXml(); } - sendXml(); } /** - * Listens for a connection and upon finding one, creates a Player object and adds it to the universal GameState + * A player has left the game, remove the player from the GameState + * @param player The player that left */ - private void acceptConnection() { - try { - SocketChannel thisClient = server.accept(); - if (thisClient.socket() != null){ - Player thisPlayer = new Player(thisClient); - GameState.addPlayer(thisPlayer); - } - } catch (IOException e) { - e.getMessage(); - } + @Override + public void clientDisconnected(Player player) { + serverLog("Player disconnected", 0); + GameState.removePlayer(player); + sendXml(); } - void unicast(Message message, SocketChannel client) throws IOException { - message.send(client); // TODO: 11/07/17 Do we incement seqNum for individual messages? + message.send(client); } - void broadcast(Message message) throws IOException{ for(Player player : GameState.getPlayers()) { - System.out.println("Sending message seqNo[" + seqNum + "] to Player: " + player.toString()); + //System.out.println("Sending message seqNo[" + seqNum + "] to Player: " + player.toString()); message.send(player.getSocketChannel()); } - seqNum++; // TODO: 11/07/17 Do we increment seqNum for every message or for the one message to everyone + seqNum++; } - - /** * Send a boat location message when they are updated by the simulator * @param o . @@ -388,8 +354,6 @@ public class GameServerThread implements Runnable, Observer, ClientConnectionDel }*/ } - - // if (numOfBoatsFinished == ((List) arg).size()) { // startSendingRaceFinishedBoatPositions(); // } diff --git a/src/main/java/seng302/gameServer/HeartbeatThread.java b/src/main/java/seng302/gameServer/HeartbeatThread.java new file mode 100644 index 00000000..e10a85f5 --- /dev/null +++ b/src/main/java/seng302/gameServer/HeartbeatThread.java @@ -0,0 +1,81 @@ +package seng302.gameServer; + +import seng302.models.Player; +import seng302.server.messages.Heartbeat; +import seng302.server.messages.Message; + +import java.io.IOException; +import java.util.*; + +/** + * Send Heartbeat messages to connected player at a specified interval + * Will call .clientDisconnected on the delegate when a heartbeat message + * cannot be sent to a player + */ +public class HeartbeatThread extends Thread{ + private final int HEARTBEAT_PERIOD = 5000; + private ClientConnectionDelegate delegate; + private Integer seqNum; + private Stack disconnectedPlayers; + + HeartbeatThread(ClientConnectionDelegate delegate){ + this.delegate = delegate; + seqNum = 0; + disconnectedPlayers = new Stack<>(); + } + + /** + * A player has lost connection to the server + * The player is added to a stack so that the delegate + * can be notified + * + * @param player The player that has disconnected + */ + private void playerLostConnection(Player player){ + disconnectedPlayers.push(player); + } + + /** + * Sends a heartbeat message to each connected player + * The delegate is notified if a player has disconnected + */ + private void sendHeartbeatToAllPlayers(){ + Message heartbeat = new Heartbeat(seqNum); + + for (Player player : GameState.getPlayers()){ + if (!player.getSocketChannel().isConnected()){ + playerLostConnection(player); + } + + try { + heartbeat.send(player.getSocketChannel()); + } catch (IOException e) { + playerLostConnection(player); + } + } + + updateDelegate(); + seqNum++; + } + + /** + * Notifies the delegate about + * each disconnected player + */ + private void updateDelegate() { + while (!disconnectedPlayers.empty()){ + delegate.clientDisconnected(disconnectedPlayers.pop()); + } + } + + public void run(){ + Timer t = new Timer(); + + t.schedule(new TimerTask() { + @Override + public void run() { + sendHeartbeatToAllPlayers(); + } + }, 0, HEARTBEAT_PERIOD); + } +} diff --git a/src/main/java/seng302/gameServer/ServerListenThread.java b/src/main/java/seng302/gameServer/ServerListenThread.java index 67ab285c..cfcbdcaa 100644 --- a/src/main/java/seng302/gameServer/ServerListenThread.java +++ b/src/main/java/seng302/gameServer/ServerListenThread.java @@ -1,6 +1,5 @@ package seng302.gameServer; -import com.sun.corba.se.spi.activation.Server; import seng302.models.Player; import java.io.IOException; @@ -15,7 +14,7 @@ public class ServerListenThread extends Thread{ private ServerSocketChannel socketChannel; private ClientConnectionDelegate delegate; - public ServerListenThread(ServerSocketChannel socketChannel, ClientConnectionDelegate delegate){ + ServerListenThread(ServerSocketChannel socketChannel, ClientConnectionDelegate delegate){ this.socketChannel = socketChannel; this.delegate = delegate; } diff --git a/src/main/java/seng302/models/Player.java b/src/main/java/seng302/models/Player.java index 6768eefd..fb062d0d 100644 --- a/src/main/java/seng302/models/Player.java +++ b/src/main/java/seng302/models/Player.java @@ -24,7 +24,6 @@ public class Player { return socketChannel; } - public Integer getLastMarkPassed() { return lastMarkPassed; } @@ -40,6 +39,11 @@ public class Player { @Override public String toString() { String playerAddress = null; + + if (socketChannel == null){ + return "Disconnected Player"; + } + try { playerAddress = socketChannel.getRemoteAddress().toString(); } catch (IOException e) { @@ -48,4 +52,22 @@ public class Player { return playerAddress; } + + @Override + public boolean equals(Object obj) { + if (obj == null){ + return false; + } + + if (!(obj instanceof Player)){ + return false; + } + + return ((Player) obj).socketChannel.equals(socketChannel); + } + + @Override + public int hashCode(){ + return socketChannel.hashCode(); + } } diff --git a/src/main/java/seng302/server/messages/RaceStartStatusMessage.java b/src/main/java/seng302/server/messages/RaceStartStatusMessage.java index 368a18fd..9aa886ee 100644 --- a/src/main/java/seng302/server/messages/RaceStartStatusMessage.java +++ b/src/main/java/seng302/server/messages/RaceStartStatusMessage.java @@ -54,6 +54,15 @@ public class RaceStartStatusMessage extends Message { writeCRC(); rewind(); - outputStream.write(getBuffer()); + if (outputStream == null){ + return; + } + + try{ + outputStream.write(getBuffer()); + } + catch (IOException e){ + return; + } } }