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.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;
import com.esotericsoftware.kryonet.FrameworkMessage.Ping;
import com.esotericsoftware.kryonet.KryoNetException;
import rocks.inspectit.shared.all.storage.nio.stream.StreamProvider;
// 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.
* <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 Connection {
/**
* {@link StreamProvider} needed for the {@link NoLimitTcpConnection}.
*/
private StreamProvider streamProvider; // Added by ISE
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;
// Changed by ISE: Added StreamProvider
protected Connection(StreamProvider streamProvider) {
this.streamProvider = streamProvider;
}
// Changed by ISE: Changed to IExtendedSerialization, added streamProvider
void initialize(IExtendedSerialization serialization, int writeBufferSize, int objectBufferSize) {
tcp = new TcpConnection(serialization, writeBufferSize, objectBufferSize, streamProvider);
}
/**
* 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 (Listener listener : listeners) {
listener.connected(this);
}
}
void notifyDisconnected() {
Listener[] listeners = this.listeners;
for (Listener listener : listeners) {
listener.disconnected(this);
}
}
void notifyIdle() {
Listener[] listeners = this.listeners;
for (Listener listener : listeners) {
listener.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 (Listener listener : listeners) {
listener.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;
}
@Override
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;
}
}
}