Coverage Summary for Class: TCPClient (it.polimi.ingsw.gc14.Network.TCP.Client)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
| TCPClient |
0%
(0/1)
|
0%
(0/15)
|
0%
(0/24)
|
0%
(0/83)
|
package it.polimi.ingsw.gc14.Network.TCP.Client;
import it.polimi.ingsw.gc14.Controller.ClientController;
import it.polimi.ingsw.gc14.ErrorType;
import it.polimi.ingsw.gc14.Model.MiniModel;
import it.polimi.ingsw.gc14.Network.IClient;
import it.polimi.ingsw.gc14.Network.NetworkEvent;
import it.polimi.ingsw.gc14.Network.NetworkConfig;
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 {
/** Byte value sent by the client to signal it is still alive. */
private static final int PING = 1;
/** Byte value expected from the server in response to a {@link #PING}. */
private static final int PONG = 2;
/** Socket TCP */
private Socket communicationSocket;
/** Input stream used receive objects from the server */
private ObjectInputStream socketReceive;
/** Output stream used to send objects to the server */
private ObjectOutputStream socketSend;
/** Client game's controller */
private final ClientController controller;
/** IP address of the server to connect to */
private final String hostname;
/** {@code true} while the connection is active; set to {@code false} on disconnect. */
private boolean running;
/** TCP port */
private final int mainPort;
/** TCP port dedicated to heartbeat communication. */
private final int heartbeatPort;
/** Socket used exclusively for heartbeat ping/pong exchange. */
private Socket heartbeatSocket;
/** Output stream of {@link #heartbeatSocket}, used to write {@link #PING} bytes. */
private OutputStream heartbeatOut;
/** Input stream of {@link #heartbeatSocket}, used to read {@link #PONG} bytes. */
private InputStream heartbeatIn;
/**
* Constructs a TCP client and initializes its connection parameters.
*
* @param controller the client controller.
* @param hostname the IP address or hostname of the server.
* @param mainPort the main TCP port of the server.
* @param heartbeatPort the TCP port used for heartbeat communication.
*/
public TCPClient(ClientController controller, String hostname, int mainPort,int heartbeatPort ) {
this.controller = controller;
this.hostname = hostname;
this.mainPort = mainPort;
this.heartbeatPort = heartbeatPort;
}
/**
* Starts the TCP connection with the server.
* Sends an {@link AddPlayer} event, if the server responds with {@code -1}, the connection is refused and the method returns {@code false}.
* Otherwise, a listener thread is started.
* @param user The username of the player
* @param proposedNPlayers The desired number of players for the game
* @return null if the connection is successful, GENERIC_ERROR otherwise.
*/
public ErrorType connect(String user, int proposedNPlayers) {
try {
communicationSocket = new Socket(hostname, mainPort);
socketSend = new ObjectOutputStream(communicationSocket.getOutputStream());
socketReceive = new ObjectInputStream(communicationSocket.getInputStream());
NetworkEvent event= new AddPlayer(user, proposedNPlayers);
socketSend.writeObject(event);
try {
if( socketReceive.readObject() instanceof AddPlayer x)
{
if(x.isError())
return x.getErrorType();
}
} catch (ClassNotFoundException e) {
return ErrorType.GENERIC_ERROR;
}
running = true;
new Thread(this::receiveMessage, "tcp-reader").start();
// heartbeat socket
this.heartbeatSocket = new Socket(hostname, heartbeatPort);
this.heartbeatOut = heartbeatSocket.getOutputStream();
this.heartbeatIn = heartbeatSocket.getInputStream();
// send username immediately so server can associate the two sockets
new ObjectOutputStream(heartbeatSocket.getOutputStream()).writeObject(user);
heartbeatOut.flush();
new Thread(this::heartbeatLoop, "heartbeat").start();
return null;
} catch (IOException e) {
e.printStackTrace();
return ErrorType.SERVER_UNREACHABLE;
}
}
/**
* Sends a {@link #PING} byte to the server every 3 seconds and waits for a {@link #PONG} reply.
* If no reply arrives within {@value NetworkConfig#SILENCE_THRESHOLD_MS} ms, the server is
* considered unreachable and {@link #disconnect()} is called.
*/
private void heartbeatLoop() {
ScheduledExecutorService sender = Executors.newSingleThreadScheduledExecutor();
sender.scheduleAtFixedRate(() -> {
try {
synchronized (heartbeatOut)
{
heartbeatOut.write(PING);
heartbeatOut.flush();
}
} catch (IOException e) {
sender.shutdownNow();
if(running)
{
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED,ErrorType.SERVER_CRASHED.toString());
}
}
}, 0, 3, TimeUnit.SECONDS);
try {
heartbeatSocket.setSoTimeout((int) NetworkConfig.SILENCE_THRESHOLD_MS);
while (running) {
int b = heartbeatIn.read();
if (b != PONG) {
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
break;
}
// pong received: server is alive
}
} catch (SocketTimeoutException e) {
System.out.println("Server heartbeat timeout");
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
} catch (IOException e) {
if (running) {
disconnect();
controller.getView().showError(ErrorType.SERVER_CRASHED, ErrorType.SERVER_CRASHED.toString());
}
} finally {
sender.shutdownNow();
}
}
/**
* Disconnects the player and cleans model and controller
*/
private synchronized void disconnect() {
if (!running) return;
running = false;
try { communicationSocket.close(); } catch (IOException ignored) {}
try { heartbeatSocket.close(); } catch (IOException ignored) {}
controller.setClient(null);
controller.setModel(null);
}
/**
* Listens continuously for incoming objects from the server.
* - If the received object is a {@link NetworkEvent} flagged as an error, it is printed.
* - If the received object is a valid {@link NetworkEvent}, it is applied to the game controller.
* - If the received object is a {@link MiniModel}, the controller's model is set.
*/
private void receiveMessage() {
while (true) {
try {
Object read;
try {
read = socketReceive.readObject();
} catch (IOException e) {
if (running) disconnect();
break;
}
if (read instanceof NetworkEvent event) {
if (event.isError()) {
if(event.getErrorType() == ErrorType.WRONG_ACTION) {
controller.getView().showError(ErrorType.WRONG_ACTION,event.toString());
}
else
controller.getView().showError(event.getErrorType(),event.getErrorType().toString());
} else {
event.apply(controller.getMiniModel());
controller.getView().render();
}
} else if (read instanceof MiniModel model) {
controller.setModel(model);
controller.getView().render();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
break;
}
}
}
/**
* Requests to draw a tribe card from the upper list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawUpperTribeCard(String playerUsername, int pos) {
doEvent(new DrawUpperTribeCard(playerUsername,pos));
}
/**
* Requests to draw a tribe card from the lower list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawLowerTribeCard(String playerUsername,int pos) {
doEvent(new DrawLowerTribeCard(playerUsername,pos));
}
/**
* Requests to draw a building card from the upper list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawUpperBuildingCard(String playerUsername,int pos) {
doEvent(new DrawUpperBuildingCard(playerUsername,pos));
}
/**
* Requests to draw a building card from the lower list.
* Creates a NetworkEvent and sends it through the network client.
* @param playerUsername the name of the player performing the action
* @param pos the index of the card to draw
*/
public void drawLowerBuildingCard(String playerUsername,int pos) {
doEvent(new DrawLowerBuildingCard(playerUsername,pos));
}
/**
* Requests to skip drawing turn.
* This action is available only when the player cannot draw any tribe card, but still can buy some buildings.
* @param playerUsername the name of the player performing the action
*/
public void skipTurn(String playerUsername) {
doEvent(new SkipTurn(playerUsername));
}
/**
* Used to perform the slot choice action for the specified player at the specified position.
* @param playerUsername the name of the player performing the action
* @param pos the index of the selected slot
*/
public void slotChoice(String playerUsername,int pos) {
doEvent(new SlotChoice(playerUsername,pos));
}
/**
* Sends a {@link NetworkEvent} to the server.
* @param event The NetworkEvent to send.
*/
private void doEvent(NetworkEvent event) {
try {
synchronized (socketSend) {
socketSend.writeObject(event);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Sends a totem choice event to the server for the specified player.
*
* @param playerUsername the username of the player choosing the totem.
* @param totems the name of the chosen totem.
*/
public void totemChoice(String playerUsername,String totems) {
doEvent(new TotemChoice(playerUsername,totems));
}
/**
* Notifies the server of a voluntary disconnection by sending a sentinel value
* on the heartbeat channel, then closes the connection.
*/
public void notifyDisconnection()
{
synchronized (heartbeatOut)
{
try {
heartbeatOut.write(-1);
} catch (IOException e) {
disconnect();
}
}
disconnect();
}
}