Package com.esotericsoftware.kryonet

Source Code of com.esotericsoftware.kryonet.Client

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 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;
  private ClientDiscoveryHandler discoveryHandler;

  /** 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;

    this.discoveryHandler = ClientDiscoveryHandler.DEFAULT;

    initialize(serialization, writeBufferSize, objectBufferSize);

    try {
      selector = Selector.open();
    } catch (IOException ex) {
      throw new RuntimeException("Error opening selector.", ex);
    }
  }

  public void setDiscoveryHandler (ClientDiscoveryHandler newDiscoveryHandler) {
    discoveryHandler = newDiscoveryHandler;
  }

  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, 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, 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(timeout, 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) {
      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();) {
          keepAlive();
          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
        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);
  }

  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) {
      if (!isClosed) {
        isClosed = true;
        selector.wakeup();
        try {
          selector.selectNow();
        } catch (IOException ignored) {
        }
      }
    }
  }

  /** Releases the resources used by this client, which may no longer be used. */
  public void dispose () throws IOException {
    close();
    selector.close();
  }

  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
        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 = discoveryHandler.onRequestNewDatagramPacket();
      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());
      discoveryHandler.onDiscoveredHost(packet, getKryo());
      return packet.getAddress();
    } catch (IOException ex) {
      if (ERROR) error("kryonet", "Host discovery failed.", ex);
      return null;
    } finally {
      if (socket != null) socket.close();
      discoveryHandler.onFinally();
    }
  }

  /** 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 = discoveryHandler.onRequestNewDatagramPacket();
        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());
        discoveryHandler.onDiscoveredHost(packet, getKryo());
        hosts.add(packet.getAddress());
      }
    } catch (IOException ex) {
      if (ERROR) error("kryonet", "Host discovery failed.", ex);
      return hosts;
    } finally {
      if (socket != null) socket.close();
      discoveryHandler.onFinally();
    }
  }
}
TOP

Related Classes of com.esotericsoftware.kryonet.Client

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.