Coverage Summary for Class: HeartbeatHandler (it.polimi.ingsw.gc14.Network.TCP.Server)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| HeartbeatHandler |
0%
(0/1)
|
0%
(0/5)
|
0%
(0/8)
|
0%
(0/28)
|
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;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
/**
* Handles the heartbeat communication associated with a TCP client.
*
* <p>This component listens for heartbeat {@code PING} messages from the client,
* replies with {@code PONG} messages, and periodically checks whether the client
* has remained silent for too long. If a timeout or communication error occurs,
* both the heartbeat connection and the main client connection are disconnected.
*/
public class HeartbeatHandler implements Runnable {
/** Username of the client associated with this heartbeat connection. */
private final String username;
/** Dedicated socket used solely for heartbeat ping/pong exchange. */
private final Socket socket;
/** Input stream of {@link #socket} used to receive {@code PING} bytes from the client. */
private final InputStream in;
/** Output stream of {@link #socket} used to send {@code PONG} bytes back to the client. */
private final OutputStream out;
/**
* Byte value representing a heartbeat ping message.
*/
private static final int PING = 1;
/**
* Byte value representing a heartbeat pong response.
*/
private static final int PONG = 2;
/**
* Main client handler associated with this heartbeat connection.
*/
private final ClientHandler mainHandler;
/**
* Timestamp of the last received heartbeat message.
*/
private volatile long lastReceivedTime = System.currentTimeMillis();
/**
* Indicates whether the heartbeat handler is still active.
*/
private volatile boolean running = true;
/**
* Executor used to periodically check heartbeat timeouts.
*/
private final ScheduledExecutorService watchdog =
Executors.newSingleThreadScheduledExecutor();
/**
* Constructs a heartbeat handler for the specified client.
*
* @param username the username of the connected client.
* @param socket the socket dedicated to heartbeat communication.
* @param mainHandler the main client handler associated with the same player.
* @throws IOException if the input or output stream cannot be obtained from the socket.
*/
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();
}
/**
* Starts the heartbeat listening loop.
*
* <p>The method activates the watchdog task, then waits for incoming heartbeat
* messages. When a {@code PING} is received, the last-received timestamp is
* updated and a {@code PONG} response is sent back to the client.
* If the stream is closed or an I/O error occurs, the client is disconnected.
*/
@Override
public void run() {
startWatchdog();
try {
while (running) {
int b = in.read();
if (b == -1) {
disconnect();
break;
}
if (b == PING) {
lastReceivedTime = System.currentTimeMillis();
out.write(PONG);
out.flush();
}
}
} catch (IOException e) {
disconnect();
}
}
/**
* Starts the periodic watchdog task that detects heartbeat timeouts.
*
* <p>If no heartbeat message is received within the configured silence threshold,
* the associated client is disconnected.
*/
private void startWatchdog() {
watchdog.scheduleAtFixedRate(() -> {
if (System.currentTimeMillis() - lastReceivedTime > NetworkConfig.SILENCE_THRESHOLD_MS) {
System.out.println("Heartbeat timeout: " + username);
disconnect();
}
}, 1, 1, TimeUnit.SECONDS);
}
/**
* Disconnects the heartbeat channel and the associated main client connection.
*
* <p>The handler is stopped, the watchdog task is terminated, the main
* {@link ClientHandler} is disconnected, and the heartbeat socket is closed.
*/
private synchronized void disconnect() {
running = false;
watchdog.shutdownNow();
mainHandler.disconnect();
try { socket.close(); } catch (IOException ignored) {}
}
}