public class DragoniteSocketParameters {

    private int packetSize = 1300;

    private boolean autoSplit = true;

    private int maxPacketBufferSize = 0;

    private int windowMultiplier = 4;

    private int resendMinDelayMS = 50;

    private int heartbeatIntervalSec = 5;

    private int receiveTimeoutSec = 10;

    private boolean enableWebPanel = false;

    private InetSocketAddress webPanelBindAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), DragoniteGlobalConstants.WEB_PANEL_PORT);

    private PacketCryptor packetCryptor = null;

    private int trafficClass = 0;

    public int getPacketSize() {
        return packetSize;
    }

    public void setPacketSize(final int packetSize) {
        checkArgument(packetSize >= 500, "Packet size is too small");
        this.packetSize = packetSize;
    }

    public boolean isAutoSplit() {
        return autoSplit;
    }

    public void setAutoSplit(final boolean autoSplit) {
        this.autoSplit = autoSplit;
    }

    public int getMaxPacketBufferSize() {
        return maxPacketBufferSize;
    }

    public void setMaxPacketBufferSize(final int maxPacketBufferSize) {
        checkArgument(maxPacketBufferSize >= 1, "Receive window must be greater than zero");
        this.maxPacketBufferSize = maxPacketBufferSize;
    }

    public int getWindowMultiplier() {
        return windowMultiplier;
    }

    public void setWindowMultiplier(final int windowMultiplier) {
        checkArgument(windowMultiplier >= 1 && windowMultiplier <= 10, "Multiplier must be greater than zero (and no more than 10)");
        this.windowMultiplier = windowMultiplier;
    }

    public int getResendMinDelayMS() {
        return resendMinDelayMS;
    }

    public void setResendMinDelayMS(final int resendMinDelayMS) {
        checkArgument(resendMinDelayMS >= 1, "Resend delay must be greater than zero");
        this.resendMinDelayMS = resendMinDelayMS;
    }

    public int getHeartbeatIntervalSec() {
        return heartbeatIntervalSec;
    }

    public void setHeartbeatIntervalSec(final int heartbeatIntervalSec) {
        checkArgument(heartbeatIntervalSec >= 1, "Heartbeat interval must be greater than zero");
        this.heartbeatIntervalSec = heartbeatIntervalSec;
    }

    public int getReceiveTimeoutSec() {
        return receiveTimeoutSec;
    }

    public void setReceiveTimeoutSec(final int receiveTimeoutSec) {
        checkArgument(receiveTimeoutSec >= 1, "Receive timeout must be greater than zero");
        this.receiveTimeoutSec = receiveTimeoutSec;
    }

    public boolean isEnableWebPanel() {
        return enableWebPanel;
    }

    public void setEnableWebPanel(final boolean enableWebPanel) {
        this.enableWebPanel = enableWebPanel;
    }

    public InetSocketAddress getWebPanelBindAddress() {
        return webPanelBindAddress;
    }

    public void setWebPanelBindAddress(final InetSocketAddress webPanelBindAddress) {
        checkArgument(webPanelBindAddress != null, "Bind address cannot be null");
        this.webPanelBindAddress = webPanelBindAddress;
    }

    public PacketCryptor getPacketCryptor() {
        return packetCryptor;
    }

    public void setPacketCryptor(final PacketCryptor packetCryptor) {
        this.packetCryptor = packetCryptor;
    }

    public int getTrafficClass() {
        return trafficClass;
    }

    public void setTrafficClass(final int trafficClass) {
        checkArgument(inTrafficClassRange(trafficClass), "TC must be in the range 0 <= tc <= 255");
        this.trafficClass = trafficClass;
    }
} public interface PacketCryptor {

    /**
     * Encrypt the message and return the ciphertext.
     * This function may be called by multiple threads at the same time.
     *
     * @param rawData Content to be encrypted
     * @return Encrypted content
     */
    byte[] encrypt(final byte[] rawData);

    /**
     * Decrypt the ciphertext and return the message.
     * This function may be called by multiple threads at the same time.
     *
     * @param encryptedData Content to be decrypted
     * @return Decrypted content
     */
    byte[] decrypt(final byte[] encryptedData);

    /**
     * Many encryption methods need to add additional bytes to the original message.
     * Please specify the maximum length of additional bytes that the encryption might cause.
     *
     * @return Length of additional bytes
     */
    int getMaxAdditionalBytesLength();

} public class IncorrectMessageException extends DragoniteException {

    public IncorrectMessageException(final String msg) {
        super(msg);
    }

} public class SenderClosedException extends DragoniteException {

    public SenderClosedException() {
        super("Sender closed");
    }

    public SenderClosedException(final String msg) {
        super(msg);
    }

} public interface Message {

    byte getVersion();

    MessageType getType();

    byte[] toBytes();

    int getFixedLength();

} public final class MessageParser {

    public static Message parseMessage(final byte[] msgBytes) throws IncorrectMessageException {
        if (msgBytes.length >= 2) {
            try {
                switch (MessageType.fromByte(msgBytes[1])) {
                    case DATA:
                        return new DataMessage(msgBytes);
                    case CLOSE:
                        return new CloseMessage(msgBytes);
                    case ACK:
                        return new ACKMessage(msgBytes);
                    case HEARTBEAT:
                        return new HeartbeatMessage(msgBytes);
                    default:
                        throw new IncorrectMessageException("Unknown Message Type");
                }
            } catch (final IllegalArgumentException e) {
                throw new IncorrectMessageException("Unknown Message Type");
            }
        } else {
            throw new IncorrectMessageException("Packet is too short");
        }
    }

} public interface ReliableMessage extends Message {

    int getSequence();

    void setSequence(int sequence);

} public class ACKMessage implements Message {

    private static final byte VERSION = DragoniteGlobalConstants.PROTOCOL_VERSION;

    private static final MessageType TYPE = MessageType.ACK;

    public static final int FIXED_LENGTH = 8;

    private int[] sequenceList;

    private int consumedSeq;

    public ACKMessage(final int[] sequenceList, final int consumedSeq) {
        this.sequenceList = sequenceList;
        this.consumedSeq = consumedSeq;
    }

    public ACKMessage(final byte[] msg) throws IncorrectMessageException {
        final BinaryReader reader = new BinaryReader(msg);

        try {

            final byte remoteVersion = reader.getSignedByte();
            final byte remoteType = reader.getSignedByte();

            if (remoteVersion != VERSION) {
                throw new IncorrectMessageException("Incorrect version (" + remoteVersion + ", should be " + VERSION + ")");
            }
            if (remoteType != TYPE.getValue()) {
                throw new IncorrectMessageException("Incorrect type (" + remoteType + ", should be " + TYPE + ")");
            }

            consumedSeq = reader.getSignedInt();

            final int seqCount = reader.getUnsignedShort();

            sequenceList = new int[seqCount];
            for (int i = 0; i < seqCount; i++) {
                sequenceList[i] = reader.getSignedInt();
            }

        } catch (final BufferUnderflowException e) {
            throw new IncorrectMessageException("Incorrect message length");
        }
    }

    @Override
    public byte getVersion() {
        return VERSION;
    }

    @Override
    public MessageType getType() {
        return TYPE;
    }

    @Override
    public byte[] toBytes() {
        final BinaryWriter writer = new BinaryWriter(getFixedLength() + sequenceList.length * Integer.BYTES);

        writer.putSignedByte(VERSION)
                .putSignedByte(TYPE.getValue())
                .putSignedInt(consumedSeq)
                .putUnsignedShort(sequenceList.length);

        for (final int seq : sequenceList) {
            writer.putSignedInt(seq);
        }

        return writer.toBytes();
    }

    @Override
    public int getFixedLength() {
        return FIXED_LENGTH;
    }

    public int[] getSequenceList() {
        return sequenceList;
    }

    public void setSequenceList(final int[] sequenceList) {
        this.sequenceList = sequenceList;
    }

    public int getConsumedSeq() {
        return consumedSeq;
    }

    public void setConsumedSeq(final int consumedSeq) {
        this.consumedSeq = consumedSeq;
    }
} public class CloseMessage implements ReliableMessage {

    private static final byte VERSION = DragoniteGlobalConstants.PROTOCOL_VERSION;

    private static final MessageType TYPE = MessageType.CLOSE;

    public static final int FIXED_LENGTH = 8;

    private int sequence;

    private short status;

    public CloseMessage(final int sequence, final short status) {
        this.sequence = sequence;
        this.status = status;
    }

    public CloseMessage(final byte[] msg) throws IncorrectMessageException {
        final BinaryReader reader = new BinaryReader(msg);

        try {

            final byte remoteVersion = reader.getSignedByte();
            final byte remoteType = reader.getSignedByte();

            if (remoteVersion != VERSION) {
                throw new IncorrectMessageException("Incorrect version (" + remoteVersion + ", should be " + VERSION + ")");
            }
            if (remoteType != TYPE.getValue()) {
                throw new IncorrectMessageException("Incorrect type (" + remoteType + ", should be " + TYPE + ")");
            }

            sequence = reader.getSignedInt();

            status = reader.getSignedShort();

        } catch (final BufferUnderflowException e) {
            throw new IncorrectMessageException("Incorrect message length");
        }
    }

    @Override
    public byte getVersion() {
        return VERSION;
    }

    @Override
    public MessageType getType() {
        return TYPE;
    }

    @Override
    public byte[] toBytes() {
        final BinaryWriter writer = new BinaryWriter(getFixedLength());

        writer.putSignedByte(VERSION)
                .putSignedByte(TYPE.getValue())
                .putSignedInt(sequence)
                .putSignedShort(status);

        return writer.toBytes();
    }

    @Override
    public int getFixedLength() {
        return FIXED_LENGTH;
    }

    @Override
    public int getSequence() {
        return sequence;
    }

    @Override
    public void setSequence(final int sequence) {
        this.sequence = sequence;
    }

    public short getStatus() {
        return status;
    }

    public void setStatus(final short status) {
        this.status = status;
    }
} public class DevConsoleWebServer {

    private static final String WEB_TITLE = "Dragonite Developer Web Interface";

    private static final Charset CHARSET = StandardCharsets.UTF_8;

    private static final int SHUTDOWN_SEC = 2;

    private final HttpServer httpServer;

    private final StatisticsProvider statisticsProvider;

    public DevConsoleWebServer(final InetSocketAddress bindAddress, final StatisticsProvider provider) throws IOException {
        this.statisticsProvider = provider;

        httpServer = HttpServer.create(bindAddress, 0);
        httpServer.createContext("/statistics", httpExchange -> {
            final byte[] jsonBytes = getJSON(statisticsProvider.getLatest()).getBytes(CHARSET);
            httpExchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
            httpExchange.sendResponseHeaders(200, jsonBytes.length);
            httpExchange.getResponseBody().write(jsonBytes);
            httpExchange.close();
        });

        httpServer.start();
    }

    private String getJSON(final List<DragoniteSocketStatistics> statisticsList) {
        return "{\"title\":\"" + WEB_TITLE +
                "\",\"version\":\"" + DragoniteGlobalConstants.LIBRARY_VERSION +
                "\",\"connections\":" + getConnectionsJSONArray(statisticsList) + "}";
    }

    private String getConnectionsJSONArray(final List<DragoniteSocketStatistics> statisticsList) {
        StringBuilder json = new StringBuilder("[");
        int i = 0;
        for (final DragoniteSocketStatistics statistics : statisticsList) {
            final String cinfo = "{\"remote\":\"" + statistics.getRemoteAddress().toString() +
                    "\",\"description\":\"" + statistics.getDescription() +
                    "\",\"rtt\":" + statistics.getEstimatedRTT() +
                    ",\"devrtt\":" + statistics.getDevRTT() +
                    ",\"send\":" + statistics.getSendLength() +
                    ",\"sendraw\":" + statistics.getSendRawLength() +
                    ",\"sendpkt\":" + statistics.getSendCount() +
                    ",\"resend\":" + statistics.getResendCount() +
                    ",\"recv\":" + statistics.getReadLength() +
                    ",\"recvraw\":" + statistics.getReceiveRawLength() +
                    ",\"recvcount\":" + statistics.getReceiveCount() +
                    ",\"dup\":" + statistics.getDupCount() + "}";
            json.append(cinfo);
            if (++i != statisticsList.size()) {
                json.append(",");
            }
        }
        json.append("]");
        return json.toString();
    }

    public void stop() {
        httpServer.stop(SHUTDOWN_SEC);
    }

}
import com.vecsight.dragonite.sdk.config.DragoniteSocketParameters;
import com.vecsight.dragonite.sdk.cryptor.PacketCryptor;
import com.vecsight.dragonite.sdk.exception.IncorrectMessageException;
import com.vecsight.dragonite.sdk.exception.SenderClosedException;
import com.vecsight.dragonite.sdk.msg.Message;
import com.vecsight.dragonite.sdk.msg.MessageParser;
import com.vecsight.dragonite.sdk.msg.ReliableMessage;
import com.vecsight.dragonite.sdk.msg.types.ACKMessage;
import com.vecsight.dragonite.sdk.msg.types.CloseMessage;
import com.vecsight.dragonite.sdk.web.DevConsoleWebServer;
import java.io.IOException;
import java.net.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
/*
 * The Dragonite Project
 * -------------------------
 * See the LICENSE file in the root directory for license information.
 */


