com.xeiam.xchange.streaming.socketio.IOConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.xeiam.xchange.streaming.socketio.IOConnection.java

Source

/**
 * Copyright (C) 2012 - 2013, Enno Boland
 * Copyright (C) 2012 - 2013 Xeiam LLC http://xeiam.com
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.xeiam.xchange.streaming.socketio;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Scanner;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * The Class IOConnection.
 */
class IOConnection implements IOCallback {

    private final Logger log = LoggerFactory.getLogger(IOConnection.class);

    private final ObjectMapper objectMapper = new ObjectMapper();

    public static final String FRAME_DELIMITER = "\ufffd";

    /**
     * The Constant STATE_INIT.
     */
    private static final int STATE_INIT = 0;

    /**
     * The Constant STATE_HANDSHAKE.
     */
    private static final int STATE_HANDSHAKE = 1;

    /**
     * The Constant STATE_CONNECTING.
     */
    private static final int STATE_CONNECTING = 2;

    /**
     * The Constant STATE_READY.
     */
    private static final int STATE_READY = 3;

    /**
     * The Constant STATE_INTERRUPTED.
     */
    private static final int STATE_INTERRUPTED = 4;

    /**
     * The Constant STATE_INVALID.
     */
    private static final int STATE_INVALID = 6;

    /**
     * The state.
     */
    private int state = STATE_INIT;

    /**
     * Socket.io path.
     */
    public static final String SOCKET_IO_1 = "/socket.io/1/";

    /**
     * The SSL socket factory for HTTPS connections
     */
    private static SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();

    /**
     * All available connections.
     */
    private static HashMap<String, List<IOConnection>> connections = new HashMap<String, List<IOConnection>>();

    /**
     * The url for this connection.
     */
    private URL url;

    /**
     * The transport for this connection.
     */
    private IOTransport transport;

    /**
     * The connection timeout.
     */
    private final int connectTimeout = 10000;

    /**
     * The session id of this connection.
     */
    private String sessionId;

    /**
     * The heartbeat timeout. Set by the server
     */
    private long heartbeatTimeout;

    /**
     * The closing timeout. Set By the server
     */
    private long closingTimeout;

    /**
     * The protocols supported by the server.
     */
    private List<String> protocols;

    /**
     * The output buffer used to cache messages while (re-)connecting.
     */
    private ConcurrentLinkedQueue<String> outputBuffer = new ConcurrentLinkedQueue<String>();

    /**
     * The sockets of this connection.
     */
    private final HashMap<String, SocketIO> sockets = new HashMap<String, SocketIO>();

    /**
     * Custom Request headers used while handshaking
     */
    private final Properties headers;

    /**
     * The first socket to be connected. the socket.io server does not send a connected response to this one.
     */
    private SocketIO firstSocket = null;

    /**
     * The reconnect timer. IOConnect waits a second before trying to reconnect
     */
    final private Timer backgroundTimer = new Timer("backgroundTimer");

    /**
     * A String representation of {@link #url}.
     */
    private String urlStr;

    /**
     * The last occurred exception, which will be given to the user if IOConnection gives up.
     */
    private Exception lastException;

    /**
     * The next ID to use.
     */
    private int nextId = 1;

    /**
     * Acknowledges.
     */
    HashMap<Integer, IOAcknowledge> acknowledge = new HashMap<Integer, IOAcknowledge>();

    /**
     * true if there's already a keepalive in {@link #outputBuffer}.
     */
    private boolean keepAliveInQueue;

    /**
     * The heartbeat timeout task. Only null before connection has been initialised.
     */
    private HearbeatTimeoutTask heartbeatTimeoutTask;

    /**
     * The Class HearbeatTimeoutTask. Handles dropping this IOConnection if no heartbeat is received within life time.
     */
    private class HearbeatTimeoutTask extends TimerTask {

        @Override
        public void run() {

            setState(STATE_INVALID);
            error(new SocketIOException(
                    "Timeout Error. No heartbeat from server within life time of the socket. closing.",
                    lastException));
        }
    }

    /**
     * The reconnect task. Null if no reconnection is in progress.
     */
    private ReconnectTask reconnectTask = null;

