package org.limewire.rudp; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.channels.IllegalSelectorException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.spi.AbstractSelectableChannel; import java.nio.channels.spi.AbstractSelector; import java.nio.channels.spi.SelectorProvider; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.rudp.messages.RUDPMessage; import org.limewire.rudp.messages.SynMessage; /** * Manages the assignment of connection IDs and the routing of * {@link RUDPMessage RUDPMessages}. */ public class UDPMultiplexor extends AbstractSelector { private static final Log LOG = LogFactory.getLog(UDPMultiplexor.class); /** The 0 slot is for incoming new connections so it is not assigned. */ public static final byte UNASSIGNED_SLOT = 0; /** Keep track of the assigned connections. */ private volatile UDPSocketChannel[] _channels; /** A list of overflowed channels when registering. */ private final List<SelectableChannel> channelsToRemove = new LinkedList<SelectableChannel>(); /** A set of the currently connected keys. */ private Set<SelectionKey> selectedKeys = new HashSet<SelectionKey>(256); /** Keep track of the last assigned connection id so that we can use a circular assignment algorithm. This should cut down on message collisions after the connection is shut down. */ private int _lastConnectionID; /** The RUDPContext that contains the TransportListener. */ private final RUDPContext context; /** * Initialize the UDPMultiplexor. */ UDPMultiplexor(SelectorProvider provider, RUDPContext context) { super(provider); this.context = context; _channels = new UDPSocketChannel[256]; _lastConnectionID = 0; } /** * Determines if we're connected to the given host. */ public boolean isConnectedTo(InetAddress host) { UDPSocketChannel[] array = _channels; if (_lastConnectionID == 0) return false; for (int i = 0; i < array.length; i++) { UDPSocketChannel channel = array[i]; if (channel != null && host.equals(channel.getRemoteSocketAddress().getAddress())) { return true; } } return false; } /** * Route a message to the {@link UDPConnectionProcessor} identified via the message's * connection ID. * Notifies the provided listener (if any) if the channel is ready to produce events. */ public void routeMessage(RUDPMessage msg, InetSocketAddress addr) { UDPSocketChannel[] array = _channels; int connID = msg.getConnectionID() & 0xff; UDPSocketChannel channel = null; // If connID equals 0 and SynMessage then associate with a connection // that appears to want it (connecting and with knowledge of it). if ( connID == 0 && msg instanceof SynMessage ) { LOG.debugf("route sym: {0}", msg); for (int i = 1; i < array.length; i++) { channel = array[i]; if(channel == null) continue; LOG.debugf("non-empty index: {0}, addr: {1}", i, channel.getRemoteSocketAddress()); if ( channel.isConnectionPending() && channel.isForMe(addr, (SynMessage)msg)) { LOG.debugf("found index: {0}, sender id: {1}", i, ((SynMessage)msg).getSenderConnectionID()); channel.getProcessor().handleMessage(msg); break; } } // Note: eventually these messages should find a match // so it is safe to throw away premature ones } else if(array[connID] != null) { // If valid connID then send on to connection channel = array[connID]; if (msg instanceof SynMessage) { LOG.debugf("already assigned syn: {0}", msg); } if (channel.getRemoteSocketAddress().equals(addr) ) channel.getProcessor().handleMessage(msg); } else { LOG.debugf("message for non-existing connection: {0}", msg); } if (channel != null && channel.getProcessor().readyOps() != 0) context.getTransportListener().eventPending(); } @Override protected void implCloseSelector() throws IOException { throw new IllegalStateException("should never be closed."); } /** * Registers a new channel with this Selector. * If we've already stored over the limit of channels, this will store * the channel in a temporary list to be cancelled on the next selection. */ @Override protected synchronized SelectionKey register(AbstractSelectableChannel ch, int ops, Object att) { int connID; if(!(ch instanceof UDPSocketChannel)) throw new IllegalSelectorException(); UDPSocketChannel channel = (UDPSocketChannel)ch; UDPSocketChannel[] copy = new UDPSocketChannel[_channels.length]; for (int i = 0; i < _channels.length; i++) copy[i] = _channels[i]; for (int i = 1; i <= copy.length; i++) { connID = (_lastConnectionID + i) % 256; // We don't assign zero. if (connID == 0) continue; // If the slot is open, take it. if (copy[connID] == null) { _lastConnectionID = connID; copy[connID] = channel; channel.getProcessor().setConnectionId((byte)connID); _channels = copy; return new UDPSelectionKey(this, att, ch, ops); } } // We don't have enough space for this connection. Add it to a temporary // list of bad connections which will be removed during selectNow. LOG.warn("Attempting to add over connection limit"); channelsToRemove.add(ch); return new UDPSelectionKey(this, att, ch, ops); } /** * Returns all {@link SelectionKey SelectionKeys} this Selector is currently in control of. */ @Override public Set<SelectionKey> keys() { UDPSocketChannel[] channels = _channels; Set<SelectionKey> keys = new HashSet<SelectionKey>(); for(int i = 0; i < channels.length; i++) { if(channels[i] != null) keys.add(channels[i].keyFor(this)); } synchronized(this) { for(SelectableChannel channel : channelsToRemove) keys.add(channel.keyFor(this)); } return keys; } @Override public int select() throws IOException { throw new UnsupportedOperationException("blocking select not supported"); } @Override public int select(long timeout) throws IOException { throw new UnsupportedOperationException("blocking select not supported"); } @Override public Set<SelectionKey> selectedKeys() { return selectedKeys; } /** Polls through all available channels and returns those that are ready. */ @Override public int selectNow() throws IOException { UDPSocketChannel[] array = _channels; UDPSocketChannel[] removed = null; selectedKeys.clear(); for (int i = 0; i < array.length; i++) { UDPSocketChannel channel = array[i]; if (channel == null) continue; UDPSelectionKey key = (UDPSelectionKey)channel.keyFor(this); if (key != null) { if (key.isValid() && channel.isOpen()) { int currentOps = channel.getProcessor().readyOps(); int readyOps = currentOps & key.interestOps(); if (readyOps != 0) { key.setReadyOps(readyOps); selectedKeys.add(key); } } else { if (removed == null) removed = new UDPSocketChannel[array.length]; removed[i] = channel; } } } // Go through the removed list & remove them from _connections. // _connections may have changed (since we didn't lock while polling), // so we need to check and ensure the given UDPConnectionProcessor // is the same. synchronized (this) { if (removed != null) { UDPSocketChannel[] copy = new UDPSocketChannel[_channels.length]; for (int i = 0; i < _channels.length; i++) { if (_channels[i] == removed[i]) copy[i] = null; else copy[i] = _channels[i]; } _channels = copy; } if(!channelsToRemove.isEmpty()) { for(SelectableChannel next : channelsToRemove) { UDPSelectionKey key = (UDPSelectionKey)next.keyFor(this); key.cancel(); key.setReadyOps(0); selectedKeys.add(key); } channelsToRemove.clear(); } } return selectedKeys.size(); } @Override public Selector wakeup() { // Does nothing, since this never blocks. return this; } }