All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.rcll.llsf_comm.ProtobufTcpConnection Maven / Gradle / Ivy

There is a newer version: 0.1.19.1
Show newest version
package com.rcll.llsf_comm;

import com.google.protobuf.Descriptors;
import com.google.protobuf.Descriptors.EnumDescriptor;
import com.google.protobuf.GeneratedMessageV3;
import com.rcll.llsf_exceptions.EncryptedStreamMessageException;
import com.rcll.llsf_exceptions.UnknownProtocolVersionException;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

/**
 * The ProtobufClient is a client to communicate with a refbox via protobuf messages. You can
 * send stream messages (TCP) by calling the enqueue method. To receive messages, register a
 * ProtobufMessageHandler, incoming messages will be passed to your handler. To send and receive
 * broadcast messages (UDP), use the ProtobufBroadcastPeer.
 */
public class ProtobufTcpConnection implements ProtobufConnection {

    private String hostname;
    private int port;
    private SocketChannel sockchan;

    private Queue queue1;
    private Queue queue2;
    private Queue act_q;
    private Queue send_q;
    private ConThread con;
    private SendThread send;
    private RecvThread recv;

    private HashMap msgs = new HashMap<>();
    private HashMap classNameToKey = new HashMap<>();
    private ProtobufMessageHandler handler;

    private boolean is_connected = false;

    /**
     * Instantiates a new ProtobufClient. This method does not connect (see connect).
     *
     * @param hostname the IP address of the refbox
     * @param port     the port to which to connect
     * @see connect()
     */
    public ProtobufTcpConnection(String hostname, int port) {
        this.hostname = hostname;
        this.port = port;
    }

    /**
     * Tries to connect to the refbox at the IP address and port given in the contructor.
     * Throws an UnknownHostException if the hostname defined in the contructor is not known.
     * Throws an IOException if the connection cannot be established.
     *
     * @throws IOException Signals that the connection to the refbox cannot be established.
     */
    public void connect() throws InterruptedException, IOException, UnresolvedAddressException {
        con = new ConThread();
        con.start();
        try {
            con.join(); //Wait for Connection Thread to succeed
            if (con.getException() == null) { //there was no Exception

                //Initialize Queues
                queue1 = new LinkedList();
                queue2 = new LinkedList();
                act_q = queue1;
                send_q = queue2;

                //Start Send and Receive Threads
                send = new SendThread();
                send.start();
                recv = new RecvThread();
                recv.start();

            } else {
                if (con.getException() instanceof UnknownHostException) {
                    throw new UnknownHostException("Don't know about host " + hostname);
                } else if (con.getException() instanceof IOException) {
                    throw new IOException("Couldn't get I/O for the connection to " + hostname);
                } else if (con.getException() instanceof UnresolvedAddressException) {
                    throw new UnresolvedAddressException();
                }
            }
        } catch (InterruptedException e) {
            throw e;
        }

        is_connected = true;
    }

