package com.esotericsoftware.kryonet; import java.io.IOException; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import static com.esotericsoftware.minlog.Log.*; /** @author Nathan Sweet <misc@n4te.com> */ class TcpConnection { static private final int IPTOS_LOWDELAY = 0x10; SocketChannel socketChannel; int keepAliveMillis = 8000; final ByteBuffer readBuffer, writeBuffer; boolean bufferPositionFix; int timeoutMillis = 12000; float idleThreshold = 0.1f; final Serialization serialization; private SelectionKey selectionKey; private long lastWriteTime, lastReadTime; private int currentObjectLength; private final Object writeLock = new Object(); public TcpConnection (Serialization serialization, int writeBufferSize, int objectBufferSize) { this.serialization = serialization; writeBuffer = ByteBuffer.allocate(writeBufferSize); readBuffer = ByteBuffer.allocate(objectBufferSize); readBuffer.flip(); } public SelectionKey accept (Selector selector, SocketChannel socketChannel) throws IOException { writeBuffer.clear(); readBuffer.clear(); readBuffer.flip(); currentObjectLength = 0; try { this.socketChannel = socketChannel; socketChannel.configureBlocking(false); Socket socket = socketChannel.socket(); socket.setTcpNoDelay(true); selectionKey = socketChannel.register(selector, SelectionKey.OP_READ); if (DEBUG) { debug("kryonet", "Port " + socketChannel.socket().getLocalPort() + "/TCP connected to: " + socketChannel.socket().getRemoteSocketAddress()); } lastReadTime = lastWriteTime = System.currentTimeMillis(); return selectionKey; } catch (IOException ex) { close(); throw ex; } } public void connect (Selector selector, SocketAddress remoteAddress, int timeout) throws IOException { close(); writeBuffer.clear(); readBuffer.clear(); readBuffer.flip(); currentObjectLength = 0; try { SocketChannel socketChannel = selector.provider().openSocketChannel(); Socket socket = socketChannel.socket(); socket.setTcpNoDelay(true); // socket.setTrafficClass(IPTOS_LOWDELAY); socket.connect(remoteAddress, timeout); // Connect using blocking mode for simplicity. socketChannel.configureBlocking(false); this.socketChannel = socketChannel; selectionKey = socketChannel.register(selector, SelectionKey.OP_READ); selectionKey.attach(this); if (DEBUG) { debug("kryonet", "Port " + socketChannel.socket().getLocalPort() + "/TCP connected to: " + socketChannel.socket().getRemoteSocketAddress()); } lastReadTime = lastWriteTime = System.currentTimeMillis(); } catch (IOException ex) { close(); IOException ioEx = new IOException("Unable to connect to: " + remoteAddress); ioEx.initCause(ex); throw ioEx; } } public Object readObject (Connection connection) throws IOException { SocketChannel socketChannel = this.socketChannel; if (socketChannel == null) throw new SocketException("Connection is closed."); if (currentObjectLength == 0) { // Read the length of the next object from the socket. int lengthLength = serialization.getLengthLength(); if (readBuffer.remaining() < lengthLength) { readBuffer.compact(); int bytesRead = socketChannel.read(readBuffer); readBuffer.flip(); if (bytesRead == -1) throw new SocketException("Connection is closed."); lastReadTime = System.currentTimeMillis(); if (readBuffer.remaining() < lengthLength) return null; } currentObjectLength = serialization.readLength(readBuffer); if (currentObjectLength <= 0) throw new KryoNetException("Invalid object length: " + currentObjectLength); if (currentObjectLength > readBuffer.capacity()) throw new KryoNetException("Unable to read object larger than read buffer: " + currentObjectLength); } int length = currentObjectLength; if (readBuffer.remaining() < length) { // Fill the tcpInputStream. readBuffer.compact(); int bytesRead = socketChannel.read(readBuffer); readBuffer.flip(); if (bytesRead == -1) throw new SocketException("Connection is closed."); lastReadTime = System.currentTimeMillis(); if (readBuffer.remaining() < length) return null; } currentObjectLength = 0; int startPosition = readBuffer.position(); int oldLimit = readBuffer.limit(); readBuffer.limit(startPosition + length); Object object; try { object = serialization.read(connection, readBuffer); } catch (Exception ex) { throw new KryoNetException("Error during deserialization.", ex); } readBuffer.limit(oldLimit); if (readBuffer.position() - startPosition != length) throw new KryoNetException("Incorrect number of bytes (" + (startPosition + length - readBuffer.position()) + " remaining) used to deserialize object: " + object); return object; } public void writeOperation () throws IOException { synchronized (writeLock) { if (writeToSocket()) { // Write successful, clear OP_WRITE. selectionKey.interestOps(SelectionKey.OP_READ); } lastWriteTime = System.currentTimeMillis(); } } private boolean writeToSocket () throws IOException { SocketChannel socketChannel = this.socketChannel; if (socketChannel == null) throw new SocketException("Connection is closed."); ByteBuffer buffer = writeBuffer; buffer.flip(); while (buffer.hasRemaining()) { if (bufferPositionFix) { buffer.compact(); buffer.flip(); } if (socketChannel.write(buffer) == 0) break; } buffer.compact(); return buffer.position() == 0; } /** This method is thread safe. */ public int send (Connection connection, Object object) throws IOException { SocketChannel socketChannel = this.socketChannel; if (socketChannel == null) throw new SocketException("Connection is closed."); synchronized (writeLock) { // Leave room for length. int start = writeBuffer.position(); int lengthLength = serialization.getLengthLength(); writeBuffer.position(writeBuffer.position() + lengthLength); // Write data. try { serialization.write(connection, writeBuffer, object); } catch (KryoNetException ex) { throw new KryoNetException("Error serializing object of type: " + object.getClass().getName(), ex); } int end = writeBuffer.position(); // Write data length. writeBuffer.position(start); serialization.writeLength(writeBuffer, end - lengthLength - start); writeBuffer.position(end); // Write to socket if no data was queued. if (start == 0 && !writeToSocket()) { // A partial write, set OP_WRITE to be notified when more writing can occur. selectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else { // Full write, wake up selector so idle event will be fired. selectionKey.selector().wakeup(); } if (DEBUG || TRACE) { float percentage = writeBuffer.position() / (float)writeBuffer.capacity(); if (DEBUG && percentage > 0.75f) debug("kryonet", connection + " TCP write buffer is approaching capacity: " + percentage + "%"); else if (TRACE && percentage > 0.25f) trace("kryonet", connection + " TCP write buffer utilization: " + percentage + "%"); } lastWriteTime = System.currentTimeMillis(); return end - start; } } public void close () { try { if (socketChannel != null) { socketChannel.close(); socketChannel = null; if (selectionKey != null) selectionKey.selector().wakeup(); } } catch (IOException ex) { if (DEBUG) debug("kryonet", "Unable to close TCP connection.", ex); } } public boolean needsKeepAlive (long time) { return socketChannel != null && keepAliveMillis > 0 && time - lastWriteTime > keepAliveMillis; } public boolean isTimedOut (long time) { return socketChannel != null && timeoutMillis > 0 && time - lastReadTime > timeoutMillis; } }