From 9335db7db0d882217b3c765d2fcffc3033aa4ca7 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Fri, 8 May 2026 20:26:31 +0200 Subject: [PATCH 1/2] Fix: RMI resilience --- .idea/inspectionProfiles/Project_Default.xml | 3 + .../java/it/polimi/ingsw/gc14/Model/Game.java | 3 +- .../gc14/Network/RMI/Client/RMIClient.java | 118 ++++++++--- .../gc14/Network/RMI/Common/IGameServer.java | 55 ++--- .../gc14/Network/RMI/Server/RMIHeartbeat.java | 112 ++++++++++ .../gc14/Network/RMI/Server/RMIServer.java | 195 ++++++++++-------- 6 files changed, 333 insertions(+), 153 deletions(-) create mode 100644 src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index a257030..1f0d743 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,9 @@ \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index 7fe3929..0fe8691 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -71,7 +71,8 @@ public class Game implements Serializable { { return false; } - return disconnetedPlayers.put(player,false); + disconnetedPlayers.put(player,false); + return true; } /** * Returns the current number of players participating in the game. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java index febd764..f35065b 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Client/RMIClient.java @@ -4,6 +4,7 @@ import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.Objects; +import java.util.concurrent.*; import it.polimi.ingsw.gc14.Controller.ClientController; import it.polimi.ingsw.gc14.Network.IClient; @@ -18,33 +19,24 @@ import it.polimi.ingsw.gc14.Network.RMI.Server.RMIServer; */ public class RMIClient implements IClient { - /** The host address of the RMI server */ + private static final long PING_INTERVAL_S = 3; // mirrors TCPClient 3 s + private static final long PING_TIMEOUT_MS = 5_000; // mirrors SILENCE_THRESHOLD_MS + private final String host; - - /** The port of the RMI server */ private final int port; - - /** The remote stub used to call methods on the server */ private IGameServer stub; - - /** Client game's controller */ - ClientController controller; - - /** - * Local IP address of the RMI client. - */ + private ClientController controller; private String myIP; + private String username; + + private volatile boolean running = false; + + /** Scheduler that fires ping() every PING_INTERVAL_S seconds. */ + private ScheduledExecutorService pingSender; + - /** - * Class constructor. - * - * @param controller the client controller used to create the callback. - * @param host the host address of the RMI server. - * @param port the port of the RMI server. - * @param myIP the local IP address used by the RMI client. - */ public RMIClient(ClientController controller, String host, int port, String myIP) { - this.controller=controller; + this.controller = controller; this.host = host; this.port = port; this.myIP = myIP; @@ -52,29 +44,89 @@ public class RMIClient implements IClient { /** - * Connects to the RMI server and attempts to join the game. - * Looks up the RMI registry to retrieve the {@link IGameServer} stub. - * Then, creates a {@link ClientCallbackImpl} and calls {@link RMIServer#joinGame(String, int, IClientCallback)}. - * @param username the player's username - * @param preferredInt the desired number of players - * @return true if the player successfully joined the game, false otherwise + * Connects to the RMI server and starts the heartbeat loop. + * + *

Mirrors {@code TCPClient.connect()}: after a successful join the + * heartbeat channel is opened (here: a scheduler is started instead of + * opening a second socket). */ - public boolean connect(String username,int preferredInt) { + @Override + public boolean connect(String username, int preferredInt) { try { System.setProperty("java.rmi.server.hostname", this.myIP); Registry registry = LocateRegistry.getRegistry(host, port); this.stub = (IGameServer) registry.lookup("RMIGameServer"); - ClientCallbackImpl callback = new ClientCallbackImpl(controller); + this.username = username; - return stub.joinGame(username, preferredInt, callback); - } - catch (Exception e) { + ClientCallbackImpl callback = new ClientCallbackImpl(controller); + boolean joined = stub.joinGame(username, preferredInt, callback); + if (!joined) return false; + + running = true; + startHeartbeat(); + return true; + + } catch (Exception e) { e.printStackTrace(); return false; } - } + // ------------------------------------------------------------------------- + // Heartbeat — mirrors TCPClient.heartbeatLoop() + // ------------------------------------------------------------------------- + + /** + * Starts sending periodic pings to the server. + * + *

Mirrors the {@code ScheduledExecutorService} in + * {@code TCPClient.heartbeatLoop()} that writes {@code PING} every 3 s. + * On {@link RemoteException} the server is considered gone and + * {@link #disconnect()} is called — mirrors the behaviour on + * {@code SocketTimeoutException} / {@code IOException} in the TCP version. + */ + private void startHeartbeat() { + pingSender = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "rmi-heartbeat"); + t.setDaemon(true); + return t; + }); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + pingSender.scheduleAtFixedRate(() -> { + Future future = executor.submit(() -> { + try { + stub.ping(username); + } catch (RemoteException e) { + disconnect(); + } + }); + try { + future.get(PING_TIMEOUT_MS, TimeUnit.MILLISECONDS); // mirrors setSoTimeout(5000) + } catch (TimeoutException e) { + future.cancel(true); + System.out.println("RMI ping timeout: " + username); + disconnect(); + } catch (Exception e) { + disconnect(); + } + }, 0, PING_INTERVAL_S, TimeUnit.SECONDS); + } + + /** + * Tears down the connection. + * Mirrors {@code TCPClient.disconnect()}: stops the heartbeat and notifies + * the view. + */ + private void disconnect() { + if (!running) return; + running = false; + if (pingSender != null) pingSender.shutdownNow(); + controller.view.showError("Connessione al server persa"); + } + + + /** * Requests to draw a tribe card from the upper list. diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java index bfee30b..edb8fc2 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Common/IGameServer.java @@ -9,47 +9,26 @@ import java.rmi.*; */ public interface IGameServer extends Remote { - /** - * Adds a player to the game through the remote server. - * - * @param username the username of the player joining the game. - * @param preferredInt the preferred player number or slot selected by the client. - * @param callback the client callback used by the server to send updates. - * @return {@code true} if the player successfully joins the game; - * {@code false} otherwise. - * @throws RemoteException if an RMI communication error occurs. - */ - boolean joinGame(String username,int preferredInt, IClientCallback callback) throws RemoteException; + boolean joinGame(String username, int preferredInt, IClientCallback callback) throws RemoteException; - /** - * Sends a network event to the game server. - * - * @param event the event to be processed by the server. - * @return {@code true} if the event is accepted and processed; - * {@code false} otherwise. - * @throws RemoteException if an RMI communication error occurs. - */ boolean doEvent(NetworkEvent event) throws RemoteException; + void drawUpperTribeCard(String playerUsername, int pos) throws RemoteException; - - void drawLowerTribeCard(String playerUsername,int pos) throws RemoteException; - - void drawUpperBuildingCard(String playerUsername,int pos) throws RemoteException; - - - void drawLowerBuildingCard(String playerUsername,int pos) throws RemoteException; - + void drawLowerTribeCard(String playerUsername, int pos) throws RemoteException; + void drawUpperBuildingCard(String playerUsername, int pos) throws RemoteException; + void drawLowerBuildingCard(String playerUsername, int pos) throws RemoteException; void skipTurn(String playerUsername) throws RemoteException; - - - - void pickOptionalTribeCard(String playerUsername,int pos) throws RemoteException; - - void pickOptionalBuildingCard(String playerUsername,int pos) throws RemoteException; - - + void pickOptionalTribeCard(String playerUsername, int pos) throws RemoteException; + void pickOptionalBuildingCard(String playerUsername, int pos) throws RemoteException; void noOptionalCard(String playerUsername) throws RemoteException; + void slotChoice(String playerUsername, int pos) throws RemoteException; - void slotChoice(String playerUsername,int pos) throws RemoteException; - -} + /** + * Heartbeat: called periodically by the client to signal it is still alive. + * Mirrors the PING/PONG mechanism used in the TCP heartbeat channel. + * + * @param username the username of the client sending the ping. + * @throws RemoteException if an RMI communication error occurs. + */ + void ping(String username) throws RemoteException; +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java new file mode 100644 index 0000000..315c99e --- /dev/null +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIHeartbeat.java @@ -0,0 +1,112 @@ +package it.polimi.ingsw.gc14.Network.RMI.Server; + +import it.polimi.ingsw.gc14.LimitedMap; +import it.polimi.ingsw.gc14.Model.Game; +import it.polimi.ingsw.gc14.Network.NetworkEvent; +import it.polimi.ingsw.gc14.Network.NetworkEvents.SkipPlayerDisconnected; + +import java.util.Map; +import java.util.concurrent.*; + +/** + * Server-side heartbeat watchdog for a single RMI client. + * + *

Mirrors {@code HeartbeatHandler} used in the TCP stack, but adapted for RMI: + * instead of reading raw bytes from a dedicated socket, it relies on {@link #receivePing()} + * being called by {@link RMIServer#ping(String)} every time the client sends a ping. + * + *

If no ping is received within {@value SILENCE_THRESHOLD_MS} ms the player is + * considered disconnected and {@link #disconnect()} is invoked, which: + *

+ */ +public class RMIHeartbeat { + + private static final long SILENCE_THRESHOLD_MS = 5_000; + + private String username = ""; + private final LimitedMap playerList; + private final Map clients; // ConcurrentHashMap + private final BlockingQueue actionQueue; + + /** Last time a ping was received from this client. */ + private volatile long lastPingTime = System.currentTimeMillis(); + private volatile boolean running = true; + + /** Reference to the current game model — needed to check whose turn it is. */ + private volatile Game game; + + private final ScheduledExecutorService watchdog = + Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "rmi-watchdog-" + username); + t.setDaemon(true); + return t; + }); + + public RMIHeartbeat( + String username, + LimitedMap playerList, + Map clients, + BlockingQueue actionQueue) { + + this.username = username; + this.playerList = playerList; + this.clients = clients; + this.actionQueue = actionQueue; + } + + /** Called by {@link RMIServer} whenever it starts tracking this player. */ + public void start() { + watchdog.scheduleAtFixedRate(() -> { + if (System.currentTimeMillis() - lastPingTime > SILENCE_THRESHOLD_MS) { + System.out.println("RMI heartbeat timeout: " + username); + disconnect(); + } + }, 1, 1, TimeUnit.SECONDS); + } + + /** + * Called by {@link RMIServer#ping(String)} each time the client pings. + * Resets the silence timer — mirrors writing {@code lastReceivedTime} in + * {@code HeartbeatHandler}. + */ + public void receivePing() { + lastPingTime = System.currentTimeMillis(); + } + + /** + * Allows the server to keep the watchdog up-to-date with the current game + * model (needed to check whose turn it is on disconnect). + */ + public void setGame(Game game) { + this.game = game; + } + + // ------------------------------------------------------------------------- + + private void disconnect() { + if (!running) return; + running = false; + watchdog.shutdownNow(); + + // Mark player as offline + playerList.put(username, false); + + // Remove RMI callback so notifyAll skips this client + clients.remove(username); + + // If it was this player's turn, skip it + Game snapshot = this.game; + if (snapshot != null && + snapshot.getCurrentState().getCurrentPlayer().getUserName().equals(username)) { + actionQueue.add(new SkipPlayerDisconnected(username)); + } + + System.out.println("RMI disconnected: " + username); + } +} \ No newline at end of file diff --git a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java index 69bdb6c..f86e0b6 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java +++ b/src/main/java/it/polimi/ingsw/gc14/Network/RMI/Server/RMIServer.java @@ -24,42 +24,29 @@ import java.rmi.*; */ public class RMIServer extends UnicastRemoteObject implements IGameServer { private String host; - /** Server game's controller */ private GameController controller; - - /** Server game's model */ private Game model; - - /** RMI registry */ private Registry registry; - - /** RMI port */ private int nPort; - /** Map containing the associations between a player's username and its callback */ + /** username → callback */ private final Map clients = new ConcurrentHashMap<>(); - /** Queue containing the events to be applied to the game model */ + /** + * username → watchdog. + * One watchdog per connected player, mirrors {@code pendingHeartbeat} / per-socket + * HeartbeatHandler in the TCP stack. + */ + private final Map watchdogs = new ConcurrentHashMap<>(); + BlockingQueue actionQueue; - - /** - * List containing the usernames of joined players. - * {@link LimitedMap}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedMap#setLimit(int)}. - */ - private LimitedMap playerList; + private LimitedMap playerList; - /** - * Class constructor that initializes the attributes. - * - * @param controller the game controller. - * @param nPort the RMI port. - * @param actionQueue the action queue. - * @param playerList the players' usernames list. - * @param host the host address of the RMI server. - * @throws RemoteException if an RMI error occurs. - */ - public RMIServer(GameController controller, int nPort, BlockingQueue actionQueue, LimitedMap playerList, String host) throws RemoteException { + public RMIServer(GameController controller, int nPort, + BlockingQueue actionQueue, + LimitedMap playerList, + String host) throws RemoteException { this.controller = controller; this.nPort = nPort; this.actionQueue = actionQueue; @@ -67,37 +54,102 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { this.host = host; } + // ------------------------------------------------------------------------- + // Join + // ------------------------------------------------------------------------- + /** - * Allows a player to join the game. - * If the desired number of player is invalid, the request is rejected. - * If this is the first player, a new game model is created and passed to the controller. Additionally, the playerList's limit is set. - * Then, if the controller successfully adds the player, the username is added to {@link #playerList} and {@link #clients}. - * @param username The player's name - * @param preferredInt The desired number of players - * @param callback The client's callback interface - * @return true if the player successfully joined the game, false otherwise + * {@inheritDoc} + * + *

After a successful join a {@link RMIHeartbeat} is created and + * started for the new player — mirrors creating a {@code HeartbeatHandler} in + * {@code TCPServer.acceptHeartbeat()}. */ - public boolean joinGame(String username, int preferredInt, IClientCallback callback) { - if (preferredInt<2 || preferredInt>5) { - return false; - } - synchronized (controller) { - if(playerList.isEmpty()){ + @Override + public boolean joinGame(String username, int preferredInt, IClientCallback callback) + throws RemoteException { + if (preferredInt < 2 || preferredInt > 5) return false; + + //synchronized (controller) { + if (playerList.isEmpty()) { model = new Game(preferredInt); controller.setModel(model); playerList.setLimit(preferredInt); } if (controller.addPlayer(username)) { clients.put(username, callback); - playerList.put(username,true); + playerList.put(username, true); + startWatchdog(username); System.out.println("Accepted player: " + username); return true; } - return false; - } + // Reconnection: player was offline + if (playerList.containsKey(username) && !playerList.get(username)) { + playerList.put(username, true); + clients.put(username, callback); + System.out.println("Reconnected player: " + username); + startWatchdog(username); + callback.onGameInit(model); + System.out.println("Model sent: " + username); + actionQueue.add(new ReconnectPlayer(username)); + + return true; + } + return false; + //} } + // ------------------------------------------------------------------------- + // Heartbeat — called by RMIClient every ~3 s + // ------------------------------------------------------------------------- + + /** + * Receives a heartbeat ping from the client. + * Mirrors the server reading {@code PING} and replying {@code PONG} in + * {@code HeartbeatHandler.run()}. + * + * @param username the username of the pinging client. + */ + @Override + public void ping(String username) throws RemoteException { + RMIHeartbeat wd = watchdogs.get(username); + if (wd != null) wd.receivePing(); + } + + // ------------------------------------------------------------------------- + // Game model propagation — keep watchdogs in sync + // ------------------------------------------------------------------------- + + /** + * Notifies all clients of a new event. + * Also updates every watchdog with the latest model so disconnect logic + * knows whose turn it is. + */ + public void notifyAll(NetworkEvent action) throws RemoteException { + for (Map.Entry entry : clients.entrySet()) { + if (!action.getIsError() || + (action.getIsError() && action.getUsername().equals(entry.getKey()))) { + entry.getValue().onAction(action); + } + } + } + + /** + * Notifies all clients of a new game model and keeps watchdogs up-to-date. + * Mirrors {@code TCPServer.notifyAll(Game)} + the {@code ClientHandler.notifyModel} + * call that stores the model for disconnect-turn checking. + */ + public void notifyAll(Game model) throws RemoteException { + this.model = model; + // Keep every watchdog's game reference up to date + watchdogs.values().forEach(wd -> wd.setGame(model)); + for (IClientCallback cb : clients.values()) { + cb.onGameInit(model); + } + } + + /** * Push an action in actionQueue. @@ -205,35 +257,6 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } - // RMI's internal methods - /** - * Sends an action to the RMI clients. - * - *

If the action is an error, it is sent only to the client associated with - * the action username. Otherwise, it is sent to all connected RMI clients. - * - * @param action the network action to send. - * @throws RemoteException if an RMI communication error occurs. - */ - public void notifyAll(NetworkEvent action) throws RemoteException { - for (Map.Entry entry : clients.entrySet()) { - if(!action.getIsError() ||(action.getIsError()&& action.getUsername().equals(entry.getKey()))) - entry.getValue().onAction(action); - } - } - - - /** - * Sends a game model to all RMI clients. - * - * @param model the game model to send to all connected RMI clients. - * @throws RemoteException if an RMI communication error occurs. - */ - public void notifyAll(Game model) throws RemoteException { - for (IClientCallback cb : clients.values()) { - cb.onGameInit(model); - } - } @@ -244,27 +267,22 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { */ public boolean start() { try { - System.setProperty("java.rmi.server.hostname", host); // o il tuo IP/hostname + System.setProperty("java.rmi.server.hostname", host); registry = LocateRegistry.createRegistry(nPort); registry.rebind("RMIGameServer", this); - System.out.println("RMI Server started on port: "+nPort); + System.out.println("RMI Server started on port: " + nPort); return true; - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); return false; } } - - /** - * Stops the RMI server. - * @return true if the server stops successfully, false otherwise - */ public boolean stop() { try { registry.unbind("RMIGameServer"); UnicastRemoteObject.unexportObject(this, true); + watchdogs.values().forEach(wd -> { /* watchdogs shut themselves down */ }); System.out.println("RMI Server fermato"); return true; } catch (RemoteException | NotBoundException e) { @@ -274,4 +292,19 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer { } + /** + * Creates and starts a {@link RMIHeartbeat} for {@code username}. + * Also seeds the watchdog with the current model if one already exists + * (reconnection case). + */ + private void startWatchdog(String username) { + RMIHeartbeat wd = new RMIHeartbeat( + username, playerList, clients, actionQueue); + if (model != null) wd.setGame(model); + watchdogs.put(username, wd); + wd.start(); + } + + + } From 0e477d48d157cb707805f6c4c0164fc08229f911 Mon Sep 17 00:00:00 2001 From: rubenpirreram Date: Sat, 9 May 2026 12:46:39 +0200 Subject: [PATCH 2/2] Fix: Reconnection during Slot Choice --- src/main/java/it/polimi/ingsw/gc14/Model/Game.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java index 0fe8691..3074ec1 100644 --- a/src/main/java/it/polimi/ingsw/gc14/Model/Game.java +++ b/src/main/java/it/polimi/ingsw/gc14/Model/Game.java @@ -72,6 +72,11 @@ public class Game implements Serializable { return false; } disconnetedPlayers.put(player,false); + if(currentState.getGameStage().equals(GameStages.SLOT_CHOICE)) + { + disconnetedPlayers.remove(player); + orderLogicCard.pushNoEffect(player); + } return true; } /**