/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
package illarion.client.net;
import illarion.client.IllaClient;
import illarion.client.Servers;
import illarion.client.crash.NetCommCrashHandler;
import illarion.client.net.client.AbstractCommand;
import illarion.client.net.client.KeepAliveCmd;
import illarion.client.util.ConnectionPerformanceClock;
import javolution.text.TextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.*;
/**
* Network communication interface. All activities like sending and transmitting of messages and commands in handled
* by this class. It handles the sockets and the in and output queues.
*/
public final class NetComm {
/**
* This constant holds the encoding for strings that are received from and send to the server.
*/
public static final Charset SERVER_STRING_ENCODING = Charset.forName("ISO-8859-1");
/**
* The value that is added and used for the modulus division that is done on the buffer value before printing it.
*/
private static final int CHAR_MOD = 265;
/**
* This is the string used to format the debugging output of the received and transmitted data.
*/
private static final String DUMP_FORMAT_BYTES = "[%1$02X]";
/**
* This is the string used to format the debugging output of the total amount of received bytes.
*/
private static final String DUMP_FORMAT_TOTAL = "[%1$d byte]";
/**
* The value of the first printable character using {@link String#valueOf(char)}.
*/
private static final int FIRST_PRINT_CHAR = 65;
/**
* The instance of the logger that is used to write out the data.
*/
@Nonnull
private static final Logger log = LoggerFactory.getLogger(NetComm.class);
/**
* General time to wait in case its needed that other threads need to react on some input.
*/
private static final int THREAD_WAIT_TIME = 100;
@Nonnull
private final ScheduledExecutorService keepAliveExecutor;
/**
* The receiver that accepts and decodes data that was received from the server.
*/
@Nullable
private Receiver inputThread;
/**
* The thread that handles the messages that arrive from the server.
*/
@Nullable
private MessageExecutor messageHandler;
/**
* The sender instance that accepts all server client commands that shall be send and forwards the data to this
* class.
*/
@Nullable
private Sender sender;
/**
* Communication socket to the Illarion server.
*/
@Nullable
private SocketChannel socket;
/**
* Default constructor that prepares all values of the NetComm.
*/
public NetComm() {
ReplyFactory.getInstance();
keepAliveExecutor = new ScheduledThreadPoolExecutor(1);
}
/**
* New version of the checksum calculation. All bytes from the current position of the buffer to the limit are
* included to the calculation. The limit, mark and position is restored by this function. So the ByteBuffer is
* unchanged after the function leaves.
*
* @param buffer the byte buffer that provides the byte data
* @param len the amount of byte that shall be included to the checksum calculation
* @return the calculated checksum
*/
public static int getCRC(@Nonnull ByteBuffer buffer, int len) {
int crc = 0;
int remain = len;
int pos = buffer.position();
while (buffer.hasRemaining() && (remain-- > 0)) {
byte data = buffer.get();
crc += data;
if (data < 0) {
crc += 1 << Byte.SIZE;
}
}
buffer.position(pos);
return crc % ((1 << Short.SIZE) - 1);
}
/**
* Check if the dumping function will do anything.
*
* @return {@code true} if the dumping is active.
*/
static boolean isDumpingActive() {
return log.isTraceEnabled();
}
/**
* This function has only debug purposes and is used to print the contents of a buffer to the output log. This is
* used for the debug output when debugging the protocol. The bytes that are written are all remaining bytes of
* the buffer. Also the position of the buffer with point at the end after this function was called.
*
* @param prefix The prefix that shall be written first to the log
* @param buffer The buffer that contains the values that shall be written
*/
static void dump(String prefix, @Nonnull ByteBuffer buffer) {
TextBuilder builder = new TextBuilder();
TextBuilder builderText = new TextBuilder();
builder.append(prefix);
builder.append(' ');
int bytes = 0;
while (buffer.hasRemaining()) {
byte bufferValue = buffer.get();
builder.append(String.format(DUMP_FORMAT_BYTES, bufferValue));
char c = (char) ((bufferValue + CHAR_MOD) % CHAR_MOD);
if (c >= FIRST_PRINT_CHAR) {
builderText.append(c);
} else {
builderText.append('.');
}
++bytes;
}
builder.append(' ');
builder.append(String.format(DUMP_FORMAT_TOTAL, bytes));
builder.append(' ');
builder.append('<');
builder.append(builderText);
builder.append('>');
log.trace(builder.toString());
}
/**
* Establish a connection with the server.
*
* @return true in case the connection got established. False if not.
*/
public boolean connect() {
setLoginDone(false);
try {
Servers usedServer = IllaClient.getInstance().getUsedServer();
@Nonnull String serverAddress;
int serverPort;
if (usedServer == Servers.Customserver) {
String configServer = IllaClient.getCfg().getString("serverAddress");
serverAddress = (configServer == null) ? Servers.Customserver.getServerHost() : configServer;
serverPort = IllaClient.getCfg().getInteger("serverPort");
} else {
serverAddress = usedServer.getServerHost();
serverPort = usedServer.getServerPort();
}
InetSocketAddress address = new InetSocketAddress(serverAddress, serverPort);
socket = SelectorProvider.provider().openSocketChannel();
socket.configureBlocking(true);
socket.socket().setPerformancePreferences(0, 2, 1);
socket.socket().setTcpNoDelay(true);
if (!socket.connect(address)) {
while (socket.isConnectionPending()) {
socket.finishConnect();
}
}
sender = new Sender(socket);
messageHandler = new MessageExecutor();
inputThread = new Receiver(messageHandler, socket);
inputThread.setUncaughtExceptionHandler(NetCommCrashHandler.getInstance());
inputThread.start();
keepAliveExecutor.scheduleAtFixedRate(() -> {
if (ConnectionPerformanceClock.isReadyForNewPing()) {
ConnectionPerformanceClock.notifySendToNetComm();
sendCommand(new KeepAliveCmd());
}
}, 500, 500, TimeUnit.MILLISECONDS);
} catch (@Nonnull IOException e) {
log.error("Connection error");
return false;
}
return true;
}
/**
* Disconnect the client-server connection and shut the socket along with all threads for sending and receiving
* down.
*/
public void disconnect() {
setLoginDone(false);
try {
Collection<Future<?>> terminationFutures = new ArrayList<>();
keepAliveExecutor.shutdown();
while (!keepAliveExecutor.isTerminated()) {
try {
keepAliveExecutor.awaitTermination(1, TimeUnit.SECONDS);
} catch (InterruptedException ignore) {
}
}
// stop threads
if (sender != null) {
terminationFutures.add(sender.saveShutdown());
sender = null;
}
if (inputThread != null) {
inputThread.saveShutdown();
inputThread = null;
}
if (messageHandler != null) {
terminationFutures.add(messageHandler.saveShutdown());
messageHandler = null;
}
for (Future<?> future : terminationFutures) {
try {
future.get();
} catch (InterruptedException e) {
log.warn("Problem while shutting down NetComm. Something got interrupted.", e);
} catch (ExecutionException e) {
log.warn("Problem while shutting down NetComm. Showdown execution failed.", e);
}
}
// wait for threads to react
try {
Thread.sleep(THREAD_WAIT_TIME);
} catch (@Nonnull InterruptedException e) {
log.warn("Disconnecting wait got interrupted.");
}
// close connection
if (socket != null) {
socket.close();
socket = null;
}
} catch (@Nonnull IOException e) {
log.warn("Disconnecting failed.", e);
}
}
private boolean loginDone;
public void setLoginDone(boolean done) {
loginDone = done;
}
public boolean isLoginDone() {
return loginDone;
}
public void sendCommand(@Nonnull AbstractCommand cmd) {
if (sender != null) {
sender.sendCommand(cmd);
} else {
log.error("Sending {} failed. Sender is nowhere to be found.", cmd);
}
}
}