package com.esotericsoftware.kryonet; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.channels.SocketChannel; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryonet.FrameworkMessage.Ping; import static com.esotericsoftware.minlog.Log.*; // BOZO - Layer to handle handshake state. /** Represents a TCP and optionally a UDP connection between a {@link Client} and a {@link Server}. If either underlying connection * is closed or errors, both connections are closed. * @author Nathan Sweet <misc@n4te.com> */ public class Connection { int id = -1; private String name; EndPoint endPoint; TcpConnection tcp; UdpConnection udp; InetSocketAddress udpRemoteAddress; private Listener[] listeners = {}; private Object listenerLock = new Object(); private int lastPingID; private long lastPingSendTime; private int returnTripTime; volatile boolean isConnected; protected Connection () { } void initialize (Serialization serialization, int writeBufferSize, int objectBufferSize) { tcp = new TcpConnection(serialization, writeBufferSize, objectBufferSize); } /** Returns the server assigned ID. Will return -1 if this connection has never been connected or the last assigned ID if this * connection has been disconnected. */ public int getID () { return id; } /** Returns true if this connection is connected to the remote end. Note that a connection can become disconnected at any time. */ public boolean isConnected () { return isConnected; } /** Sends the object over the network using TCP. * @return The number of bytes sent. * @see Kryo#register(Class, com.esotericsoftware.kryo.Serializer) */ public int sendTCP (Object object) { if (object == null) throw new IllegalArgumentException("object cannot be null."); try { int length = tcp.send(this, object); if (length == 0) { if (TRACE) trace("kryonet", this + " TCP had nothing to send."); } else if (DEBUG) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); if (!(object instanceof FrameworkMessage)) { debug("kryonet", this + " sent TCP: " + objectString + " (" + length + ")"); } else if (TRACE) { trace("kryonet", this + " sent TCP: " + objectString + " (" + length + ")"); } } return length; } catch (IOException ex) { if (DEBUG) debug("kryonet", "Unable to send TCP with connection: " + this, ex); close(); return 0; } catch (KryoNetException ex) { if (ERROR) error("kryonet", "Unable to send TCP with connection: " + this, ex); close(); return 0; } } /** Sends the object over the network using UDP. * @return The number of bytes sent. * @see Kryo#register(Class, com.esotericsoftware.kryo.Serializer) * @throws IllegalStateException if this connection was not opened with both TCP and UDP. */ public int sendUDP (Object object) { if (object == null) throw new IllegalArgumentException("object cannot be null."); SocketAddress address = udpRemoteAddress; if (address == null && udp != null) address = udp.connectedAddress; if (address == null && isConnected) throw new IllegalStateException("Connection is not connected via UDP."); try { if (address == null) throw new SocketException("Connection is closed."); int length = udp.send(this, object, address); if (length == 0) { if (TRACE) trace("kryonet", this + " UDP had nothing to send."); } else if (DEBUG) { if (length != -1) { String objectString = object == null ? "null" : object.getClass().getSimpleName(); if (!(object instanceof FrameworkMessage)) { debug("kryonet", this + " sent UDP: " + objectString + " (" + length + ")"); } else if (TRACE) { trace("kryonet", this + " sent UDP: " + objectString + " (" + length + ")"); } } else debug("kryonet", this + " was unable to send, UDP socket buffer full."); } return length; } catch (IOException ex) { if (DEBUG) debug("kryonet", "Unable to send UDP with connection: " + this, ex); close(); return 0; } catch (KryoNetException ex) { if (ERROR) error("kryonet", "Unable to send UDP with connection: " + this, ex); close(); return 0; } } public void close () { boolean wasConnected = isConnected; isConnected = false; tcp.close(); if (udp != null && udp.connectedAddress != null) udp.close(); if (wasConnected) { notifyDisconnected(); if (INFO) info("kryonet", this + " disconnected."); } setConnected(false); } /** Requests the connection to communicate with the remote computer to determine a new value for the * {@link #getReturnTripTime() return trip time}. When the connection receives a {@link FrameworkMessage.Ping} object with * {@link Ping#isReply isReply} set to true, the new return trip time is available. */ public void updateReturnTripTime () { Ping ping = new Ping(); ping.id = lastPingID++; lastPingSendTime = System.currentTimeMillis(); sendTCP(ping); } /** Returns the last calculated TCP return trip time, or -1 if {@link #updateReturnTripTime()} has never been called or the * {@link FrameworkMessage.Ping} response has not yet been received. */ public int getReturnTripTime () { return returnTripTime; } /** An empty object will be sent if the TCP connection has not sent an object within the specified milliseconds. Periodically * sending a keep alive ensures that an abnormal close is detected in a reasonable amount of time (see {@link #setTimeout(int)} * ). Also, some network hardware will close a TCP connection that ceases to transmit for a period of time (typically 1+ * minutes). Set to zero to disable. Defaults to 8000. */ public void setKeepAliveTCP (int keepAliveMillis) { tcp.keepAliveMillis = keepAliveMillis; } /** If the specified amount of time passes without receiving an object over TCP, the connection is considered closed. When a TCP * socket is closed normally, the remote end is notified immediately and this timeout is not needed. However, if a socket is * closed abnormally (eg, power loss), KryoNet uses this timeout to detect the problem. The timeout should be set higher than * the {@link #setKeepAliveTCP(int) TCP keep alive} for the remote end of the connection. The keep alive ensures that the remote * end of the connection will be constantly sending objects, and setting the timeout higher than the keep alive allows for * network latency. Set to zero to disable. Defaults to 12000. */ public void setTimeout (int timeoutMillis) { tcp.timeoutMillis = timeoutMillis; } /** If the listener already exists, it is not added again. */ 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", "Connection listener added: " + listener.getClass().getName()); } 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; if (n == 0) return; 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", "Connection listener removed: " + listener.getClass().getName()); } void notifyConnected () { if (INFO) { SocketChannel socketChannel = tcp.socketChannel; if (socketChannel != null) { Socket socket = tcp.socketChannel.socket(); if (socket != null) { InetSocketAddress remoteSocketAddress = (InetSocketAddress)socket.getRemoteSocketAddress(); if (remoteSocketAddress != null) info("kryonet", this + " connected: " + remoteSocketAddress.getAddress()); } } } Listener[] listeners = this.listeners; for (int i = 0, n = listeners.length; i < n; i++) listeners[i].connected(this); } void notifyDisconnected () { Listener[] listeners = this.listeners; for (int i = 0, n = listeners.length; i < n; i++) listeners[i].disconnected(this); } void notifyIdle () { Listener[] listeners = this.listeners; for (int i = 0, n = listeners.length; i < n; i++) { listeners[i].idle(this); if (!isIdle()) break; } } void notifyReceived (Object object) { if (object instanceof Ping) { Ping ping = (Ping)object; if (ping.isReply) { if (ping.id == lastPingID - 1) { returnTripTime = (int)(System.currentTimeMillis() - lastPingSendTime); if (TRACE) trace("kryonet", this + " return trip time: " + returnTripTime); } } else { ping.isReply = true; sendTCP(ping); } } Listener[] listeners = this.listeners; for (int i = 0, n = listeners.length; i < n; i++) listeners[i].received(this, object); } /** Returns the local {@link Client} or {@link Server} to which this connection belongs. */ public EndPoint getEndPoint () { return endPoint; } /** Returns the IP address and port of the remote end of the TCP connection, or null if this connection is not connected. */ public InetSocketAddress getRemoteAddressTCP () { SocketChannel socketChannel = tcp.socketChannel; if (socketChannel != null) { Socket socket = tcp.socketChannel.socket(); if (socket != null) { return (InetSocketAddress)socket.getRemoteSocketAddress(); } } return null; } /** Returns the IP address and port of the remote end of the UDP connection, or null if this connection is not connected. */ public InetSocketAddress getRemoteAddressUDP () { InetSocketAddress connectedAddress = udp.connectedAddress; if (connectedAddress != null) return connectedAddress; return udpRemoteAddress; } /** Workaround for broken NIO networking on Android 1.6. If true, the underlying NIO buffer is always copied to the beginning of * the buffer before being given to the SocketChannel for sending. The Harmony SocketChannel implementation in Android 1.6 * ignores the buffer position, always copying from the beginning of the buffer. This is fixed in Android 2.0+. */ public void setBufferPositionFix (boolean bufferPositionFix) { tcp.bufferPositionFix = bufferPositionFix; } /** Sets the friendly name of this connection. This is returned by {@link #toString()} and is useful for providing application * specific identifying information in the logging. May be null for the default name of "Connection X", where X is the * connection ID. */ public void setName (String name) { this.name = name; } /** Returns the number of bytes that are waiting to be written to the TCP socket, if any. */ public int getTcpWriteBufferSize () { return tcp.writeBuffer.position(); } /** @see #setIdleThreshold(float) */ public boolean isIdle () { return tcp.writeBuffer.position() / (float)tcp.writeBuffer.capacity() < tcp.idleThreshold; } /** If the percent of the TCP write buffer that is filled is less than the specified threshold, * {@link Listener#idle(Connection)} will be called for each network thread update. Default is 0.1. */ public void setIdleThreshold (float idleThreshold) { tcp.idleThreshold = idleThreshold; } public String toString () { if (name != null) return name; return "Connection " + id; } void setConnected (boolean isConnected) { this.isConnected = isConnected; if (isConnected && name == null) name = "Connection " + id; } }