package org.limewire.rudp;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.io.ByteBufferOutputStream;
import org.limewire.io.NetworkUtils;
import org.limewire.nio.NIODispatcher;
import org.limewire.nio.observer.ReadWriteObserver;
import org.limewire.rudp.messages.RUDPMessageFactory;
import org.limewire.rudp.messages.MessageFormatException;
import org.limewire.rudp.messages.RUDPMessage;
import org.limewire.rudp.messages.impl.DefaultMessageFactory;
import org.limewire.service.ErrorService;
/**
* <p>A default implementation of <code>UDPService</code>.
* This implementation is basic and will not work in the real world.
* You must provide an implementation that implements these methods correctly.
* </p>
* Messages are created using the <code>DefaultMessageFactory</code>
* and handed off to the <code>UDPMultiplexor</code>.
*/
public class DefaultUDPService implements UDPService, ReadWriteObserver {
private static final Log LOG = LogFactory.getLog(DefaultUDPService.class);
/** The MessageFactory this is using. */
private RUDPMessageFactory factory = new DefaultMessageFactory();
/** The DatagramChannel we're reading from & writing to. */
private DatagramChannel channel;
/** The list of messages to be sent, as SendBundles. */
private final List<SendBundle> OUTGOING_MSGS;
/** The buffer that's re-used for reading incoming messages. */
private final ByteBuffer BUFFER;
/** The maximum size of a UDP message we'll accept. */
private final int BUFFER_SIZE = 1024 * 2;
/** The dispatcher this dispatches msgs to. */
private final MessageDispatcher DISPATCHER;
/** Constructs a new DefaultUDPService. */
public DefaultUDPService(MessageDispatcher dispatcher) {
OUTGOING_MSGS = new LinkedList<SendBundle>();
byte[] backing = new byte[BUFFER_SIZE];
BUFFER = ByteBuffer.wrap(backing);
DISPATCHER = dispatcher;
}
/**
* Returns the port that the service is listening on.
* This WILL NOT CORRECTLY FIGURE OUT THE EXTERNAL PORT.
*/
public int getStableListeningPort() {
if(channel != null)
return channel.socket().getLocalPort();
else
return 0;
}
/**
* Returns the address the service is listening on.
* This WILL NOT CORRECTLY FIGURE OUT THE EXTERNAL ADDRESS.
*/
public InetAddress getStableListeningAddress() {
if(channel != null)
return channel.socket().getLocalAddress();
else {
try {
return InetAddress.getLocalHost();
} catch(UnknownHostException bad) {
return null;
}
}
}
public boolean isListening() {
return channel != null;
}
/** Always returns true. */
public boolean isNATTraversalCapable() {
return true;
}
/** Starts the UDPService. */
public void start(int port) throws IOException {
channel = getChannel(port);
NIODispatcher.instance().registerReadWrite(channel, this);
}
/** Shuts down the UDPService. */
public void shutdown() {
if(channel != null) {
try {
channel.close();
} catch(IOException ignored) {}
}
}
/** Returns a new DatagramChannel that is bound to the given port. */
private DatagramChannel getChannel(int port) throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
DatagramSocket s = channel.socket();
s.setReceiveBufferSize(64*1024);
s.setSendBufferSize(64*1024);
s.setReuseAddress(true);
s.bind(new InetSocketAddress(port));
return channel;
}
/** Notification that an IOException occurred while reading/writing. */
public void handleIOException(IOException iox) {
if( !(iox instanceof java.nio.channels.ClosedChannelException ) )
ErrorService.error(iox, "UDP Error.");
else
LOG.trace("Swallowing a UDPService ClosedChannelException", iox);
}
/** Notification that a read can happen. */
public void handleRead() throws IOException {
while (true) {
BUFFER.clear();
SocketAddress from;
try {
from = channel.receive(BUFFER);
} catch (IOException iox) {
break;
}
// no packet.
if (from == null)
break;
if (!(from instanceof InetSocketAddress)) {
ErrorService.error(new IllegalStateException("non inet address"), "from: " + from);
continue;
}
InetSocketAddress addr = (InetSocketAddress) from;
if (!NetworkUtils.isValidAddress(addr.getAddress()))
continue;
if (!NetworkUtils.isValidPort(addr.getPort()))
continue;
// Clone the buffer while creating, so the next message can be read using it.
BUFFER.flip();
ByteBuffer clone = ByteBuffer.allocate(BUFFER.remaining());
clone.put(BUFFER);
clone.flip();
RUDPMessage message = null;
try {
message = factory.createMessage(clone);
} catch(MessageFormatException ignored) {}
if(message != null)
processMessage(message, addr);
}
}
/** Processes a single message. */
protected void processMessage(RUDPMessage message, InetSocketAddress addr) {
DISPATCHER.dispatch(message, addr);
}
/**
* Sends the specified <tt>RUDPMessage</tt> to the specified host.
*
* @param msg the <tt>RUDPMessage</tt> to send
* @param host the host to send the message to
*/
public void send(RUDPMessage msg, SocketAddress host) {
if (msg == null)
throw new IllegalArgumentException("Null Message");
if (host == null)
throw new IllegalArgumentException("Null InetAddress");
if (!NetworkUtils.isValidSocketAddress(host))
throw new IllegalArgumentException("Invalid Port: " + host);
if(channel == null || channel.socket().isClosed())
return; // ignore if not open.
ByteBufferOutputStream baos = new ByteBufferOutputStream();
try {
msg.write(baos);
} catch(IOException impossible) {
ErrorService.error(impossible);
return;
}
ByteBuffer buffer = baos.getBuffer();
buffer.flip();
synchronized(OUTGOING_MSGS) {
OUTGOING_MSGS.add(new SendBundle(buffer, host));
if(channel != null)
NIODispatcher.instance().interestWrite(channel, true);
}
}
/** Notification that a write can happen. */
public boolean handleWrite() throws IOException {
synchronized(OUTGOING_MSGS) {
while(!OUTGOING_MSGS.isEmpty()) {
SendBundle bundle = OUTGOING_MSGS.remove(0);
try {
if(channel.send(bundle.buffer, bundle.addr) == 0) {
// we removed the bundle from the list but couldn't send it,
// so we have to put it back in.
OUTGOING_MSGS.add(0, bundle);
return true; // no room left to send.
}
} catch(IOException ignored) {
LOG.warn("Ignoring exception on socket", ignored);
}
}
// if there's no data left to send, we don't wanna be notified of write events.
NIODispatcher.instance().interestWrite(channel, false);
return false;
}
}
/** Wrapper for outgoing data */
private static class SendBundle {
private final ByteBuffer buffer;
private final SocketAddress addr;
SendBundle(ByteBuffer b, SocketAddress addr) {
buffer = b;
this.addr = addr;
}
}
}