package org.limewire.rudp;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.spi.SelectorProvider;
import java.util.ArrayList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.listener.EventBroadcaster;
import org.limewire.nio.AbstractNBSocket;
import org.limewire.nio.NIODispatcher;
import org.limewire.nio.channel.InterestReadableByteChannel;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.nio.observer.WriteObserver;
import org.limewire.rudp.messages.DataMessage;
import org.limewire.rudp.messages.SynMessage;
import org.limewire.rudp.messages.SynMessage.Role;
import org.limewire.util.BufferUtils;
/**
* Interface between reading channels & UDP's data.
* Analogous to SocketChannel combined w/ a SocketInterestReadAdapter & SocketInterestWriteAdapter.
* <p>
* This class _is_ the SocketChannel for UDP, except because we wrote it,
* we can make it implement InterestReadChannel & InterestWriteChannel, so
* we don't need the additional InterestAdapter.
*/
public class UDPSocketChannel extends AbstractNBSocketChannel implements InterestReadableByteChannel,
InterestWritableByteChannel,
ChunkReleaser {
private static final Log LOG = LogFactory.getLog(UDPSocketChannel.class);
/** The processor this channel is writing to / reading from. */
private final UDPConnectionProcessor processor;
/** The <tt>RUDPContext</tt> containing the TransportListener to notify for pending events. */
private final RUDPContext context;
/** The Socket object this UDPSocketChannel is used for. */
private final AbstractNBSocket socket;
/** The DataWindow containing incoming read data. */
private final DataWindow readData;
/** The WriteObserver that last requested interest from us. */
private volatile WriteObserver writer;
/** The list of buffered chunks that need to be written out. */
private final ArrayList<ByteBuffer> chunks;
/** The current chunk we're writing to. */
private ByteBuffer activeChunk;
/** Whether or not we've handled one write yet. */
private boolean writeHandled = false;
/** A lock to hold while manipulating chunks or activeChunk. */
private final Object writeLock = new Object();
/** Whether or not we've propagated the shutdown to other writers. */
private boolean shutdown = false;
private final Role role;
protected UDPSocketChannel(SelectorProvider provider,
RUDPContext context,
Role role,
EventBroadcaster<UDPSocketChannelConnectionEvent> connectionStateEventBroadcaster) {
super(provider);
this.context = context;
this.role = role;
this.processor = new UDPConnectionProcessor(this, context, role, connectionStateEventBroadcaster);
this.readData = processor.getReadWindow();
this.chunks = new ArrayList<ByteBuffer>(5);
this.socket = new UDPConnection(context, this);
allocateNewChunk();
try {
configureBlocking(false);
} catch(IOException iox) {
throw new RuntimeException(iox);
}
}
// for testing.
UDPSocketChannel(UDPConnectionProcessor processor, Role role) {
super(null);
this.role = role;
this.context = new DefaultRUDPContext();
this.processor = processor;
this.readData = processor.getReadWindow();
this.chunks = new ArrayList<ByteBuffer>(5);
this.socket = new UDPConnection(context, this);
allocateNewChunk();
try {
configureBlocking(false);
} catch(IOException iox) {
throw new RuntimeException(iox);
}
}
public boolean isForMe(InetSocketAddress address, SynMessage message) {
if(!getRemoteSocketAddress().equals(address)) {
return false;
}
if (!role.canConnectTo(message.getRole())) {
return false;
}
byte theirConnectionId = processor.getTheirConnectionID();
if (theirConnectionId != UDPMultiplexor.UNASSIGNED_SLOT
&& theirConnectionId != message.getSenderConnectionID()) {
return false;
}
return true;
}
UDPConnectionProcessor getProcessor() {
return processor;
}
/**
* Sets read interest on or off in the processor.
*/
public void interestRead(boolean status) {
NIODispatcher.instance().interestRead(this, status);
}
/// ********** reading ***************
/**
* Reads all possible data from the DataWindow into the ByteBuffer,
* sending a keep alive if more space became available.
*/
long last = 0;
@Override
public int read(ByteBuffer to) throws IOException {
// It is possible that the channel is open but the processor
// is closed. In that case, this will return -1.
// Once this closes, it throws CCE.
if(!isOpen())
throw new ClosedChannelException();
synchronized (processor) {
// Now that we've transferred all we can to the buffer, clear up
// the space & send a keep-alive if necessary
// Record how much space was previously available in the receive window
int priorSpace = readData.getWindowSpace();
int read = 0;
DataRecord currentRecord = readData.getReadableBlock();
while (currentRecord != null) {
read += transfer(currentRecord, to);
if (!to.hasRemaining())
break;
// If to still has room left, we must have written
// all we could from the record, so we assign a new one.
// Fetch a block from the receiving window.
currentRecord = readData.getReadableBlock();
}
// Remove all records we just read from the receiving window
int cleared = readData.clearEarlyReadBlocks();
// If the receive window opened up then send a special
// KeepAliveMessage so that the window state can be
// communicated.
if ((cleared > 0 && priorSpace == 0)
/*|| (priorSpace <= UDPConnectionProcessor.SMALL_SEND_WINDOW &&
readData.getWindowSpace() > UDPConnectionProcessor.SMALL_SEND_WINDOW)*/) {
if (LOG.isDebugEnabled())
LOG.debug("Sending aritifial keep alive: cleared=" + cleared + ", priorSpace="+ priorSpace + ", read=" + read + ", windowSpace=" + readData.getWindowSpace() + ", windowStart=" + readData.getWindowStart());
processor.sendKeepAlive();
}
if(read == 0 && processor.isClosed())
return -1;
else
return read;
}
}
/**
* Transfers the chunks in the DataRecord's msg to the ByteBuffer. Sets the record as being successfully read after
* all data is read from it.
*/
private int transfer(DataRecord record, ByteBuffer to) {
DataMessage msg = record.msg;
int read = 0;
ByteBuffer chunk = msg.getData1Chunk();
if(chunk.hasRemaining())
read += BufferUtils.transfer(chunk, to, false);
if(chunk.hasRemaining())
return read;
chunk = msg.getData2Chunk();
read += BufferUtils.transfer(chunk, to, false);
if(!chunk.hasRemaining())
record.read = true;
return read;
}
/// ********** writing ***************
/**
* Writes all data in src into a list of internal chunks.
* This will notify the processor if we have no pending chunks prior
* to writing, so that it will know to retrieve some data.
* Chunks will be created until the processor tells us we're at the limit,
* at which point this will forcibly will return the amount of data that
* could be written so far.
* If all data is emptied from src, this will return that amount of data.
*/
@Override
public int write(ByteBuffer src) throws IOException {
// We cannot write if either the channel or the processor is closed.
if(!isOpen() || processor.isClosed())
throw new ClosedChannelException();
synchronized(writeLock) {
// If there was no data before this, then ensure a writer is awake
if ( getNumberOfPendingChunks() == 0 ) {
processor.wakeupWriteEvent(!writeHandled);
}
writeHandled = true;
int wrote = 0;
while (true) {
if(src.hasRemaining()) {
if (activeChunk.hasRemaining()) {
wrote += BufferUtils.transfer(src, activeChunk, false);
} else if (chunks.size() < processor.getChunkLimit()) {
// If there is room for more chunks, allocate a new chunk
chunks.add(activeChunk);
allocateNewChunk();
} else {
return wrote;
}
} else {
return wrote;
}
}
}
}
/**
* Allocates a chunk for writing to.
*/
private void allocateNewChunk() {
activeChunk = NIODispatcher.instance().getBufferCache().getHeap(UDPConnectionProcessor.DATA_CHUNK_SIZE);
}
/**
* Releases a chunk.
*/
public void releaseChunk(ByteBuffer chunk) {
NIODispatcher.instance().getBufferCache().release(chunk);
}
/**
* Gets the first chunk of data that should be written to the wire.
* Returns null if no data.
*/
ByteBuffer getNextChunk() {
synchronized(writeLock) {
ByteBuffer rChunk;
if ( chunks.size() > 0 ) {
// Return the oldest chunk
rChunk = chunks.remove(0);
rChunk.flip();
} else if (activeChunk.position() > 0) {
rChunk = activeChunk;
rChunk.flip();
allocateNewChunk();
} else {
// If no data currently, return null
rChunk = null;
}
return rChunk;
}
}
/**
* Return how many pending chunks are waiting on being written to the wire.
*/
int getNumberOfPendingChunks() {
synchronized(writeLock) {
// Add the number of list blocks
int count = chunks.size();
// Add one for the current block if data available.
if (activeChunk.position() > 0)
count++;
return count;
}
}
Object writeLock() {
return writeLock;
}
/// ********** SelectableChannel methods. ***************
/** Closes the processor. */
@Override
protected void implCloseSelectableChannel() throws IOException {
processor.close();
}
/// ********** InterestWriteChannel methods. ***************
/**
* Shuts down this channel & processor, notifying the last interested party
* that we're now shutdown.
*/
public void shutdown() {
synchronized(this) {
if(shutdown)
return;
shutdown = true;
}
Shutdownable chain = writer;
if(chain != null)
chain.shutdown();
writer = null;
}
/** Sets interest on or off on the channel & stores the interested party for future writing. */
public void interestWrite(WriteObserver observer, boolean status) {
if(isOpen()) {
writer = observer;
NIODispatcher.instance().interestWrite(this, status);
}
}
/** Sends a write up the chain to the last interest party. */
public boolean handleWrite() throws IOException {
WriteObserver chain = writer;
if(chain != null)
return chain.handleWrite();
else
return false;
}
/** Unused. */
public void handleIOException(IOException iox) {
throw new UnsupportedOperationException();
}
public InetSocketAddress getRemoteSocketAddress() {
return processor.getSocketAddress();
}
@Override
public boolean connect(SocketAddress remote) throws IOException {
processor.connect((InetSocketAddress)remote);
return false;
}
@Override
public boolean finishConnect() throws IOException {
return processor.prepareOpenConnection();
}
@Override
public boolean isConnected() {
return processor.isConnected();
}
@Override
public boolean isConnectionPending() {
return processor.isConnecting();
}
@Override
public AbstractNBSocket socket() {
return socket;
}
@Override
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
throw new IOException("unsupported");
}
@Override
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
throw new IOException("unsupported");
}
@Override
protected void implConfigureBlocking(boolean block) throws IOException {
// does nothing.
}
void eventPending() {
context.getTransportListener().eventPending();
}
public boolean hasBufferedOutput() {
return getNumberOfPendingChunks() > 0;
}
@Override
public String toString() {
InetSocketAddress addr = getRemoteSocketAddress();
if(addr == null) {
return "[disconnected]";
} else {
return addr.toString();
}
}
}