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.debug; import static com.esotericsoftware.minlog.Log.error; import static com.esotericsoftware.minlog.Log.info; import static com.esotericsoftware.minlog.Log.trace; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.security.AccessControlException; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; import com.esotericsoftware.kryo.Kryo; 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; /** * Represents a TCP and optionally a UDP connection to a {@link Server}. * <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") // NOCHKALL public class Client extends Connection implements EndPoint { static { try { // Needed for NIO selectors on Android 2.2. System.setProperty("java.net.preferIPv6Addresses", "false"); } catch (AccessControlException ignored) { } } private final Serialization serialization; private Selector selector; private int emptySelects; private volatile boolean tcpRegistered, udpRegistered; private Object tcpRegistrationLock = new Object(); private Object udpRegistrationLock = new Object(); private volatile boolean shutdown; private final Object updateLock = new Object(); private Thread updateThread; private int connectTimeout; private InetAddress connectHost; private int connectTcpPort; private int connectUdpPort; private boolean isClosed; // ISE: Removed no-arg and 2-args constructors (not needed) // Added by ISE public Client(IExtendedSerialization serialization, StreamProvider streamProvider) { this(0, serialization.getLengthLength(), serialization, streamProvider); } // Changed by ISE: added StreamProvider, changed to IExtendedSerialization public Client(int writeBufferSize, int objectBufferSize, IExtendedSerialization serialization, StreamProvider streamProvider) { super(streamProvider); endPoint = this; this.serialization = serialization; initialize(serialization, writeBufferSize, objectBufferSize); 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 client. * * @see #connect(int, InetAddress, int, int) */ public void connect(int timeout, String host, int tcpPort) throws IOException { connect(timeout, InetAddress.getByName(host), tcpPort, -1); } /** * Opens a TCP and UDP client. * * @see #connect(int, InetAddress, int, int) */ public void connect(int timeout, String host, int tcpPort, int udpPort) throws IOException { connect(timeout, InetAddress.getByName(host), tcpPort, udpPort); } /** * Opens a TCP only client. * * @see #connect(int, InetAddress, int, int) */ public void connect(int timeout, InetAddress host, int tcpPort) throws IOException { connect(timeout, host, tcpPort, -1); } /** * Opens a TCP and UDP client. Blocks until the connection is complete or the timeout is * reached. * <p> * Because the framework must perform some minimal communication before the connection is * considered successful, {@link #update(int)} must be called on a separate thread during the * connection process. * * @throws IllegalStateException * if called from the connection's update thread. * @throws IOException * if the client could not be opened or connecting times out. */ public void connect(int timeout, InetAddress host, int tcpPort, int udpPort) throws IOException { if (host == null) { throw new IllegalArgumentException("host cannot be null."); } if (Thread.currentThread() == getUpdateThread()) { throw new IllegalStateException("Cannot connect on the connection's update thread."); } this.connectTimeout = timeout; this.connectHost = host; this.connectTcpPort = tcpPort; this.connectUdpPort = udpPort; close(); if (INFO) { if (udpPort != -1) { info("Connecting: " + host + ":" + tcpPort + "/" + udpPort); } else { info("Connecting: " + host + ":" + tcpPort); } } id = -1; try { if (udpPort != -1) { udp = new UdpConnection(serialization, tcp.readBuffer.capacity()); } long endTime; synchronized (updateLock) { tcpRegistered = false; selector.wakeup(); endTime = System.currentTimeMillis() + timeout; tcp.connect(selector, new InetSocketAddress(host, tcpPort), 5000); } // Wait for RegisterTCP. synchronized (tcpRegistrationLock) { while (!tcpRegistered && (System.currentTimeMillis() < endTime)) { try { tcpRegistrationLock.wait(100); } catch (InterruptedException ignored) { } } if (!tcpRegistered) { throw new SocketTimeoutException("Connected, but timed out during TCP registration.\n" + "Note: Client#update must be called in a separate thread during connect."); } } if (udpPort != -1) { InetSocketAddress udpAddress = new InetSocketAddress(host, udpPort); synchronized (updateLock) { udpRegistered = false; selector.wakeup(); udp.connect(selector, udpAddress); } // Wait for RegisterUDP reply. synchronized (udpRegistrationLock) { while (!udpRegistered && (System.currentTimeMillis() < endTime)) { RegisterUDP registerUDP = new RegisterUDP(); registerUDP.connectionID = id; udp.send(this, registerUDP, udpAddress); try { udpRegistrationLock.wait(100); } catch (InterruptedException ignored) { } } if (!udpRegistered) { throw new SocketTimeoutException("Connected, but timed out during UDP registration: " + host + ":" + udpPort); } } } } catch (IOException ex) { close(); throw ex; } } /** * Calls {@link #connect(int, InetAddress, int) connect} with the values last passed to connect. * * @throws IllegalStateException * if connect has never been called. */ public void reconnect() throws IOException { reconnect(connectTimeout); } /** * Calls {@link #connect(int, InetAddress, int) connect} with the specified timeout and the * other values last passed to connect. * * @throws IllegalStateException * if connect has never been called. */ public void reconnect(int timeout) throws IOException { if (connectHost == null) { throw new IllegalStateException("This client has never been connected."); } connect(connectTimeout, connectHost, connectTcpPort, connectUdpPort); } /** * Reads or writes any pending data for this client. Multiple threads should not call this * method at the same time. * * @param timeout * Wait for up to the specified milliseconds for data to be ready to process. May be * zero to return immediately if there is no data 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; isClosed = false; Set<SelectionKey> keys = selector.selectedKeys(); synchronized (keys) { for (Iterator<SelectionKey> iter = keys.iterator(); iter.hasNext();) { SelectionKey selectionKey = iter.next(); iter.remove(); try { int ops = selectionKey.readyOps(); if ((ops & SelectionKey.OP_READ) == SelectionKey.OP_READ) { if (selectionKey.attachment() == tcp) { while (true) { Object object = tcp.readObject(this); if (object == null) { break; } if (!tcpRegistered) { if (object instanceof RegisterTCP) { id = ((RegisterTCP) object).connectionID; synchronized (tcpRegistrationLock) { tcpRegistered = true; tcpRegistrationLock.notifyAll(); if (TRACE) { trace("kryonet", this + " received TCP: RegisterTCP"); } if (udp == null) { setConnected(true); } } if (udp == null) { notifyConnected(); } } continue; } if ((udp != null) && !udpRegistered) { if (object instanceof RegisterUDP) { synchronized (udpRegistrationLock) { udpRegistered = true; udpRegistrationLock.notifyAll(); if (TRACE) { trace("kryonet", this + " received UDP: RegisterUDP"); } if (DEBUG) { debug("kryonet", "Port " + udp.datagramChannel.socket().getLocalPort() + "/UDP connected to: " + udp.connectedAddress); } setConnected(true); } notifyConnected(); } continue; } if (!isConnected) { continue; } keepAlive(); if (DEBUG) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); if (!(object instanceof FrameworkMessage)) { debug("kryonet", this + " received TCP: " + objectString); } else if (TRACE) { trace("kryonet", this + " received TCP: " + objectString); } } notifyReceived(object); } } else { if (udp.readFromAddress() == null) { continue; } Object object = udp.readObject(this); if (object == null) { continue; } keepAlive(); if (DEBUG) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); debug("kryonet", this + " received UDP: " + objectString); } notifyReceived(object); } } if ((ops & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) { tcp.writeOperation(); } } catch (CancelledKeyException ignored) { // Connection is closed. } } } } if (isConnected) { long time = System.currentTimeMillis(); if (tcp.isTimedOut(time)) { if (DEBUG) { debug("kryonet", this + " timed out."); } close(); } else { keepAlive(); } if (isIdle()) { notifyIdle(); } } } void keepAlive() { if (!isConnected) { return; } long time = System.currentTimeMillis(); if (tcp.needsKeepAlive(time)) { sendTCP(FrameworkMessage.keepAlive); } if ((udp != null) && udpRegistered && udp.needsKeepAlive(time)) { sendUDP(FrameworkMessage.keepAlive); } } @Override public void run() { if (TRACE) { trace("kryonet", "Client thread started."); } shutdown = false; while (!shutdown) { try { update(250); } catch (IOException ex) { if (TRACE) { if (isConnected) { trace("kryonet", "Unable to update connection: " + this, ex); } else { trace("kryonet", "Unable to update connection.", ex); } } else if (DEBUG) { if (isConnected) { debug("kryonet", this + " update: " + ex.getMessage()); } else { debug("kryonet", "Unable to update connection: " + ex.getMessage()); } } close(); } catch (KryoNetException ex) { if (ERROR) { if (isConnected) { error("kryonet", "Error updating connection: " + this, ex); } else { error("kryonet", "Error updating connection.", ex); } } close(); throw ex; } } if (TRACE) { trace("kryonet", "Client thread stopped."); } } @Override public void start() { // Try to let any previous update thread stop. if (updateThread != null) { shutdown = true; try { updateThread.join(5000); } catch (InterruptedException ignored) { } } updateThread = new Thread(this, "inspectit-Client"); updateThread.setDaemon(true); updateThread.start(); } @Override public void stop() { if (shutdown) { return; } close(); if (TRACE) { trace("kryonet", "Client thread stopping."); } shutdown = true; // Try to let any previous update thread stop. (added by ISE) if (updateThread != null) { try { updateThread.join(5000); } catch (InterruptedException ignored) { } } selector.wakeup(); } @Override public void close() { super.close(); 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. if (!isClosed) { isClosed = true; selector.wakeup(); try { selector.selectNow(); } catch (IOException ignored) { } } } @Override public void addListener(Listener listener) { super.addListener(listener); if (TRACE) { trace("kryonet", "Client listener added."); } } @Override public void removeListener(Listener listener) { super.removeListener(listener); if (TRACE) { trace("kryonet", "Client listener removed."); } } /** * An empty object will be sent if the UDP connection is inactive more than the specified * milliseconds. Network hardware may keep a translation table of inside to outside IP addresses * and a UDP keep alive keeps this table entry from expiring. Set to zero to disable. Defaults * to 19000. */ public void setKeepAliveUDP(int keepAliveMillis) { if (udp == null) { throw new IllegalStateException("Not connected via UDP."); } udp.keepAliveMillis = keepAliveMillis; } @Override public Thread getUpdateThread() { return updateThread; } private void broadcast(int udpPort, DatagramSocket socket) throws IOException { ByteBuffer dataBuffer = ByteBuffer.allocate(64); serialization.write(null, dataBuffer, new DiscoverHost()); dataBuffer.flip(); byte[] data = new byte[dataBuffer.limit()]; dataBuffer.get(data); for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) { for (InetAddress address : Collections.list(iface.getInetAddresses())) { // Java 1.5 doesn't support getting the subnet mask, so try the two most common. byte[] ip = address.getAddress(); ip[3] = -1; // 255.255.255.0 try { socket.send(new DatagramPacket(data, data.length, InetAddress.getByAddress(ip), udpPort)); } catch (Exception ignored) { } ip[2] = -1; // 255.255.0.0 try { socket.send(new DatagramPacket(data, data.length, InetAddress.getByAddress(ip), udpPort)); } catch (Exception ignored) { } } } if (DEBUG) { debug("kryonet", "Broadcasted host discovery on port: " + udpPort); } } /** * Broadcasts a UDP message on the LAN to discover any running servers. The address of the first * server to respond is returned. * * @param udpPort * The UDP port of the server. * @param timeoutMillis * The number of milliseconds to wait for a response. * @return the first server found, or null if no server responded. */ public InetAddress discoverHost(int udpPort, int timeoutMillis) { DatagramSocket socket = null; try { socket = new DatagramSocket(); broadcast(udpPort, socket); socket.setSoTimeout(timeoutMillis); DatagramPacket packet = new DatagramPacket(new byte[0], 0); try { socket.receive(packet); } catch (SocketTimeoutException ex) { if (INFO) { info("kryonet", "Host discovery timed out."); } return null; } if (INFO) { info("kryonet", "Discovered server: " + packet.getAddress()); } return packet.getAddress(); } catch (IOException ex) { if (ERROR) { error("kryonet", "Host discovery failed.", ex); } return null; } finally { if (socket != null) { socket.close(); } } } /** * Broadcasts a UDP message on the LAN to discover any running servers. * * @param udpPort * The UDP port of the server. * @param timeoutMillis * The number of milliseconds to wait for a response. */ public List<InetAddress> discoverHosts(int udpPort, int timeoutMillis) { List<InetAddress> hosts = new ArrayList<InetAddress>(); DatagramSocket socket = null; try { socket = new DatagramSocket(); broadcast(udpPort, socket); socket.setSoTimeout(timeoutMillis); while (true) { DatagramPacket packet = new DatagramPacket(new byte[0], 0); try { socket.receive(packet); } catch (SocketTimeoutException ex) { if (INFO) { info("kryonet", "Host discovery timed out."); } return hosts; } if (INFO) { info("kryonet", "Discovered server: " + packet.getAddress()); } hosts.add(packet.getAddress()); } } catch (IOException ex) { if (ERROR) { error("kryonet", "Host discovery failed.", ex); } return hosts; } finally { if (socket != null) { socket.close(); } } } /** * {@inheritDoc} * <p> * Returns the {@link NoLimitTcpConnection#getWriteBuffersSize()}. */ // Added by ISE @Override public int getTcpWriteBufferSize() { return tcp.getWriteBuffersSize(); } /** * {@inheritDoc} * <p> * Returns true only if {@link NoLimitTcpConnection#getWriteBuffersSize()} return zero. */ // Added by ISE @Override public boolean isIdle() { return getTcpWriteBufferSize() == 0; } }