package edu.brown.net; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.ByteChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SocketChannel; /** Provides a non-blocking buffer for both sending and receiving to/from a SocketChannel. */ public class NonBlockingConnection { private final SelectableChannel channel; private final NIOReadStream read; private final NIOWriteStream write; private boolean writeBlocked = false; /** Creates a NonBlockingConnection wrapping channel. If the channel is not yet connected, * writeAvailable() must be called when it is connected. * * @param channel SocketChannel used for I/O. Must be open. */ public NonBlockingConnection(SocketChannel channel) { this(channel, channel); try { // NOTE: On Mac OS X, when an async connect to a localhost socket fails, this // can fail with an illegal argument exception, before finishConnect() is called. channel.socket().setTcpNoDelay(true); channel.configureBlocking(false); } catch (IOException e) { throw new RuntimeException(e); } if (!channel.isConnected()) { if (!channel.isOpen()) { // not possible? configureBlocking will throw ClosedChannelException throw new IllegalArgumentException("channel is closed"); } // The channel ether has a pending connect, or the connect hasn't started. writeBlocked = true; } else { // We haven't started connecting yet, to avoid the setTcpNoDelay issue assert channel.isConnectionPending() || channel.isOpen(); } } /** Constructor provided mostly for unit tests. */ public NonBlockingConnection(SelectableChannel selectable, ByteChannel channel) { this.channel = selectable; read = new NIOReadStream(channel); write = new NIOWriteStream(channel); } /** Returns an OutputStream that buffers output in this connection. */ public OutputStream getOutputStream() { return new ZeroCopyOutputStreamAdaptor(write); } /** Attempt to write the data out to the buffer, if we can. This will either immediately * perform the write, or it will buffer it. * @return true if this connection blocked and now needs a write callback. */ public boolean tryFlush() { if (!writeBlocked) { writeBlocked = write.flush(); return writeBlocked; } return false; } /** Called when the channel can accept more data so we should try writing. * * @return true if this connection is still blocked. */ public boolean writeAvailable() { assert writeBlocked; writeBlocked = write.flush(); return writeBlocked; } /** See {@link NIOReadStream#readAllAvailable()}. * @see NIOReadStream#readAllAvailable() */ public boolean readAllAvailable() { return read.readAllAvailable(); } /** Read up to desired bytes of data into the internal buffer. * @return the number of bytes available, or -1 if the connection is closed. */ public int readAvailable(int desired) { int available = read.tryRead(desired); return available; } public int available() { return read.dataAvailable(); } public class NonBlockingConnectionInputStream extends InputStream { private int limitRemaining = -1; public void setLimit(int limit) { limitRemaining = limit; } @Override public int read() throws IOException { throw new UnsupportedOperationException("TODO: implement"); } @Override public int read(byte[] destination, int offset, int length) { if (limitRemaining == 0) { return -1; } if (limitRemaining >= 0) { if (limitRemaining < length) { length = limitRemaining; } limitRemaining -= length; assert limitRemaining >= 0; } read.getBytes(destination, offset, length); return length; } } /** Returns an OutputStream that buffers output in this connection. */ public NonBlockingConnectionInputStream getInputStream() { return new NonBlockingConnectionInputStream(); } /** Returns the underlying channel for registration with a selector. */ public SelectableChannel getChannel() { return channel; } public void close() { read.close(); } }