Add: PING PONG TCP

This commit is contained in:
rubenpirreram
2026-05-07 18:09:39 +02:00
parent 68be878ef1
commit 7757b38155
10 changed files with 388 additions and 160 deletions
@@ -0,0 +1,15 @@
package it.polimi.ingsw.gc14.Network;
//TODO
public class ClientPlayer {
private String username;
public String getUsername() {
return username;
}
public boolean connected;
public ClientPlayer(String username,boolean connected) {
this.username = username;
this.connected = connected;
}
}
@@ -1,7 +1,7 @@
package it.polimi.ingsw.gc14.Network.RMI.Server;
import it.polimi.ingsw.gc14.Controller.GameController;
import it.polimi.ingsw.gc14.LimitedList;
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.*;
@@ -44,9 +44,9 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
/**
* List containing the usernames of joined players.
* {@link LimitedList}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedList#setLimit(int)}.
* {@link LimitedMap}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedMap#setLimit(int)}.
*/
private LimitedList<String> playerList;
private LimitedMap<String,Boolean> playerList;
/**
@@ -59,7 +59,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
* @param host the host address of the RMI server.
* @throws RemoteException if an RMI error occurs.
*/
public RMIServer(GameController controller, int nPort, BlockingQueue<NetworkEvent> actionQueue,LimitedList<String> playerList,String host) throws RemoteException {
public RMIServer(GameController controller, int nPort, BlockingQueue<NetworkEvent> actionQueue, LimitedMap<String,Boolean> playerList, String host) throws RemoteException {
this.controller = controller;
this.nPort = nPort;
this.actionQueue = actionQueue;
@@ -89,7 +89,7 @@ public class RMIServer extends UnicastRemoteObject implements IGameServer {
}
if (controller.addPlayer(username)) {
clients.put(username, callback);
playerList.add(username);
playerList.put(username,true);
System.out.println("Accepted player: " + username);
return true;
}
@@ -8,11 +8,16 @@ import it.polimi.ingsw.gc14.Network.NetworkEvents.*;
import java.io.*;
import java.net.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Client TCP. Sends and receives messages with the TCP server.
*/
public class TCPClient implements IClient {
private static final int PING = 1;
private static final int PONG = 2;
/** Socket TCP */
Socket communicationSocket;
@@ -29,8 +34,14 @@ public class TCPClient implements IClient {
/** IP address of the server to connect to */
String hostname;
private boolean running;
/** TCP port */
int port;
int mainPort;
int heartbeatPort;
private Socket heartbeatSocket;
private OutputStream heartbeatOut;
private InputStream heartbeatIn;
/**
@@ -39,10 +50,11 @@ public class TCPClient implements IClient {
* @param hostname The IP address of the server
* @param port The TCP port of the server
*/
public TCPClient(ClientController controller, String hostname, int port) {
public TCPClient(ClientController controller, String hostname, int mainPort,int heartbeatPort ) {
this.controller = controller;
this.hostname = hostname;
this.port = port;
this.mainPort = mainPort;
this.heartbeatPort = heartbeatPort;
}
@@ -56,27 +68,78 @@ public class TCPClient implements IClient {
*/
public boolean connect(String user, int proposedNPlayers) {
try {
communicationSocket = new Socket(hostname, port);
socketSend = new ObjectOutputStream(communicationSocket.getOutputStream());
// Socket principale
communicationSocket = new Socket(hostname, mainPort);
socketSend = new ObjectOutputStream(communicationSocket.getOutputStream());
socketReceive = new ObjectInputStream(communicationSocket.getInputStream());
doEvent(new AddPlayer(user, proposedNPlayers));
if (communicationSocket.getInputStream().read() == -1) {
System.out.println("Could not connect to server");
return false;
} else {
Thread listener = new Thread(() -> receiveMessage());
listener.start();
return true;
}
// Socket heartbeat
this.heartbeatSocket = new Socket(hostname, heartbeatPort);
this.heartbeatOut =heartbeatSocket.getOutputStream() ;
this.heartbeatIn = heartbeatSocket.getInputStream();
// manda subito username per associare i due socket lato server
new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user);
heartbeatOut.flush();
running = true;
new Thread(this::receiveMessage, "tcp-reader").start();
new Thread(this::heartbeatLoop, "heartbeat").start();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
private void heartbeatLoop() {
// thread che manda ping ogni 3s
ScheduledExecutorService sender = Executors.newSingleThreadScheduledExecutor();
sender.scheduleAtFixedRate(() -> {
try {
heartbeatOut.write(PING);
heartbeatOut.flush();
} catch (IOException e) {
sender.shutdownNow();
disconnect();
}
}, 0, 3, TimeUnit.SECONDS);
// thread corrente: aspetta pong con timeout
try {
heartbeatSocket.setSoTimeout(5_000);
while (running) {
int b = heartbeatIn.read();
if (b == -1 || b != PONG) {
disconnect();
break;
}
// pong ricevuto → server vivo
}
} catch (SocketTimeoutException e) {
System.out.println("Server heartbeat timeout");
disconnect();
} catch (IOException e) {
disconnect();
} finally {
sender.shutdownNow();
}
}
private void disconnect() {
if (!running) return;
running = false;
try { communicationSocket.close(); } catch (IOException ignored) {}
try { heartbeatSocket.close(); } catch (IOException ignored) {}
controller.view.showError("Connessione al server persa");
}
/**
* Listens continuously for incoming objects from the server.
@@ -1,5 +1,6 @@
package it.polimi.ingsw.gc14.Network.TCP.Server;
import it.polimi.ingsw.gc14.LimitedMap;
import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
@@ -20,6 +21,8 @@ public class ClientHandler implements Runnable {
*/
private final String username;
private boolean running ;
/**
* Returns the username associated with this client.
*
@@ -43,6 +46,9 @@ public class ClientHandler implements Runnable {
*/
List<ClientHandler> clientHandlers;
//TODO
LimitedMap<String,Boolean> limitedMap;
/** Queue containing the events to be applied to the game model */
BlockingQueue<NetworkEvent> actionQueue;
@@ -57,7 +63,7 @@ public class ClientHandler implements Runnable {
* @param clientHandlers the shared list of all active client handlers.
* @param actionQueue the queue containing incoming events.
*/
public ClientHandler(String username, Socket clientSocket, ObjectOutputStream out, ObjectInputStream in, List<ClientHandler> clientHandlers, BlockingQueue<NetworkEvent> actionQueue) {
public ClientHandler(String username, Socket clientSocket, ObjectOutputStream out, ObjectInputStream in, List<ClientHandler> clientHandlers, LimitedMap<String,Boolean> playersMap, BlockingQueue<NetworkEvent> actionQueue) {
this.username=username;
this.clientSocket = clientSocket;
this.in = in;
@@ -74,8 +80,8 @@ public class ClientHandler implements Runnable {
@Override
public void run() {
try {
while (true) {
running = true;
while (running) {
NetworkEvent event = (NetworkEvent) in.readObject();
if (!actionQueue.add(event)) {
System.out.println("Error inserting action into queue");
@@ -90,6 +96,7 @@ public class ClientHandler implements Runnable {
}
/**
* Sends a {@link NetworkEvent} to the client.
* @param event The network event to send to the client.
@@ -115,4 +122,11 @@ public class ClientHandler implements Runnable {
e.printStackTrace();
}
}
public void disconnect() {
if (!running) return;
running = false;
clientHandlers.remove(this);
try { clientSocket.close(); } catch (IOException ignored) {}
}
}
@@ -0,0 +1,75 @@
package it.polimi.ingsw.gc14.Network.TCP.Server;
import java.io.*;
import java.net.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class HeartbeatHandler implements Runnable {
private static final long SILENCE_THRESHOLD_MS = 5_000;
private static final long KEEPALIVE_INTERVAL_MS = 3_000;
private final String username;
private final Socket socket;
private final InputStream in;
private final OutputStream out;
private static final int PING = 1;
private static final int PONG = 2;
// riferimento al ClientHandler principale per disconnetterlo insieme
private final ClientHandler mainHandler;
private volatile long lastReceivedTime = System.currentTimeMillis();
private volatile boolean running = true;
private final ScheduledExecutorService watchdog =
Executors.newSingleThreadScheduledExecutor();
public HeartbeatHandler(String username, Socket socket, ClientHandler mainHandler)
throws IOException {
this.username = username;
this.socket = socket;
this.mainHandler = mainHandler;
this.in = socket.getInputStream();
this.out = socket.getOutputStream();
}
@Override
public void run() {
startWatchdog();
try {
while (running) {
int b = in.read(); // blocca finché non arriva un byte
if (b == -1) { disconnect(); break; } // stream chiusa
if (b == PING) {
lastReceivedTime = System.currentTimeMillis();
out.write(PONG);
out.flush();
}
}
} catch (IOException e) {
disconnect();
}
}
private void startWatchdog() {
watchdog.scheduleAtFixedRate(() -> {
if (System.currentTimeMillis() - lastReceivedTime > SILENCE_THRESHOLD_MS) {
System.out.println("Heartbeat timeout: " + username);
disconnect();
}
}, 1, 1, TimeUnit.SECONDS);
}
private void disconnect() {
if (!running) return;
running = false;
watchdog.shutdownNow();
mainHandler.disconnect(); // disconnette anche il socket principale
try { socket.close(); } catch (IOException ignored) {}
}
}
@@ -1,8 +1,9 @@
package it.polimi.ingsw.gc14.Network.TCP.Server;
import it.polimi.ingsw.gc14.Controller.GameController;
import it.polimi.ingsw.gc14.LimitedList;
import it.polimi.ingsw.gc14.LimitedMap;
import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Network.ClientPlayer;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;
@@ -11,151 +12,175 @@ import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
/**
* Server TCP. Accepts connections and manages all client handlers.
*/
public class TCPServer {
/** TCP port */
int port;
int heartbeatPort; // ← nuova porta
/** Number of currently connected clients */
int ConnectedPlayers;
/** Socket TCP */
int connectedPlayers;
ServerSocket socketTCP;
ServerSocket heartbeatSocketTCP; // ← nuovo ServerSocket
/** Server game's controller */
GameController controller;
/** Queue containing the events to be applied to the game model */
BlockingQueue<NetworkEvent> actionQueue;
LimitedMap<String, Boolean> playerList;
List<ClientHandler> clientHandlers;
/**
* List containing the usernames of joined players.
* {@link LimitedList}'s limit defines at which size the list calls its action. The limit can be set using {@link LimitedList#setLimit(int)}.
*/
private LimitedList<String> playerList;
// Mappa temporanea: username → ClientHandler
// Serve per associare il socket heartbeat al giusto ClientHandler
private final Map<String, ClientHandler> pendingHeartbeat = new ConcurrentHashMap<>();
/** List containing all client's handlers */
private List<ClientHandler> clientHandlers;
/**
* Class constructor that initializes the attributes.
* @param controller The game controller
* @param port The TCP port
* @param actionQueue The action queue
* @param playerList The player's usernames list
*/
public TCPServer(GameController controller, int port, BlockingQueue<NetworkEvent> actionQueue, LimitedList<String> playerList){
public TCPServer(GameController controller, int port, int heartbeatPort,
BlockingQueue<NetworkEvent> actionQueue, LimitedMap<String, Boolean> playerList) {
this.port = port;
this.ConnectedPlayers = 0;
this.socketTCP = null;
this.heartbeatPort = heartbeatPort;
this.connectedPlayers = 0;
this.controller = controller;
this.actionQueue = actionQueue;
this.playerList = playerList;
this.clientHandlers = new ArrayList<>();
}
/**
* Starts the TCP server.
* If the first event is not AddPlayer, the request is rejected.
* If the desired number of player is invalid, the request is rejected.
* If this is the first player to connect, a new game model is created and passed to the controller. Additionally, the playerList's limit is set.
* If the controller successfully adds the player, the username is added to {@link #playerList} and the handler is added to {@link #clientHandlers}.
* If any error occurs, the server sends -1 back to the client. Otherwise, it sends 1.
*/
public void start(){
try{
public void start() {
try {
socketTCP = new ServerSocket(port);
}
catch (IOException e){
System.out.println("Could not start the server TCP on port: " + port);
heartbeatSocketTCP = new ServerSocket(heartbeatPort);
} catch (IOException e) {
System.out.println("Could not start TCP server");
e.printStackTrace();
return;
}
System.out.println("Server TCP started on port: " + port);
Socket clientSocket;
while(true){
System.out.println("TCP server started on port: " + port);
System.out.println("Heartbeat server started on port: " + heartbeatPort);
try{
clientSocket = socketTCP.accept();
// Thread separato per accettare le connessioni heartbeat
new Thread(this::acceptHeartbeat, "heartbeat-acceptor").start();
// Loop principale — invariato nella logica, cambia solo la creazione del ClientHandler
while (true) {
try {
Socket clientSocket = socketTCP.accept();
ObjectOutputStream clientSend = new ObjectOutputStream(clientSocket.getOutputStream());
ObjectInputStream clientReceive = new ObjectInputStream(clientSocket.getInputStream());
NetworkEvent event = (NetworkEvent) clientReceive.readObject();
if(!(event.getEventType() == EventType.ADD_PLAYER)){
clientSocket.getOutputStream().write((int)(-1));
if (!(event.getEventType() == EventType.ADD_PLAYER)) {
clientSocket.getOutputStream().write(-1);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.\n");
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
else{
AddPlayer eventAddPlayer = (AddPlayer) event;
if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) {
clientSocket.getOutputStream().write((int) (-1));
AddPlayer eventAddPlayer = (AddPlayer) event;
if (eventAddPlayer.getProposedNPlayer() < 2 || eventAddPlayer.getProposedNPlayer() > 5) {
clientSocket.getOutputStream().write(-1);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
synchronized (controller) {
if (playerList.isEmpty()) {
Game model = new Game(eventAddPlayer.getProposedNPlayer());
controller.setModel(model);
playerList.setLimit(eventAddPlayer.getProposedNPlayer());
}
String username = eventAddPlayer.getUsername();
if (controller.addPlayer(username)) {
// nuovo giocatore
playerList.put(username, true);
clientSocket.getOutputStream().write(1);
System.out.println("Accepted player: " + username);
ClientHandler handler = new ClientHandler(
username, clientSocket, clientSend, clientReceive,
clientHandlers,playerList, actionQueue
);
clientHandlers.add(handler);
connectedPlayers++;
// metti in attesa del socket heartbeat
pendingHeartbeat.put(username, handler);
} else if (playerList.containsKey(username) && !playerList.get(username)) {
// riconnessione
playerList.put(username, true);
clientSocket.getOutputStream().write(1);
System.out.println("Reconnected player: " + username);
ClientHandler handler = new ClientHandler(
username, clientSocket, clientSend, clientReceive,
clientHandlers, playerList, actionQueue
);
handler.notifyModel(controller.getModel());
clientHandlers.add(handler);
connectedPlayers++;
pendingHeartbeat.put(username, handler);
} else {
clientSocket.getOutputStream().write(-1);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.\n");
}
synchronized (controller) {
if (playerList.isEmpty()){
Game model = new Game(eventAddPlayer.getProposedNPlayer());
controller.setModel(model);
playerList.setLimit(eventAddPlayer.getProposedNPlayer());
}
if (controller.addPlayer(eventAddPlayer.getUsername())) {
playerList.add(eventAddPlayer.getUsername());
clientSocket.getOutputStream().write((int) (1));
System.out.println("Accepted player: " + eventAddPlayer.getUsername());
ClientHandler clientHandler = new ClientHandler(eventAddPlayer.getUsername(),clientSocket, clientSend, clientReceive, clientHandlers, actionQueue);
clientHandlers.add(clientHandler);
ConnectedPlayers++;
Thread t = new Thread(clientHandler);
t.start();
} else {
clientSocket.getOutputStream().write((int) (-1));
clientSocket.close();
System.out.println("Player could not be added. Connection terminated.\n");
}
System.out.println("Player could not be added. Connection terminated.");
}
}
}
catch(IOException e){
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
catch(ClassNotFoundException e){
throw new RuntimeException(e);
}
}
}
/**
* Sends an action to all TCP clients.
*
* @param event the network event to send to all connected TCP clients.
* Accetta connessioni sul socket heartbeat e le associa al ClientHandler giusto.
* Il client manda subito il proprio username per identificarsi.
*/
public void notifyAll(NetworkEvent event){
clientHandlers.forEach((x) -> {
if(!event.getIsError()||(event.getIsError()&& event.getUsername().equals(x.getUsername())))
x.notifyEvent(event);
private void acceptHeartbeat() {
while (true) {
try {
Socket hbSocket = heartbeatSocketTCP.accept();
ObjectInputStream hbIn = new ObjectInputStream(hbSocket.getInputStream());
// il client manda subito il suo username
String username = (String) hbIn.readObject();
ClientHandler handler = pendingHeartbeat.remove(username);
if (handler != null) {
HeartbeatHandler hb = new HeartbeatHandler(username, hbSocket, handler);
new Thread(hb, "heartbeat-" + username).start();
System.out.println("Heartbeat connected for: " + username);
} else {
System.out.println("No pending handler for: " + username + ", closing heartbeat.");
hbSocket.close();
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
public void notifyAll(NetworkEvent event) {
clientHandlers.forEach(h -> {
if (!event.getIsError() || event.getUsername().equals(h.getUsername()))
h.notifyEvent(event);
});
}
/**
* Sends a game model to all TCP clients.
*
* @param model the game model to send to all connected TCP clients.
*/
public void notifyAll(Game model){
clientHandlers.forEach((x) -> x.notifyModel(model));
public void notifyAll(Game model) {
clientHandlers.forEach(h -> h.notifyModel(model));
}
}
}