    /**
     * The Class ReconnectTask. Handles reconnect attempts
     */
    private class ReconnectTask extends TimerTask {

        @Override
        public void run() {

            connectTransport();
            if (!keepAliveInQueue) {
                sendPlain("2::");
                keepAliveInQueue = true;
            }
        }
    }

    /**
     * The Class ConnectThread. Handles connecting to the server with an {@link IOTransport}
     */
    private class ConnectThread extends Thread {

        /**
         * Instantiates a new thread for handshaking/connecting.
         */
        public ConnectThread() {

            super("ConnectThread");
        }

        /**
         * Tries handshaking if necessary and connects with corresponding transport afterwards.
         */
        @Override
        public void run() {

            if (IOConnection.this.getState() == STATE_INIT) {
                handshake();
            }
            connectTransport();
        }

    }

    ;

    /**
     * Set the socket factory used for SSL connections.
     * 
     * @param socketFactory The socket factory
     */
    public static void setDefaultSSLSocketFactory(SSLSocketFactory socketFactory) {

        sslSocketFactory = socketFactory;
    }

    /**
     * Creates a new connection or returns the corresponding one.
     * 
     * @param origin the origin
     * @param socket the socket
     * @return a IOConnection object
     */
    static public IOConnection register(String origin, SocketIO socket) {

        List<IOConnection> list = connections.get(origin);
        if (list == null) {
            list = new LinkedList<IOConnection>();
            connections.put(origin, list);
        } else {
            for (IOConnection connection : list) {
                if (connection.register(socket)) {
                    return connection;
                }
            }
        }

        IOConnection connection = new IOConnection(origin, socket);
        list.add(connection);
        return connection;
    }

    /**
     * Connects a socket to the IOConnection.
     * 
     * @param socket the socket to be connected
     * @return true, if successfully registered on this transport, otherwise false.
     */
    public boolean register(SocketIO socket) {

        String namespace = socket.getNamespace();
        if (sockets.containsKey(namespace)) {
            return false;
        }
        sockets.put(namespace, socket);
        socket.setHeaders(headers);
        IOMessage connect = new IOMessage(IOMessage.TYPE_CONNECT, socket.getNamespace(), "");
        sendPlain(connect.toString());
        return true;
    }

    /**
     * Disconnect a socket from the IOConnection. Shuts down this IOConnection if no further connections are available for this IOConnection.
     * 
     * @param socket the socket to be shut down
     */
    public void unregister(SocketIO socket) {

        sendPlain("0::" + socket.getNamespace());
        sockets.remove(socket.getNamespace());
        socket.getCallback().onDisconnect();

        if (sockets.size() == 0) {
            cleanup();
        }
    }

