/* * 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.net.client.AbstractCommand; import illarion.common.net.NetCommWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.concurrent.NotThreadSafe; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.WritableByteChannel; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.util.concurrent.*; /** * The Sender class handles all data that is send from the client, encodes the * commands and prepares them for sending. * * @author Martin Karing <nitram@illarion.org> * @author Nop */ @NotThreadSafe final class Sender implements NetCommWriter { /** * 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(Sender.class); /** * The maximal size in bytes one command can use. */ private static final int MAX_COMMAND_SIZE = 1000; /** * Length of the byte buffer used to store the data before its send to the * server. */ @Nonnull private final ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_COMMAND_SIZE); /** * The string encoder that is used to encode the strings before they are * send to the server. */ @Nonnull private final CharsetEncoder encoder; /** * The buffer that is used to temporary store the decoded characters that * were send to the player. */ @Nonnull private final CharBuffer encodingBuffer = CharBuffer.allocate(65535); /** * The output stream of the socket connection to the server. The encoded * data is written on this stream to be send to the server. */ @Nonnull private final WritableByteChannel outChannel; @Nonnull private final ExecutorService commandExecutor; /** * The basic constructor for the sender that sets up all needed data. * * @param out the output channel of the socket connection used to send the * data to the server */ Sender(@Nonnull WritableByteChannel out) { commandExecutor = Executors.newSingleThreadExecutor(); outChannel = out; encoder = NetComm.SERVER_STRING_ENCODING.newEncoder(); } void sendCommand(@Nonnull AbstractCommand cmd) { commandExecutor.submit(() -> { try { encodeCommand(cmd); } catch (Exception e) { log.error("Error while sending command.", e); } return null; }); } private void encodeCommand(@Nonnull AbstractCommand cmd) throws IOException { if (cmd.getId() != CommandList.CMD_KEEPALIVE) { log.debug("SND: {}", cmd); } buffer.clear(); buffer.put((byte) cmd.getId()); buffer.put((byte) (cmd.getId() ^ COMMAND_XOR_MASK)); // keep some space for the length and the CRC int headerLenCRC = buffer.position(); buffer.putShort((short) 0); buffer.putShort((short) 0); int startOfCmd = buffer.position(); // encode command into net protocol cmd.encode(this); int length = buffer.position() - startOfCmd; buffer.flip(); buffer.position(startOfCmd); int crc = NetComm.getCRC(buffer, length); buffer.position(headerLenCRC); buffer.putShort((short) length); buffer.putShort((short) crc); buffer.position(0); if (NetComm.isDumpingActive()) { NetComm.dump("snd => ", buffer); buffer.flip(); } outChannel.write(buffer); } /** * Shutdown the sender. */ public Future<Boolean> saveShutdown() { commandExecutor.shutdown(); return new Future<Boolean>() { @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; } @Override public boolean isCancelled() { return false; } @Override public boolean isDone() { return commandExecutor.isTerminated(); } @Override @Nonnull public Boolean get() throws InterruptedException, ExecutionException { try { return get(1, TimeUnit.HOURS); } catch (TimeoutException e) { throw new ExecutionException(e); } } @Override @Nonnull public Boolean get(long timeout, @Nonnull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return commandExecutor.awaitTermination(timeout, unit); } }; } /** * Write 1 byte as signed value to the network. * * @param value the signed byte that shall be send to the server */ @Override public void writeByte(byte value) { buffer.put(value); } /** * Write 4 byte as signed value to the network. * * @param value the signed integer that shall be send to the server */ @Override public void writeInt(int value) { buffer.putInt(value); } /** * Write 2 byte as signed value to the network. * * @param value the signed integer that shall be send to the server */ @Override public void writeShort(short value) { buffer.putShort(value); } /** * Write a string to the network. The length header of the string is written * automatically and its encoded to the correct CharSet automatically. * * @param value the string that shall be send to the server */ @Override public void writeString(@Nonnull String value) throws CharacterCodingException { int startIndex = buffer.position(); buffer.putShort((short) 0); encodingBuffer.clear(); encodingBuffer.put(value, 0, Math.min(encodingBuffer.capacity(), value.length())); encodingBuffer.flip(); do { CoderResult encodingResult = encoder.encode(encodingBuffer, buffer, true); if (!encodingResult.isError()) { break; } if (encodingResult.isUnmappable()) { log.warn("Found a character that failed to encode for the transfer to the server: {} - SKIP", encodingBuffer.get()); } else { encodingResult.throwException(); } } while (encodingBuffer.hasRemaining()); int lastIndex = buffer.position(); buffer.position(startIndex); writeUShort(lastIndex - startIndex - 2); buffer.position(lastIndex); } /** * Write 1 byte as unsigned value to the network. * * @param value the value that shall be send as unsigned byte */ @Override public void writeUByte(short value) { buffer.put((byte) (value % (1 << Byte.SIZE))); } /** * Write 2 byte as unsigned value to the network. * * @param value the value that shall be send as unsigned short */ @Override public void writeUShort(int value) { buffer.putShort((short) (value % (1 << Short.SIZE))); } }