/* * This file is part of the Illarion project. * * Copyright © 2016 - 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.net.server.ServerReply; import illarion.client.util.Lang; import illarion.common.net.NetCommReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.charset.CharsetDecoder; /** * The Receiver class handles all data that is send from the server, decodes the messages and prepares them for * execution. * * @author Martin Karing <nitram@illarion.org> * @author Nop */ @NotThreadSafe final class Receiver extends Thread implements NetCommReader { /** * Length of the byte buffer used to store the data from the server. */ private static final int INITIAL_BUFFER_SIZE = 1000; /** * The XOR mask the command ID is masked with to decode the checking ID and ensure that the start of a command * was found. */ private static final int COMMAND_XOR_MASK = 0xFF; /** * The instance of the logger that is used to write out the data. */ @Nonnull private static final Logger log = LoggerFactory.getLogger(Receiver.class); /** * Time the receiver waits for more data before throwing away the incomplete things it already got. */ private static final int RECEIVER_TIMEOUT = 1000; /** * The decoder that is used to decode the strings that are send to the client by the server. */ @Nonnull private final CharsetDecoder decoder; /** * The buffer that is used to temporary store the decoded characters that were send to the player. */ @Nonnull private final CharBuffer decodingBuffer = CharBuffer.allocate(65535); /** * The input stream of the connection socket of the connection to the server. */ @Nonnull private final ReadableByteChannel inChannel; /** * The list that stores the commands there were decoded and prepared for the NetComm for execution. */ @Nonnull private final MessageExecutor executor; /** * The buffer that stores the byte that we received from the server for decoding. */ @Nullable private ByteBuffer buffer = null; /** * Indicator if the Receiver is currently running. */ private boolean running; /** * The time until a timeout occurs. */ private long timeOut; /** * The basic constructor for the receiver that sets up all needed data. * * @param executor the executor that takes care to send the messages to the rest of the client * @param in the input stream of the socket connection to the server that contains the data that needs to * be decoded */ Receiver(@Nonnull MessageExecutor executor, @Nonnull ReadableByteChannel in) { super("Illarion input thread"); this.executor = executor; inChannel = in; decoder = NetComm.SERVER_STRING_ENCODING.newDecoder(); setDaemon(true); } @Nonnull private ByteBuffer getBuffer() { return getBuffer(0); } @Nonnull private ByteBuffer getBuffer(int bufferSize) { ByteBuffer oldBuffer = buffer; if ((oldBuffer != null) && (oldBuffer.capacity() >= bufferSize)) { return oldBuffer; } buffer = ByteBuffer.allocateDirect( ((bufferSize / INITIAL_BUFFER_SIZE) * INITIAL_BUFFER_SIZE) + INITIAL_BUFFER_SIZE); buffer.order(ByteOrder.BIG_ENDIAN); if (oldBuffer != null) { buffer.put(oldBuffer); buffer.flip(); } else { buffer.limit(0); } return buffer; } /** * Read a single byte from the buffer and handle it as signed byte. * * @return The byte from the buffer handled as signed byte * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public byte readByte() throws IOException { return getBuffer().get(); } /** * Read four bytes from the buffer and handle them as a single signed value. * * @return The two bytes in the buffer handled as signed 4 byte value * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public int readInt() throws IOException { return getBuffer().getInt(); } /** * Read two bytes from the buffer and handle them as a single signed value. * * @return The two bytes in the buffer handled as signed 2 byte value * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public short readShort() throws IOException { return getBuffer().getShort(); } /** * Read a string from the input buffer and encode it for further usage. * * @return the decoded string * @throws IOException If there are more byte read then there are written in * the buffer */ @Nonnull @Override public String readString() throws IOException { int len = readUShort(); if (len == 0) { return ""; } ByteBuffer buffer = getBuffer(); if (len > buffer.remaining()) { throw new IndexOutOfBoundsException("reading beyond receive buffer " + (buffer.remaining() + len)); } decodingBuffer.clear(); int lastLimit = buffer.limit(); buffer.limit(buffer.position() + len); decoder.decode(buffer, decodingBuffer, false); buffer.limit(lastLimit); decodingBuffer.flip(); return decodingBuffer.toString(); } /** * Read a single byte from the buffer and handle it as unsigned byte. * * @return The byte of the buffer handled as unsigned byte. * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public short readUByte() throws IOException { short data = readByte(); if (data < 0) { return (short) (data + (1 << Byte.SIZE)); } return data; } /** * Read four bytes from the buffer and handle them as a single unsigned * value. * * @return The two bytes in the buffer handled as unsigned 4 byte value * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public long readUInt() throws IOException { long data = readInt(); if (data < 0) { return data + (1L << Integer.SIZE); } return data; } /** * Read two bytes from the buffer and handle them as a single unsigned * value. * * @return The two bytes in the buffer handled as unsigned 2 byte value * @throws IOException If there are more byte read then there are written in * the buffer */ @Override public int readUShort() throws IOException { int data = readShort(); if (data < 0) { return data + (1 << Short.SIZE); } return data; } /** * The main loop the the receiver thread. Decodes the data of the input * stream and places the server messages in the queue. * <p> * The decoding of the data happens as instantly as soon as a command is * completely read from the input stream. Searching the start of a command * is done by looking for a valid ID with a valid XOR id right behind. * </p> */ @Override public void run() { running = true; int minRequiredData = CommandList.HEADER_SIZE; while (running) { try { while (running && receiveData(minRequiredData)) { while (true) { ByteBuffer buffer = getBuffer(); // wait for a complete message header if (buffer.remaining() < CommandList.HEADER_SIZE) { break; } // identify command int id = readUByte(); int xor = readUByte(); // valid command id if (id != (xor ^ COMMAND_XOR_MASK)) { // delete only first byte from buffer, scanning for valid command buffer.position(1); buffer.compact(); log.warn("Skipping invalid data [{}]", id); continue; } // read length and CRC int len = readUShort(); int crc = readUShort(); // wait for complete data if (!isDataComplete(len)) { // scroll the cursor back and wait for more. buffer.position(0); minRequiredData = len + CommandList.HEADER_SIZE; break; } minRequiredData = CommandList.HEADER_SIZE; // check CRC if (crc != NetComm.getCRC(buffer, len)) { int oldLimit = buffer.limit(); buffer.limit(len + CommandList.HEADER_SIZE); buffer.position(CommandList.HEADER_SIZE); NetComm.dump("Invalid CRC ", buffer); buffer.position(1); buffer.limit(oldLimit); buffer.compact(); buffer.flip(); continue; } // decode try { ServerReply rpl = ReplyFactory.getInstance().getReply(id); if (rpl != null) { rpl.decode(this); if (id != CommandList.MSG_KEEP_ALIVE) { log.debug("REC: {}", rpl); } // put decoded command in input queue executor.scheduleReplyExecution(rpl); } else { // throw away the command that was incorrectly decoded buffer.position(len + CommandList.HEADER_SIZE); } } catch (@Nonnull IllegalArgumentException ex) { log.error("Invalid command id received {}", Integer.toHexString(id)); } buffer.compact(); buffer.flip(); } } } catch (@Nonnull IOException e) { if (running) { log.error("The connection to the server is not working anymore.", e); IllaClient.sendDisconnectEvent(Lang.getMsg("error.receiver"), true); running = false; return; } } catch (@Nonnull Exception e) { if (running) { log.error("General error in the receiver", e); IllaClient.sendDisconnectEvent(Lang.getMsg("error.receiver"), true); running = false; return; } } } } /** * Shutdown the receiver. */ public void saveShutdown() { log.info("{}: Shutdown requested!", getName()); running = false; interrupt(); } /** * This function checks of the received data contains a complete command. * * @param len the amount of bytes that were received for that command * @return true in case the command is complete, false if not */ private boolean isDataComplete(int len) { ByteBuffer buffer = getBuffer(); if (len <= buffer.remaining()) { timeOut = 0; return true; } // set timeout for data if (timeOut == 0) { timeOut = System.currentTimeMillis() + RECEIVER_TIMEOUT; } // timeout exceeded if (System.currentTimeMillis() > timeOut) { NetComm.dump("Receiver timeout. Skipping ", buffer); buffer.clear(); buffer.limit(0); } else { // still waiting buffer.position(0); } return false; } /** * Read data from the input stream of the socket and store it in the buffer. * * @param neededDataInBuffer The data that is needed at least before the method has to return in order to parse * the values correctly * @return true in case there is any data to be decoded in the buffer * @throws IOException In case there is something wrong with the input stream */ private boolean receiveData(int neededDataInBuffer) throws IOException { ByteBuffer buffer = getBuffer(neededDataInBuffer); int data = buffer.remaining(); int appPos = buffer.limit(); buffer.clear(); buffer.position(appPos); int newData = 0; while (true) { if (inChannel.isOpen()) { newData = inChannel.read(buffer); } data += newData; if (data >= neededDataInBuffer) { break; } } buffer.flip(); if ((newData > 0) && NetComm.isDumpingActive()) { buffer.position(appPos); NetComm.dump("rcv <= ", buffer); buffer.position(0); } return buffer.hasRemaining(); } }