    /**
     * Handshake.
     */
    private void handshake() {

        URL url;
        String response;
        URLConnection connection;
        try {
            setState(STATE_HANDSHAKE);
            url = new URL(this.url.getProtocol() + "://" + this.url.getAuthority() + SOCKET_IO_1
                    + (this.url.getQuery() == null ? "" : "?" + this.url.getQuery()));
            connection = url.openConnection();
            if (connection instanceof HttpsURLConnection) {
                ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);
            }
            connection.setConnectTimeout(connectTimeout);
            connection.setReadTimeout(connectTimeout);

            /* Setting the request headers */
            for (Entry<Object, Object> entry : headers.entrySet()) {
                connection.setRequestProperty((String) entry.getKey(), (String) entry.getValue());
            }
            log.debug("> " + connection.toString());
            InputStream stream = connection.getInputStream();
            Scanner in = new Scanner(stream);
            response = in.nextLine();
            log.debug("< " + response);
            String[] data = response.split(":");
            sessionId = data[0];
            heartbeatTimeout = Long.parseLong(data[1]) * 1000;
            closingTimeout = Long.parseLong(data[2]) * 1000;
            protocols = Arrays.asList(data[3].split(","));
        } catch (Exception e) {
            error(new SocketIOException("Error while handshaking", e));
        }
    }

    /**
     * Connect transport
     */
    private void connectTransport() {

        if (getState() == STATE_INVALID) {
            return;
        }
        setState(STATE_CONNECTING);
        if (protocols.contains(WebSocketTransport.TRANSPORT_NAME)) {
            transport = WebSocketTransport.create(this.url.getProtocol() + "://" + this.url.getAuthority(), this);
        } else if (protocols.contains(XhrTransport.TRANSPORT_NAME)) {
            transport = XhrTransport.create(this.url.getProtocol() + "://" + this.url.getAuthority(), this);
        } else {
            error(new SocketIOException(
                    "Server supports no available transports. You should reconfigure the server to support a available transport"));
            return;
        }
        transport.connect();
    }

    /**
     * Creates a new {@link IOAcknowledge} instance which sends its arguments back to the server.
     * 
     * @param message the message
     * @return an {@link IOAcknowledge} instance, may be <code>null</code> if server doesn't request one.
     */
    private IOAcknowledge remoteAcknowledge(IOMessage message) {

        String _id = message.getId();
        if (_id.equals("")) {
            return null;
        } else if (!_id.endsWith("+")) {
            _id = _id + "+";
        }
        final String id = _id;
        final String endPoint = message.getEndpoint();
        return new IOAcknowledge() {

            @Override
            public void ack(Object... args) {

                String jsonString = null;
                try {
                    jsonString = objectMapper.writeValueAsString(args);
                } catch (Exception e) {
                    error(new SocketIOException(
                            "You can only put values in IOAcknowledge.ack() which can be serialized", e));
                }
                IOMessage ackMsg = new IOMessage(IOMessage.TYPE_ACK, endPoint, id + jsonString);
                sendPlain(ackMsg.toString());
            }
        };
    }

    /**
     * adds an {@link IOAcknowledge} to an {@link IOMessage}.
     * 
     * @param message the {@link IOMessage}
     * @param ack the {@link IOAcknowledge}
     */
    private void synthesizeAck(IOMessage message, IOAcknowledge ack) {

        if (ack != null) {
            int id = nextId++;
            acknowledge.put(id, ack);
            message.setId(id + "+");
        }
    }

    /**
     * Instantiates a new IOConnection.
     * 
     * @param url the URL
     * @param socket the socket
     */
    private IOConnection(String url, SocketIO socket) {

        try {
            this.url = new URL(url);
            this.urlStr = url;
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        firstSocket = socket;
        headers = socket.getHeaders();
        sockets.put(socket.getNamespace(), socket);
        new ConnectThread().start();
    }

    /**
     * Cleanup. IOConnection is not usable after calling this.
     */
    private void cleanup() {

        setState(STATE_INVALID);
        if (transport != null) {
            transport.disconnect();
        }
        sockets.clear();
        synchronized (connections) {
            List<IOConnection> con = connections.get(urlStr);
            if (con != null && con.size() > 1) {
                con.remove(this);
            } else {
                connections.remove(urlStr);
            }
        }
        log.debug("cleanup");
        backgroundTimer.cancel();
    }

    /**
     * Populates an error to the connected {@link IOCallback}s and shuts down.
     * 
     * @param e an exception
     */
    protected void error(SocketIOException e) {

        // Ensure any errors during disconnect are ignored
        if (state != STATE_INVALID) {

            for (SocketIO socket : sockets.values()) {
                socket.getCallback().onError(e);
            }
            cleanup();
        }
    }

    /**
     * Sends a plain message to the {@link IOTransport}.
     * 
     * @param text the Text to be send.
     */
    private void sendPlain(String text) {

        synchronized (outputBuffer) {
            if (getState() == STATE_READY) {
                try {
                    log.debug("> " + text);
                    transport.send(text);
                } catch (Exception e) {
                    log.debug("IOEx: saving");
                    outputBuffer.add(text);
                }
            } else {
                outputBuffer.add(text);
            }
        }
    }

    /**
     * Invalidates an {@link IOTransport}, used for forced reconnecting.
     */
    private void invalidateTransport() {

        if (transport != null) {
            transport.invalidate();
        }
        transport = null;
    }

    /**
     * Reset timeout.
     */
    private void resetTimeout() {

        if (heartbeatTimeoutTask != null) {
            heartbeatTimeoutTask.cancel();
        }
        heartbeatTimeoutTask = new HearbeatTimeoutTask();
        backgroundTimer.schedule(heartbeatTimeoutTask, closingTimeout + heartbeatTimeout);
    }

    /**
     * finds the corresponding callback object to an incoming message. Returns a dummy callback if no corresponding callback can be found
     * 
     * @param message the message
     * @return the iO callback
     * @throws SocketIOException
     */
    private IOCallback findCallback(IOMessage message) throws SocketIOException {

        if ("".equals(message.getEndpoint())) {
            return this;
        }
        SocketIO socket = sockets.get(message.getEndpoint());
        if (socket == null) {
            throw new SocketIOException("Cannot find socket for '" + message.getEndpoint() + "'");
        }
        return socket.getCallback();
    }

    /**
     * Transport connected. {@link IOTransport} calls this when a connection is established.
     */
    public void transportConnected() {

        setState(STATE_READY);
        if (reconnectTask != null) {
            reconnectTask.cancel();
            reconnectTask = null;
        }
        resetTimeout();
        synchronized (outputBuffer) {
            if (transport.canSendBulk()) {
                ConcurrentLinkedQueue<String> outputBuffer = this.outputBuffer;
                this.outputBuffer = new ConcurrentLinkedQueue<String>();
                try {
                    // DEBUG
                    String[] texts = outputBuffer.toArray(new String[outputBuffer.size()]);
                    log.debug("Bulk start:");
                    for (String text : texts) {
                        log.debug("> " + text);
                    }
                    log.debug("Bulk end");
                    // DEBUG END
                    transport.sendBulk(texts);
                } catch (IOException e) {
                    this.outputBuffer = outputBuffer;
                }
            } else {
                String text;
                while ((text = outputBuffer.poll()) != null) {
                    sendPlain(text);
                }
            }
            this.keepAliveInQueue = false;
        }
    }

    /**
     * Transport disconnected. {@link IOTransport} calls this when a connection has been shut down.
     */
    public void transportDisconnected() {

        log.debug("transportDisconnected called!");

        this.lastException = null;
        setState(STATE_INTERRUPTED);
        reconnect();
    }

    /**
     * Transport error.
     * 
     * @param error the error {@link IOTransport} calls this, when an exception has occurred and the transport is not usable anymore.
     */
    public void transportError(Exception error) {

        this.lastException = error;
        setState(STATE_INTERRUPTED);
        reconnect();
    }

    /**
     * {@link IOTransport} should call this function if it does not support framing. If it does, transportMessage should be used
     * 
     * @param text the text
     */
    public void transportData(String text) {

        if (!text.startsWith(FRAME_DELIMITER)) {
            transportMessage(text);
            return;
        }

        Iterator<String> fragments = Arrays.asList(text.split(FRAME_DELIMITER)).listIterator(1);
        while (fragments.hasNext()) {
            int length = Integer.parseInt(fragments.next());
            String string = fragments.next();
            // Potential BUG: it is not defined if length is in bytes or
            // characters. Assuming characters.

            if (length != string.length()) {
                error(new SocketIOException("Garbage from server: " + text));
                return;
            }

            transportMessage(string);
        }
    }

    /**
     * Transport message. {@link IOTransport} calls this, when a message has been received.
     * 
     * @param text the text
     */
    public void transportMessage(String text) {

        log.debug("< " + text);

        IOMessage message;
        try {
            message = new IOMessage(text);
        } catch (Exception e) {
            error(new SocketIOException("Garbage from server: " + text, e));
            return;
        }
        resetTimeout();
        switch (message.getType()) {
        case IOMessage.TYPE_DISCONNECT:
            try {
                findCallback(message).onDisconnect();
            } catch (Exception e) {
                error(new SocketIOException("Exception was thrown in onDisconnect()", e));
            }
            break;
        case IOMessage.TYPE_CONNECT:
            try {
                if (firstSocket != null && "".equals(message.getEndpoint())) {
                    if (firstSocket.getNamespace().equals("")) {
                        firstSocket.getCallback().onConnect();
                    } else {
                        IOMessage connect = new IOMessage(IOMessage.TYPE_CONNECT, firstSocket.getNamespace(), "");
                        sendPlain(connect.toString());
                    }
                } else {
                    findCallback(message).onConnect();
                }
                firstSocket = null;
            } catch (Exception e) {
                error(new SocketIOException("Exception was thrown in onConnect()", e));
            }
            break;
        case IOMessage.TYPE_HEARTBEAT:
            sendPlain("2::");
            break;
        case IOMessage.TYPE_MESSAGE:
            try {
                findCallback(message).onMessage(message.getData(), remoteAcknowledge(message));
            } catch (Exception e) {
                error(new SocketIOException(
                        "Exception was thrown in onMessage(String).\n" + "Message was: " + message.toString(), e));
            }
            break;
        case IOMessage.TYPE_JSON_MESSAGE:

            // TODO perhaps try to catch this exception later instead of parsing the json string twice, just trust that it's really JSON.
            try {
                // test if JSON is valid by catching a parse Exception
                objectMapper.readValue(message.getData(), new TypeReference<Map<String, Object>>() {
                });
                findCallback(message).onJSONMessage(message.getData(), remoteAcknowledge(message));
            } catch (JsonParseException e) {
                log.warn("Malformated JSON received: " + message.getData());
            } catch (Exception e) {
                error(new SocketIOException(
                        "Exception was thrown in onMessage(JSON).\n" + "Message was: " + message.toString(), e));
            }

            break;
        case IOMessage.TYPE_EVENT:

            try {

                Map<String, Object> map = objectMapper.readValue(message.getData(),
                        new TypeReference<Map<String, Object>>() {
                        });

                Object[] argsArray;
                if (map.containsKey("args")) {

                    Object argsString = map.get("args");
                    List<Object> argObjects = objectMapper.readValue(argsString.toString(),
                            new TypeReference<List<Object>>() {
                            });

                    argsArray = new Object[argObjects.size()];
                    for (int i = 0; i < argObjects.size(); i++) {
                        if (argObjects.get(i) != null) {
                            argsArray[i] = argObjects.get(i);
                        }
                    }
                } else {
                    argsArray = new Object[0];
                }
                String eventName = map.get("name").toString();
                try {
                    findCallback(message).on(eventName, remoteAcknowledge(message), argsArray);
                } catch (Exception e) {
                    error(new SocketIOException(
                            "Exception was thrown in on(String, JSON[]).\n" + "Message was: " + message.toString(),
                            e));
                }
            } catch (JsonParseException e) {
                log.warn("Malformated JSON received: " + message.getData());
            } catch (JsonMappingException e) {
                log.warn("Mapping JSON received: " + message.getData());
            } catch (IOException e) {
                log.warn("IO Exception: " + message.getData());
            }
            break;

        case IOMessage.TYPE_ACK:

            String[] data = message.getData().split("\\+", 2);
            if (data.length == 2) {
                try {
                    int id = Integer.parseInt(data[0]);
                    IOAcknowledge ack = acknowledge.get(id);
                    if (ack == null) {
                        log.warn("Received unknown ack packet");
                    } else {

                        List<Object> argObjects = objectMapper.readValue(data[1].toString(),
                                new TypeReference<List<Object>>() {
                                });
                        Object[] argsArray = new Object[argObjects.size()];
                        for (int i = 0; i < argObjects.size(); i++) {
                            if (argObjects.get(i) != null) {
                                argsArray[i] = argObjects.get(i);
                            }
                        }
                        ack.ack(argsArray);

                    }
                } catch (NumberFormatException e) {
                    log.warn("Received malformated Acknowledge! This is potentially filling up the acknowledges!");
                } catch (JsonParseException e) {
                    log.warn("Malformated JSON received: " + message.getData());
                } catch (JsonMappingException e) {
                    log.warn("Mapping JSON received: " + message.getData());
                } catch (IOException e) {
                    log.warn("IO Exception: " + message.getData());
                }
            } else if (data.length == 1) {
                sendPlain("6:::" + data[0]);
            }
            break;
        case IOMessage.TYPE_ERROR:
            try {
                findCallback(message).onError(new SocketIOException(message.getData()));
            } catch (SocketIOException e) {
                error(e);
            }
            if (message.getData().endsWith("+0")) {
                // We are advised to disconnect
                cleanup();
            }
            break;
        case IOMessage.TYPE_NOOP:
            break;
        default:
            log.warn("Unkown type received" + message.getType());
            break;
        }
    }

    /**
     * Forces a reconnect. This had become useful on some android devices which do not shut down TCP-connections when switching from HSDPA to Wifi
     */
    public void reconnect() {

        log.info("reconnect called!");

        synchronized (this) {
            if (getState() != STATE_INVALID) {
                invalidateTransport();
                setState(STATE_INTERRUPTED);
                if (reconnectTask != null) {
                    reconnectTask.cancel();
                }
                reconnectTask = new ReconnectTask();
                try {
                    backgroundTimer.schedule(reconnectTask, 1000);
                } catch (IllegalStateException e) {
                    // Probably a race condition
                    setState(STATE_INVALID);
                }
            }
        }
    }

    /**
     * Returns the session id. This should be called from a {@link IOTransport}
     * 
     * @return the session id to connect to the right Session.
     */
    public String getSessionId() {

        return sessionId;
    }

    /**
     * sends a String message from {@link SocketIO} to the {@link IOTransport}.
     * 
     * @param socket the socket
     * @param ack acknowledge package which can be called from the server
     * @param text the text
     */
    public void send(SocketIO socket, IOAcknowledge ack, String text) {

        IOMessage message = new IOMessage(IOMessage.TYPE_MESSAGE, socket.getNamespace(), text);
        synthesizeAck(message, ack);
        sendPlain(message.toString());
    }

    /**
     * emits an event from {@link SocketIO} to the {@link IOTransport}.
     * 
     * @param socket the socket
     * @param event the event
     * @param ack acknowledge package which can be called from the server
     * @param args the arguments to be send
     */
    public void emit(SocketIO socket, String event, IOAcknowledge ack, Object... args) {

        String jsonString = null;
        try {

            Map<String, Object> map = new HashMap<String, Object>();
            map.put("name", event);
            map.put("args", args);
            jsonString = objectMapper.writeValueAsString(map);

            IOMessage message = new IOMessage(IOMessage.TYPE_EVENT, socket.getNamespace(), jsonString);
            synthesizeAck(message, ack);
            sendPlain(message.toString());

        } catch (Exception e) {
            error(new SocketIOException(
                    "Error while emitting an event. Make sure you only try to send arguments, which can be serialized into JSON."));
        }

    }

    /**
     * Checks if IOConnection is currently connected.
     * 
     * @return true, if is connected
     */
    public boolean isConnected() {

        return getState() == STATE_READY;
    }

    /**
     * Gets the current state of this IOConnection.
     * 
     * @return current state
     */
    private synchronized int getState() {

        return state;
    }

    /**
     * Sets the current state of this IOConnection.
     * 
     * @param state the new state
     */
    private synchronized void setState(int state) {

        this.state = state;
    }

    /**
     * gets the currently used transport.
     * 
     * @return currently used transport
     */
    public IOTransport getTransport() {

        return transport;
    }

    @Override
    public void onDisconnect() {

        SocketIO socket = sockets.get("");
        if (socket != null) {
            socket.getCallback().onDisconnect();
        }
    }

    @Override
    public void onConnect() {

        SocketIO socket = sockets.get("");
        if (socket != null) {
            socket.getCallback().onConnect();
        }
    }

    @Override
    public void onMessage(String data, IOAcknowledge ack) {

        for (SocketIO socket : sockets.values()) {
            socket.getCallback().onMessage(data, ack);
        }
    }

    @Override
    public void onJSONMessage(String jsonString, IOAcknowledge ack) {

        for (SocketIO socket : sockets.values()) {
            socket.getCallback().onJSONMessage(jsonString, ack);
        }
    }

    @Override
    public void on(String event, IOAcknowledge ack, Object... args) {

        for (SocketIO socket : sockets.values()) {
            socket.getCallback().on(event, ack, args);
        }
    }

    @Override
    public void onError(SocketIOException socketIOException) {

        for (SocketIO socket : sockets.values()) {
            socket.getCallback().onError(socketIOException);
        }
    }
}