Coverage Summary for Class: TCPServer (it.polimi.ingsw.gc14.Network.TCP.Server)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| TCPServer |
0%
(0/1)
|
0%
(0/8)
|
0%
(0/30)
|
0%
(0/105)
|
package it.polimi.ingsw.gc14.Network.TCP.Server;
import it.polimi.ingsw.gc14.Controller.GameController;
import it.polimi.ingsw.gc14.ErrorType;
import it.polimi.ingsw.gc14.LimitedMap;
import it.polimi.ingsw.gc14.Model.Game;
import it.polimi.ingsw.gc14.Model.GamePackage.GameStages;
import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.EventType;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkEvents.AddPlayer;
import it.polimi.ingsw.gc14.Network.NetworkEvents.ReconnectPlayer;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* TCP server responsible for accepting client connections,
* handling player registration and reconnection, and sending
* game updates to connected clients.
*
* <p>The server also manages a dedicated heartbeat channel used
* to detect disconnected clients and associate each heartbeat
* connection with the corresponding {@link ClientHandler}.
*/
public class TCPServer {
/**
* Main TCP port used for standard client-server communication.
*/
private final int port;
/**
* TCP port dedicated to heartbeat communication.
*/
private final int heartbeatPort;
/**
* Main server socket used to accept client connections.
*/
private ServerSocket socketTCP;
/**
* Server socket used to accept heartbeat connections.
*/
private ServerSocket heartbeatSocketTCP;
/**
* Game controller used to manage the server-side game logic.
*/
private final GameController controller;
/**
* Queue containing network events received from clients.
*/
private final BlockingQueue<NetworkEvent> actionQueue;
/**
* Map storing the online/offline status of connected players.
*/
private final LimitedMap<String, Boolean> playerList;
/**
* List of active TCP client handlers.
*/
private final CopyOnWriteArrayList<ClientHandler> clientHandlers;
/**
* Temporary map associating each username with the corresponding
* {@link ClientHandler} waiting for its heartbeat connection.
*/
private final Map<String, ClientHandler> pendingHeartbeat = new ConcurrentHashMap<>();
/**
* Constructs a TCP server with the required game and network components.
*
* @param controller the game controller used to manage the game logic.
* @param port the main TCP port used for client communication.
* @param heartbeatPort the TCP port dedicated to heartbeat connections.
* @param actionQueue the queue containing incoming network events.
* @param playerList the map storing the connection status of the players.
*/
public TCPServer(GameController controller,
int port,
int heartbeatPort,
BlockingQueue<NetworkEvent> actionQueue,
LimitedMap<String, Boolean> playerList) {
this.port = port;
this.heartbeatPort = heartbeatPort;
this.controller = controller;
this.actionQueue = actionQueue;
this.playerList = playerList;
this.clientHandlers = new CopyOnWriteArrayList<>();
}
/**
* Starts the TCP server and begins accepting client connections.
*
* <p>The method opens both the main TCP server socket and the dedicated
* heartbeat server socket. It then starts a separate thread for heartbeat
* connections and continuously waits for new players or reconnecting clients.
*
* <p>When a new connection is received, the first event must be an
* {@link AddPlayer} request. Depending on the current server state,
* the connection is handled either as a new player joining the game
* or as a reconnection attempt.
*
*/
public void start() {
try {
socketTCP = new ServerSocket(port);
heartbeatSocketTCP = new ServerSocket(heartbeatPort);
} catch (IOException e) {
System.out.println("Could not start TCP server");
e.printStackTrace();
return;
}
System.out.println("TCP server started on port: " + port);
System.out.println("TCP heartbeat server started on port: " + heartbeatPort);
new Thread(this::acceptHeartbeat, "heartbeat-acceptor").start();
while (true) {
try {
Socket clientSocket = socketTCP.accept();
ObjectOutputStream clientSend = new ObjectOutputStream(clientSocket.getOutputStream());
clientSend.flush();
ObjectInputStream clientReceive = new ObjectInputStream(clientSocket.getInputStream());
NetworkEvent event = (NetworkEvent) clientReceive.readObject();
if (!(event.getEventType() == EventType.ADD_PLAYER)) {
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
AddPlayer eventAddPlayer = (AddPlayer) event;
if (eventAddPlayer.getProposedNPlayer() < NetworkConfig.MIN_PLAYERS
|| eventAddPlayer.getProposedNPlayer() > NetworkConfig.MAX_PLAYERS) {
eventAddPlayer.setErrorType(ErrorType.WRONG_PLAYER_NUMBER);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
synchronized (controller) {
String username = eventAddPlayer.getUsername();
//reconnect players after a server crash
if (playerList.isEmpty() &&(controller.getModel()==null || controller.getModel().getCurrentState().getGameStage().equals(GameStages.WAITING))) {
Game model = new Game(eventAddPlayer.getProposedNPlayer());
controller.setModel(model);
playerList.setLimit(eventAddPlayer.getProposedNPlayer());
System.out.println("Game Created With: "+eventAddPlayer.getProposedNPlayer()+" Players");
}
if(controller.getModel().getCurrentState().getGameStage()!= GameStages.WAITING ) {
if (controller.getModel().getPlayers().stream().noneMatch(p -> p.getUserName().equals(username))|| controller.getModel().getCurrentState().getGameStage()==GameStages.ENDED) {
eventAddPlayer.setErrorType(ErrorType.GAME_ALREADY_STARTED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
}
else {
if (controller.addPlayer(username)) {
playerList.put(username, true);
System.out.println("Accepted player: " + username);
clientSend.writeObject(eventAddPlayer);
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
new Thread(handler).start();
continue;
} else {
eventAddPlayer.setErrorType(ErrorType.INVALID_USERNAME);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
continue;
}
}
if (!playerList.containsKey(username)) {
playerList.put(username, true);
System.out.println("(After crash)Reconnected player: " + username);
clientSend.writeObject(eventAddPlayer);
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
new Thread(handler).start();
} else if (!playerList.get(username)) {
// reconnect a previously disconnected player
playerList.put(username, true);
System.out.println("Reconnected player: " + username);
clientSend.writeObject(eventAddPlayer);
ClientHandler handler = createAndRegisterHandler(
username, clientSocket, clientSend, clientReceive);
Game game = controller.getModel();
handler.notifyMiniModel(new MiniModel(
game.getSlotMap(), game.getOrderLogicCard(),
game.getCurrentState(), game.getPlayers(),
game.getAvailableTotems(),
game.getUpperListTribeCards(), game.getLowerListTribeCards(),
game.getUpperListBuilding(), game.getLowerListBuilding(),
disconnectedUsernames(game)));
new Thread(handler).start();
actionQueue.add(new ReconnectPlayer(username));
} else {
eventAddPlayer.setErrorType(ErrorType.USER_ALREADY_CONNECTED);
eventAddPlayer.setIsError(true);
clientSend.writeObject(eventAddPlayer);
clientSocket.close();
System.out.println("Invalid parameters. Connection terminated.");
}
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
/**
* Accepts heartbeat connections and associates them with the correct client handler.
*
* <p>Each client immediately sends its username on the heartbeat channel.
* The method uses that username to retrieve the pending {@link ClientHandler}
* and starts a dedicated {@link HeartbeatHandler}. If no pending handler is found,
* the heartbeat socket is closed.
*/
private void acceptHeartbeat() {
while (true) {
try {
Socket hbSocket = heartbeatSocketTCP.accept();
ObjectInputStream hbIn =
new ObjectInputStream(hbSocket.getInputStream());
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();
}
}
}
/**
* Notifies connected TCP clients of a new network event.
*
* <p>If the event does not represent an error, it is sent to all connected clients.
* If it represents an error, it is sent only to the client that requested the action.
*
* @param event the network event to send to the clients.
*/
public void notifyAll(NetworkEvent event) {
clientHandlers.forEach(h -> {
if (!event.isError() || event.getUsername().equals(h.getUsername())) {
h.notifyEvent(event);
}
});
}
/**
* Notifies all connected TCP clients of a new game model.
*
* @param model the updated mini model to send to the clients.
*/
public void notifyAll(MiniModel model) {
clientHandlers.forEach(h -> h.notifyMiniModel(model));
}
/**
* Creates a {@link ClientHandler}, registers it in {@code pendingHeartbeat}
* and {@code clientHandlers}, but does NOT start its thread — callers start
* the thread after any extra setup (e.g. sending a model snapshot).
*/
private ClientHandler createAndRegisterHandler(String username,
Socket socket,
ObjectOutputStream out,
ObjectInputStream in) {
ClientHandler handler = new ClientHandler(
username, socket, out, in, clientHandlers, playerList, actionQueue);
pendingHeartbeat.put(username, handler);
clientHandlers.add(handler);
return handler;
}
/** Returns usernames of players currently marked as disconnected in the given game. */
private static ArrayList<String> disconnectedUsernames(Game game) {
return game.getDisconnectedPlayers().entrySet().stream()
.filter(Map.Entry::getValue)
.map(e -> e.getKey().getUserName())
.collect(Collectors.toCollection(ArrayList::new));
}
}