package com.vecsight.dragonite.sdk.socket;



public class DragoniteServer {

    //From parameters
    private final int packetSize, maxReceiveWindow;
    private final int windowMultiplier;
    private final int resendMinDelayMS;
    private final int heartbeatIntervalSec, receiveTimeoutSec;
    private final boolean autoSplit;
    private final boolean enableWebPanel;
    private final PacketCryptor packetCryptor;
    private final int cryptorOverhead;
    //end

    private volatile long defaultSendSpeed;

    private final DatagramSocket datagramSocket;

    private final BlockingQueue<DatagramPacket> packetBuffer;

    private final Thread receiveThread; //THREAD

    private final Thread handleThread; //THREAD

    private final Thread aliveDetectThread; //THREAD

    private volatile boolean doReceive = true, doHandle = true, doAliveDetect = true;

    private final ConcurrentMap<SocketAddress, DragoniteServerSocket> connectionMap = new ConcurrentHashMap<>();

    private final BlockingQueue<DragoniteSocket> acceptQueue = new LinkedBlockingQueue<>();

    private volatile boolean alive = true;

    private final Object closeLock = new Object();

    private final DevConsoleWebServer devConsoleWebServer;

    private final InetSocketAddress devConsoleBindAddress;

    public DragoniteServer(final InetAddress address, final int port, final long defaultSendSpeed, final DragoniteSocketParameters parameters) throws SocketException {
        datagramSocket = new DatagramSocket(port, address);

        //set from parameters
        packetSize = parameters.getPacketSize();
        maxReceiveWindow = parameters.getMaxPacketBufferSize();
        windowMultiplier = parameters.getWindowMultiplier();
        resendMinDelayMS = parameters.getResendMinDelayMS();
        heartbeatIntervalSec = parameters.getHeartbeatIntervalSec();
        receiveTimeoutSec = parameters.getReceiveTimeoutSec();
        autoSplit = parameters.isAutoSplit();
        enableWebPanel = parameters.isEnableWebPanel();
        devConsoleBindAddress = parameters.getWebPanelBindAddress();
        packetCryptor = parameters.getPacketCryptor();
        cryptorOverhead = packetCryptor != null ? packetCryptor.getMaxAdditionalBytesLength() : 0;

        datagramSocket.setTrafficClass(parameters.getTrafficClass());
        //end

        this.defaultSendSpeed = defaultSendSpeed;

        if (maxReceiveWindow == 0) {
            packetBuffer = new LinkedBlockingQueue<>();
        } else {
            packetBuffer = new LinkedBlockingQueue<>(maxReceiveWindow);
        }

        receiveThread = new Thread(() -> {
            try {
                while (doReceive) {
                    final byte[] b = new byte[packetSize + cryptorOverhead];
                    final DatagramPacket packet = new DatagramPacket(b, b.length);
                    try {
                        datagramSocket.receive(packet);
                        packetBuffer.put(packet);
                    } catch (final IOException ignored) {
                    }
                }
            } catch (final InterruptedException ignored) {
            }
        }, "DS-Receive");
        receiveThread.start();

        handleThread = new Thread(() -> {
            try {
                while (doHandle) {
                    final DatagramPacket packet = packetBuffer.take();
                    handlePacket(packet);
                }
            } catch (final InterruptedException ignored) {
                //okay
            }
        }, "DS-PacketHandle");
        handleThread.start();

        aliveDetectThread = new Thread(() -> {
            try {
                while (doAliveDetect) {
                    final long current = System.currentTimeMillis();
                    final Iterator<Map.Entry<SocketAddress, DragoniteServerSocket>> it = connectionMap.entrySet().iterator();
                    while (it.hasNext()) {
                        final DragoniteServerSocket socket = it.next().getValue();
                        if (socket.isAlive()) {
                            if (current - socket.getLastReceiveTime() > receiveTimeoutSec * 1000) {
                                //TIMEOUT
                                //System.out.println("TIMEOUT " + socket.getRemoteSocketAddress());
                                socket.destroy_NoRemove();
                                it.remove();
                            } else if (current - socket.getLastSendTime() > heartbeatIntervalSec * 1000) {
                                try {
                                    //TODO Fix blocking
                                    socket.sendHeartbeat();
                                } catch (IOException | SenderClosedException ignored) {