package com.esotericsoftware.kryonet; 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 com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryonet.FrameworkMessage.DiscoverHost; import com.esotericsoftware.kryonet.FrameworkMessage.RegisterTCP; import com.esotericsoftware.kryonet.FrameworkMessage.RegisterUDP; import static com.esotericsoftware.minlog.Log.*; /** Represents a TCP and optionally a UDP connection to a {@link Server}. * @author Nathan Sweet <misc@n4te.com> */ 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 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; /** Creates a Client with a write buffer size of 8192 and an object buffer size of 2048. */ public Client () { this(8192, 2048); } /** @param writeBufferSize One buffer of this size is allocated. Objects are serialized to the write buffer where the bytes are * queued until they can be written to the TCP socket. * <p> * Normally the socket is writable and the bytes are written immediately. If the socket cannot be written to and * enough serialized objects are queued to overflow the buffer, then the connection will be closed. * <p> * The write buffer should be sized at least as large as the largest object that will be sent, plus some head room to * allow for some serialized objects to be queued in case the buffer is temporarily not writable. The amount of head * room needed is dependent upon the size of objects being sent and how often they are sent. * @param objectBufferSize One (using only TCP) or three (using both TCP and UDP) buffers of this size are allocated. These * buffers are used to hold the bytes for a single object graph until it can be sent over the network or * deserialized. * <p> * The object buffers should be sized at least as large as the largest object that will be sent or received. */ public Client (int writeBufferSize, int objectBufferSize) { this(writeBufferSize, objectBufferSize, new KryoSerialization()); } public Client (int writeBufferSize, int objectBufferSize, Serialization serialization) { super(); endPoint = this; this.serialization = serialization; initialize(serialization, writeBufferSize, objectBufferSize); try { selector = Selector.open(); } catch (IOException ex) { throw new RuntimeException("Error opening selector.", ex); } } public Serialization getSerialization () { return serialization; } public Kryo getKryo () { return ((KryoSerialization)serialization).getKryo(); } /** 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. */ 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(); int select = 0; if (timeout > 0) { select = selector.select(timeout); } else { select = selector.selectNow(); } if (select == 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 { 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; 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; 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 { if (tcp.needsKeepAlive(time)) sendTCP(FrameworkMessage.keepAlive); if (udp != null && udpRegistered && udp.needsKeepAlive(time)) sendUDP(FrameworkMessage.keepAlive); } if (isIdle()) notifyIdle(); } } 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."); } 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, "Client"); updateThread.setDaemon(true); updateThread.start(); } public void stop () { if (shutdown) return; close(); if (TRACE) trace("kryonet", "Client thread stopping."); shutdown = true; selector.wakeup(); } public void close () { super.close(); // Select one last time to complete closing the socket. synchronized (updateLock) { selector.wakeup(); try { selector.selectNow(); } catch (IOException ignored) { } } } public void addListener (Listener listener) { super.addListener(listener); if (TRACE) trace("kryonet", "Client listener added."); } 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; } 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 socket.send(new DatagramPacket(data, data.length, InetAddress.getByAddress(ip), udpPort)); ip[2] = -1; // 255.255.0.0 socket.send(new DatagramPacket(data, data.length, InetAddress.getByAddress(ip), udpPort)); } } 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(); } } }