    /**
     * Disconnects from the refbox.
     */
    public void disconnect(boolean wait) {
        try {
            if (send != null) {
                send.terminate();
            }
            if (recv != null) {
                recv.terminate();
            }

            if (wait) {
                try {
                    if (send != null) {
                        send.join();
                    }
                    if (recv != null) {
                        recv.join();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            if (sockchan != null) {
                sockchan.close();
                sockchan = null;
            }
            //sockchan.close();

            is_connected = false;
        } catch (IOException e) {
        }
    }

    /**
     * Checks if a connection to the refbox is established.
     *
     * @return true, if connection is established.
     */
    public boolean is_connected() {
        return is_connected;
    }

    /**
     * Registers a new ProtobufMessageHandler responsible for handling and deserializing incoming
     * protobuf messages. Required if you want to access received messages. Only allows one registered
     * handler at the same time.
     *
     * @param handler the ProtobufMessageHandler
     * @see ProtobufMessageHandler
     */
    public void register_handler(ProtobufMessageHandler handler) {
        this.handler = handler;
    }

    /**
     * Adds and registers a new protobuf message type. This is required to instantiate the correct
     * protobuf message object when a message is received from the refbox. For example, if you want
     * the client to be able to receive and process a GameState message, call
     * client.<GameState>add_message(GameState.class).
     *
     * @param  the type of the protobuf message to register, has to extend from GeneratedMessage
     * @param c   the class object of the same protobuf message
     */
    @SuppressWarnings("unchecked")
    public  void add_message(Class c) {
        try {
            Method m = c.getMethod("getDefaultInstance", (Class[]) null);
            T msg = (T) m.invoke((Object[]) null, (Object[]) null);
            EnumDescriptor desc = msg.getDescriptorForType().findEnumTypeByName("CompType");

            int cmp_id = desc.findValueByName("COMP_ID").getNumber();
            int msg_id = desc.findValueByName("MSG_TYPE").getNumber();
            Key key = new Key(cmp_id, msg_id);
            msgs.put(key, msg);
            classNameToKey.put(c.getName(), key);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (SecurityException e) {
            e.printStackTrace();
        }
    }

    private void handle_message(int cmp_id, int msg_id, ByteBuffer in_msg) {
        if (handler != null) {
            for (Map.Entry e : msgs.entrySet()) {
                Key key = e.getKey();
                if (key.cmp_id == cmp_id && key.msg_id == msg_id) {
                    handler.handle_message(in_msg, e.getValue());
                    break;
                }
            }
        }
    }


    /**
     * Puts a ProtobufMessage into the send queue in order to be sent out to the refbox.
     *
     * @param msg the ProtobufMessage to send
     * @see ProtobufMessage
     */
    @Override
    public void enqueue(ProtobufMessage msg) {
        synchronized (act_q) {
            act_q.add(msg);
            try {
                act_q.notifyAll();
            } catch (IllegalMonitorStateException e) {
            }
        }
    }

    @Override
    public void enqueue(GeneratedMessageV3 msg) {
        if (!classNameToKey.containsKey(msg.getClass().getName())) {
            throw new RuntimeException("classNameToKey does not container entry for: " + msg.getClass().getName());
        }
        Key key = classNameToKey.get(msg.getClass().getName());
        this.enqueue(new ProtobufMessage(key.cmp_id, key.msg_id, msg));
    }

    public  void enqueue(GeneratedMessageV3 msg, Class c) {
        Method m = null;
        try {
            m = c.getMethod("getDefaultInstance", (Class[]) null);
            Descriptors.EnumDescriptor desc = msg.getDescriptorForType().findEnumTypeByName("CompType");

            int cmp_id = desc.findValueByName("COMP_ID").getNumber();
            int msg_id = desc.findValueByName("MSG_TYPE").getNumber();
            this.enqueue(new ProtobufMessage(cmp_id, msg_id, msg));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    private class ConThread extends Thread {

        private Exception e = null;

        public void run() {
            try {
                sockchan = SocketChannel.open();
                sockchan.connect(new InetSocketAddress(hostname, port));
                sockchan.finishConnect();
            } catch (UnknownHostException e) {
                this.e = e;
            } catch (IOException e) {
                this.e = e;
            } catch (UnresolvedAddressException e) {
                this.e = e;
            }
        }

        public Exception getException() {
            return this.e;
        }

    }

    private class SendThread extends Thread {

        private boolean run = true;

        public void run() {
            while (run) {
                synchronized (act_q) {
                    if (act_q.isEmpty()) {
                        try {
                            act_q.wait();
                            Queue help_q = send_q;
                            send_q = act_q;
                            act_q = help_q;
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                }
                synchronized (send_q) {
                    try {
                        while (!send_q.isEmpty()) {
                            ProtobufMessage msg = send_q.remove();
                            if (sockchan != null && sockchan.isConnected()) {
                                sockchan.write(msg.serialize(false, null));
                            } else {
                                throw new IOException();
                            }
                        }
                    } catch (IOException e) {
                        run = false;
                        disconnect(true);
                        handler.connection_lost(e);
                    }
                }
            }
        }

        public void terminate() {
            this.run = false;
            this.interrupt();
            synchronized (act_q) {
                act_q.notifyAll();
            }
        }

    }

    private class RecvThread extends Thread {

        private boolean run = true;

        public void run() {
            while (run) {
                try {
                    //read headers
                    ByteBuffer in_header = ByteBuffer.allocate(ProtobufMessage.FRAME_HEADER_SIZE + ProtobufMessage.MESSAGE_HEADER_SIZE);
                    if (sockchan != null && sockchan.isConnected()) {
                        int read = sockchan.read(in_header);
                        if (read == -1) {
                            throw new IOException();
                        }
                    } else {
                        throw new IOException();
                    }
                    in_header.order(ByteOrder.BIG_ENDIAN);
                    in_header.rewind();

                    ProtobufFrameHeader frameHeader = readHeader(in_header);

                    int protocolVersion = frameHeader.getProtocolVersion();
                    if (protocolVersion != 2) {
                        throw new UnknownProtocolVersionException("Protocol version " + protocolVersion + " does not exist and cannot be processed.");
                    }

                    int cipher = frameHeader.getCipher();
                    if (cipher != 0) {
                        throw new EncryptedStreamMessageException("Encryption in stream messages is not allowed.");
                    }

                    //read payload
                    int cid = in_header.getShort();
                    int msgid = in_header.getShort();

                    int size = frameHeader.getPayloadSize();

                    if (size < 0 || size > 1000000) {
                        continue;
                    }

                    ByteBuffer in_msg = ByteBuffer.allocate(size - ProtobufMessage.MESSAGE_HEADER_SIZE);
                    while (in_msg.remaining() != 0) {
                        if (sockchan != null && sockchan.isConnected()) {
                            int read = sockchan.read(in_msg);
                            if (read == -1) {
                                throw new IOException();
                            }
                        } else {
                            throw new IOException();
                        }
                    }

                    handle_message(cid, msgid, (ByteBuffer) in_msg.rewind());
                } catch (IOException e) {
                    run = false;
                    disconnect(true);
                    handler.connection_lost(e);
                }
            }
        }

        public void terminate() {
            this.run = false;
        }

        private ProtobufFrameHeader readHeader(ByteBuffer header) {
            ProtobufFrameHeader frameHeader = new ProtobufFrameHeader();

            frameHeader.setProtocolVersion((int) header.get());
            frameHeader.setCipher((int) header.get());
            frameHeader.setReserved1((int) header.get());
            frameHeader.setReserved2((int) header.get());

            frameHeader.setPayloadSize(header.getInt());

            return frameHeader;
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy