package rocks.inspectit.shared.all.kryonet; import static com.esotericsoftware.minlog.Log.DEBUG; import static com.esotericsoftware.minlog.Log.ERROR; import static com.esotericsoftware.minlog.Log.INFO; import static com.esotericsoftware.minlog.Log.TRACE; import static com.esotericsoftware.minlog.Log.WARN; import static com.esotericsoftware.minlog.Log.debug; import static com.esotericsoftware.minlog.Log.error; import static com.esotericsoftware.minlog.Log.info; import static com.esotericsoftware.minlog.Log.trace; import static com.esotericsoftware.minlog.Log.warn; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.util.IntMap; import com.esotericsoftware.kryonet.FrameworkMessage; import com.esotericsoftware.kryonet.FrameworkMessage.DiscoverHost; import com.esotericsoftware.kryonet.FrameworkMessage.RegisterTCP; import com.esotericsoftware.kryonet.FrameworkMessage.RegisterUDP; import com.esotericsoftware.kryonet.KryoNetException; import rocks.inspectit.shared.all.storage.nio.stream.StreamProvider; /** * Manages TCP and optionally UDP connections from many {@link Client Clients}. * <p> * <b>IMPORTANT:</b> The class code is copied/taken/based from * <a href="https://github.com/EsotericSoftware/kryonet">kryonet</a>. Original author is Nathan * Sweet. License info can be found * <a href="https://github.com/EsotericSoftware/kryonet/blob/master/license.txt">here</a>. * * @author Nathan Sweet <misc@n4te.com> */ @SuppressWarnings({ "all", "unchecked" }) // NOCHKALL public class Server implements EndPoint { /** * {@link StreamProvider} needed for the Extended Connection. */ private StreamProvider streamProvider; // Added by ISE private final IExtendedSerialization serialization; private final int writeBufferSize, objectBufferSize; private final Selector selector; private int emptySelects; private ServerSocketChannel serverChannel; private UdpConnection udp; private Connection[] connections = {}; private IntMap<Connection> pendingConnections = new IntMap(); Listener[] listeners = {}; private Object listenerLock = new Object(); private int nextConnectionID = 1; private volatile boolean shutdown; private Object updateLock = new Object(); private Thread updateThread; private ByteBuffer emptyBuffer = ByteBuffer.allocate(0); private Listener dispatchListener = new Listener() { @Override public void connected(Connection connection) { Listener[] listeners = Server.this.listeners; for (Listener listener : listeners) { listener.connected(connection); } } @Override public void disconnected(Connection connection) { removeConnection(connection); Listener[] listeners = Server.this.listeners; for (Listener listener : listeners) { listener.disconnected(connection); } } @Override public void received(Connection connection, Object object) { Listener[] listeners = Server.this.listeners; for (Listener listener : listeners) { listener.received(connection, object); } } @Override public void idle(Connection connection) { Listener[] listeners = Server.this.listeners; for (Listener listener : listeners) { listener.idle(connection); } } }; // ISE: Removed no-arg and 2-args constructors (not needed) // Added by ISE public Server(IExtendedSerialization serialization, StreamProvider streamProvider) { this(0, serialization.getLengthLength(), serialization, streamProvider); } // Changed by ISE: added StreamProvider, changed to IExtendedSerialization public Server(int writeBufferSize, int objectBufferSize, IExtendedSerialization serialization, StreamProvider streamProvider) { this.writeBufferSize = writeBufferSize; this.objectBufferSize = objectBufferSize; this.streamProvider = streamProvider; // Added by ISE. this.serialization = serialization; try { selector = Selector.open(); } catch (IOException ex) { throw new RuntimeException("Error opening selector.", ex); } } @Override public Serialization getSerialization() { return serialization; } @Override public Kryo getKryo() { throw new UnsupportedOperationException("Can not provide Kryo instance."); } /** * Opens a TCP only server. * * @throws IOException * if the server could not be opened. */ public void bind(int tcpPort) throws IOException { bind(new InetSocketAddress(tcpPort), null); } /** * Opens a TCP and UDP server. * * @throws IOException * if the server could not be opened. */ public void bind(int tcpPort, int udpPort) throws IOException { bind(new InetSocketAddress(tcpPort), new InetSocketAddress(udpPort)); } /** * @param udpPort * May be null. */ public void bind(InetSocketAddress tcpPort, InetSocketAddress udpPort) throws IOException { close(); synchronized (updateLock) { selector.wakeup(); try { serverChannel = selector.provider().openServerSocketChannel(); serverChannel.socket().bind(tcpPort); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); if (DEBUG) { debug("kryonet", "Accepting connections on port: " + tcpPort + "/TCP"); } if (udpPort != null) { udp = new UdpConnection(serialization, objectBufferSize); udp.bind(selector, udpPort); if (DEBUG) { debug("kryonet", "Accepting connections on port: " + udpPort + "/UDP"); } } } catch (IOException ex) { close(); throw ex; } } if (INFO) { info("kryonet", "Server opened."); } } /** * Accepts any new connections and reads or writes any pending data for the current connections. * * @param timeout * Wait for up to the specified milliseconds for a connection to be ready to process. * May be zero to return immediately if there are no connections to process. */ @Override public void update(int timeout) throws IOException { updateThread = Thread.currentThread(); synchronized (updateLock) { // Blocks to avoid a select while the selector is used to bind // the server connection. } long startTime = System.currentTimeMillis(); /* Changed by ISE start */ // select without timeout int select = selector.selectNow(); long waitUntil = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeout); while ((select == 0) && (System.nanoTime() < waitUntil)) { // if no operation is there sleep 1/10 of a millisecond // until we reach the timeout LockSupport.parkNanos(100000); select = selector.selectNow(); } /* Changed by ISE end */ if (select == 0) { emptySelects++; if (emptySelects == 100) { emptySelects = 0; // NIO freaks and returns immediately with 0 sometimes, so try to keep from hogging // the CPU. long elapsedTime = System.currentTimeMillis() - startTime; try { if (elapsedTime < 25) { Thread.sleep(25 - elapsedTime); } } catch (InterruptedException ex) { } } } else { emptySelects = 0; Set<SelectionKey> keys = selector.selectedKeys(); synchronized (keys) { UdpConnection udp = this.udp; outer: for (Iterator<SelectionKey> iter = keys.iterator(); iter.hasNext();) { SelectionKey selectionKey = iter.next(); iter.remove(); Connection fromConnection = (Connection) selectionKey.attachment(); try { int ops = selectionKey.readyOps(); if (fromConnection != null) { // Must be a TCP read or write operation. if ((udp != null) && (fromConnection.udpRemoteAddress == null)) { fromConnection.close(); continue; } if ((ops & SelectionKey.OP_READ) == SelectionKey.OP_READ) { try { while (true) { Object object = fromConnection.tcp.readObject(fromConnection); if (object == null) { break; } if (DEBUG) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); if (!(object instanceof FrameworkMessage)) { debug("kryonet", fromConnection + " received TCP: " + objectString); } else if (TRACE) { trace("kryonet", fromConnection + " received TCP: " + objectString); } } fromConnection.notifyReceived(object); } } catch (IOException ex) { if (TRACE) { trace("kryonet", "Unable to read TCP from: " + fromConnection, ex); } else if (DEBUG) { debug("kryonet", fromConnection + " update: " + ex.getMessage()); } fromConnection.close(); } catch (KryoNetException ex) { if (ERROR) { error("kryonet", "Error reading TCP from connection: " + fromConnection, ex); } fromConnection.close(); } } if ((ops & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { try { fromConnection.tcp.writeOperation(); } catch (IOException ex) { if (TRACE) { trace("kryonet", "Unable to write TCP to connection: " + fromConnection, ex); } else if (DEBUG) { debug("kryonet", fromConnection + " update: " + ex.getMessage()); } fromConnection.close(); } } continue; } if ((ops & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { ServerSocketChannel serverChannel = this.serverChannel; if (serverChannel == null) { continue; } try { SocketChannel socketChannel = serverChannel.accept(); if (socketChannel != null) { acceptOperation(socketChannel); } } catch (IOException ex) { if (DEBUG) { debug("kryonet", "Unable to accept new connection.", ex); } } continue; } // Must be a UDP read operation. if (udp == null) { selectionKey.channel().close(); continue; } InetSocketAddress fromAddress; try { fromAddress = udp.readFromAddress(); } catch (IOException ex) { if (WARN) { warn("kryonet", "Error reading UDP data.", ex); } continue; } if (fromAddress == null) { continue; } Connection[] connections = this.connections; for (Connection connection : connections) { if (fromAddress.equals(connection.udpRemoteAddress)) { fromConnection = connection; break; } } Object object; try { object = udp.readObject(fromConnection); } catch (KryoNetException ex) { if (WARN) { if (fromConnection != null) { if (ERROR) { error("kryonet", "Error reading UDP from connection: " + fromConnection, ex); } } else { warn("kryonet", "Error reading UDP from unregistered address: " + fromAddress, ex); } } continue; } if (object instanceof FrameworkMessage) { if (object instanceof RegisterUDP) { // Store the fromAddress on the connection and reply over TCP with a // RegisterUDP to indicate success. int fromConnectionID = ((RegisterUDP) object).connectionID; Connection connection = pendingConnections.remove(fromConnectionID); if (connection != null) { if (connection.udpRemoteAddress != null) { continue outer; } connection.udpRemoteAddress = fromAddress; addConnection(connection); connection.sendTCP(new RegisterUDP()); if (DEBUG) { debug("kryonet", "Port " + udp.datagramChannel.socket().getLocalPort() + "/UDP connected to: " + fromAddress); } connection.notifyConnected(); continue; } if (DEBUG) { debug("kryonet", "Ignoring incoming RegisterUDP with invalid connection ID: " + fromConnectionID); } continue; } if (object instanceof DiscoverHost) { try { udp.datagramChannel.send(emptyBuffer, fromAddress); if (DEBUG) { debug("kryonet", "Responded to host discovery from: " + fromAddress); } } catch (IOException ex) { if (WARN) { warn("kryonet", "Error replying to host discovery from: " + fromAddress, ex); } } continue; } } if (fromConnection != null) { if (DEBUG) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); if (object instanceof FrameworkMessage) { if (TRACE) { trace("kryonet", fromConnection + " received UDP: " + objectString); } } else { debug("kryonet", fromConnection + " received UDP: " + objectString); } } fromConnection.notifyReceived(object); continue; } if (DEBUG) { debug("kryonet", "Ignoring UDP from unregistered address: " + fromAddress); } } catch (CancelledKeyException ex) { if (fromConnection != null) { fromConnection.close(); } else { selectionKey.channel().close(); } } } } } long time = System.currentTimeMillis(); Connection[] connections = this.connections; for (Connection connection : connections) { if (connection.tcp.isTimedOut(time)) { if (DEBUG) { debug("kryonet", connection + " timed out."); } connection.close(); } else { if (connection.tcp.needsKeepAlive(time)) { connection.sendTCP(FrameworkMessage.keepAlive); } } if (connection.isIdle()) { connection.notifyIdle(); } } } @Override public void run() { if (TRACE) { trace("kryonet", "Server thread started."); } shutdown = false; while (!shutdown) { try { update(250); } catch (IOException ex) { if (ERROR) { error("kryonet", "Error updating server connections.", ex); } close(); } } if (TRACE) { trace("kryonet", "Server thread stopped."); } } @Override public void start() { new Thread(this, "Server").start(); } @Override public void stop() { if (shutdown) { return; } close(); if (TRACE) { trace("kryonet", "Server thread stopping."); } shutdown = true; } private void acceptOperation(SocketChannel socketChannel) { Connection connection = newConnection(); connection.initialize(serialization, writeBufferSize, objectBufferSize); connection.endPoint = this; UdpConnection udp = this.udp; if (udp != null) { connection.udp = udp; } try { SelectionKey selectionKey = connection.tcp.accept(selector, socketChannel); selectionKey.attach(connection); int id = nextConnectionID++; if (nextConnectionID == -1) { nextConnectionID = 1; } connection.id = id; connection.setConnected(true); connection.addListener(dispatchListener); if (udp == null) { addConnection(connection); } else { pendingConnections.put(id, connection); } RegisterTCP registerConnection = new RegisterTCP(); registerConnection.connectionID = id; connection.sendTCP(registerConnection); if (udp == null) { connection.notifyConnected(); } } catch (IOException ex) { connection.close(); if (DEBUG) { debug("kryonet", "Unable to accept TCP connection.", ex); } } } /** * Allows the connections used by the server to be subclassed. This can be useful for storage * per connection without an additional lookup. */ protected Connection newConnection() { // Change by ISE return new Connection(streamProvider); } private void addConnection(Connection connection) { Connection[] newConnections = new Connection[connections.length + 1]; newConnections[0] = connection; System.arraycopy(connections, 0, newConnections, 1, connections.length); connections = newConnections; } void removeConnection(Connection connection) { ArrayList<Connection> temp = new ArrayList(Arrays.asList(connections)); temp.remove(connection); connections = temp.toArray(new Connection[temp.size()]); pendingConnections.remove(connection.id); } // BOZO - Provide mechanism for sending to multiple clients without serializing multiple times. public void sendToAllTCP(Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { connection.sendTCP(object); } } public void sendToAllExceptTCP(int connectionID, Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { if (connection.id != connectionID) { connection.sendTCP(object); } } } public void sendToTCP(int connectionID, Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { if (connection.id == connectionID) { connection.sendTCP(object); break; } } } public void sendToAllUDP(Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { connection.sendUDP(object); } } public void sendToAllExceptUDP(int connectionID, Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { if (connection.id != connectionID) { connection.sendUDP(object); } } } public void sendToUDP(int connectionID, Object object) { Connection[] connections = this.connections; for (Connection connection : connections) { if (connection.id == connectionID) { connection.sendUDP(object); break; } } } @Override public void addListener(Listener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null."); } synchronized (listenerLock) { Listener[] listeners = this.listeners; int n = listeners.length; for (int i = 0; i < n; i++) { if (listener == listeners[i]) { return; } } Listener[] newListeners = new Listener[n + 1]; newListeners[0] = listener; System.arraycopy(listeners, 0, newListeners, 1, n); this.listeners = newListeners; } if (TRACE) { trace("kryonet", "Server listener added: " + listener.getClass().getName()); } } @Override public void removeListener(Listener listener) { if (listener == null) { throw new IllegalArgumentException("listener cannot be null."); } synchronized (listenerLock) { Listener[] listeners = this.listeners; int n = listeners.length; Listener[] newListeners = new Listener[n - 1]; for (int i = 0, ii = 0; i < n; i++) { Listener copyListener = listeners[i]; if (listener == copyListener) { continue; } if (ii == (n - 1)) { return; } newListeners[ii++] = copyListener; } this.listeners = newListeners; } if (TRACE) { trace("kryonet", "Server listener removed: " + listener.getClass().getName()); } } /** Closes all open connections and the server port(s). */ @Override public void close() { Connection[] connections = this.connections; if (INFO && (connections.length > 0)) { info("kryonet", "Closing server connections..."); } for (Connection connection : connections) { connection.close(); } connections = new Connection[0]; ServerSocketChannel serverChannel = this.serverChannel; if (serverChannel != null) { try { serverChannel.close(); if (INFO) { info("kryonet", "Server closed."); } } catch (IOException ex) { if (DEBUG) { debug("kryonet", "Unable to close server.", ex); } } this.serverChannel = null; } UdpConnection udp = this.udp; if (udp != null) { udp.close(); this.udp = null; } synchronized (updateLock) { // Blocks to avoid a select while the selector is used to bind // the server connection. } // Select one last time to complete closing the socket. selector.wakeup(); try { selector.selectNow(); } catch (IOException ignored) { } } @Override public Thread getUpdateThread() { return updateThread; } /** Returns the current connections. The array returned should not be modified. */ public Connection[] getConnections() { return connections; } }