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.BufferOverflowException;
import java.nio.channels.SocketChannel;
import com.esotericsoftware.kryo.Context;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.SerializationException;
import com.esotericsoftware.kryonet.FrameworkMessage.Ping;
import static com.esotericsoftware.minlog.Log.*;
/**
* 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 (Kryo kryo, int writeBufferSize, int objectBufferSize) {
tcp = new TcpConnection(kryo, 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 (SerializationException ex) {
if (ex.causedBy(BufferOverflowException.class)) {
if (DEBUG) debug("kryonet", "Unable to send TCP with connection: " + this, ex);
} else {
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.");
Context context = Kryo.getContext();
context.put("connection", this);
context.put("connectionID", id);
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 (SerializationException ex) {
if (ex.causedBy(BufferOverflowException.class)) {
if (DEBUG) debug("kryonet", "Unable to send UDP with connection: " + this, ex);
} else {
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 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;
}
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;
}
}