package com.limegroup.gnutella;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.ThreadExecutor;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.io.NetworkUtils;
import org.limewire.service.ErrorService;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.MessageFactory;
import com.limegroup.gnutella.messages.Message.Network;
/**
* Sends and receives multicast messages.
* Currently, this only listens for messages from the Multicast group.
* Sending is done on the GUESS port, so that other nodes can reply
* appropriately to the individual request, instead of multicasting
* replies to the whole group.
*
* @see UDPService
* @see MessageRouter
*/
@Singleton
public final class MulticastServiceImpl implements MulticastService, Runnable {
private static final Log LOG =
LogFactory.getLog(MulticastServiceImpl.class);
/**
* LOCKING: Grab the _recieveLock before receiving. grab the _sendLock
* before sending. Moreover, only one thread should be wait()ing on one of
* these locks at a time or results cannot be predicted.
* This is the socket that handles sending and receiving messages over
* Multicast.
* (Currently only used for recieving)
*/
private volatile MulticastSocket _socket;
/**
* Used for synchronized RECEIVE access to the Multicast socket.
* Should only be used by the Multicast thread.
*/
private final Object _receiveLock = new Object();
/**
* The group we're joined to listen to.
*/
private InetAddress _group = null;
/**
* The port of the group we're listening to.
*/
private int _port = -1;
/**
* Constant for the size of Multicast messages to accept -- dependent upon
* IP-layer fragmentation.
*/
private final int BUFFER_SIZE = 1024 * 32;
/**
* Buffer used for reading messages.
*/
private final byte[] HEADER_BUF = new byte[23];
/**
* The thread for listening of incoming messages.
*/
private final Thread MULTICAST_THREAD;
private final Provider<UDPService> udpService;
private final Provider<MessageDispatcher> messageDispatcher;
private final MessageFactory messageFactory;
@Inject
MulticastServiceImpl(Provider<UDPService> udpService,
Provider<MessageDispatcher> messageDispatcher,
MessageFactory messageFactory) {
this.udpService = udpService;
this.messageDispatcher = messageDispatcher;
this.messageFactory = messageFactory;
MULTICAST_THREAD = ThreadExecutor.newManagedThread(this, "MulticastService");
MULTICAST_THREAD.setDaemon(true);
}
/**
* Starts the Multicast service.
*/
@Override
public void start() {
MULTICAST_THREAD.start();
}
/**
* Returns a new MulticastSocket that is bound to the given port. This
* value should be passed to setListeningSocket(MulticastSocket) to commit
* to the new port. If setListeningSocket is NOT called, you should close
* the return socket.
* @return a new MulticastSocket that is bound to the specified port.
* @exception IOException Thrown if the MulticastSocket could not be
* created.
*/
@Override
public MulticastSocket newListeningSocket(int port, InetAddress group) throws IOException {
if(LOG.isDebugEnabled())
LOG.debug("Binding port " + port + ", group address " + group);
try {
MulticastSocket sock = new MulticastSocket(port);
// Bind to a specific interface unless we're doing loopback tests
if(!ConnectionSettings.ALLOW_MULTICAST_LOOPBACK.getValue())
sock.setInterface(chooseInterface());
sock.setTimeToLive(3);
sock.joinGroup(group);
if(LOG.isDebugEnabled())
LOG.debug("Bound to " + sock.getInterface().getHostAddress());
_port = port;
_group = group;
return sock;
} catch(SocketException se) {
LOG.debug("Could not bind port", se);
throw new IOException("socket could not be set on port: "+port);
} catch(SecurityException se) {
LOG.debug("Could not bind port", se);
throw new IOException("security exception on port: "+port);
}
}
/**
* Returns the interface to which the multicast socket should be bound.
*/
static InetAddress chooseInterface() throws SocketException {
// If the user has chosen a specific network interface, bind to it.
if(ConnectionSettings.CUSTOM_NETWORK_INTERFACE.getValue()) {
try {
String addr = ConnectionSettings.CUSTOM_INETADRESS.get();
if(LOG.isDebugEnabled())
LOG.debug("Binding to configured interface " + addr);
return InetAddress.getByName(addr);
} catch(UnknownHostException fallThrough) {
LOG.debug("Failed to bind to configured interface", fallThrough);
}
}
// Try to find a LAN interface to bind to.
Enumeration<NetworkInterface> ifaces =
NetworkInterface.getNetworkInterfaces();
while(ifaces.hasMoreElements()) {
NetworkInterface iface = ifaces.nextElement();
if(iface.supportsMulticast()) {
Enumeration<InetAddress> addrs = iface.getInetAddresses();
while(addrs.hasMoreElements()) {
InetAddress addr = addrs.nextElement();
if(addr.isSiteLocalAddress()) {
LOG.debug("Binding to LAN interface " + addr.getHostAddress());
return addr;
}
}
}
}
// If there are no LAN interfaces, bind to 0.0.0.0. We try to avoid this
// because calling joinGroup() on a socket bound to 0.0.0.0 doesn't join
// the multicast group on all interfaces.
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4082533
LOG.debug("No suitable interfaces found");
try {
return InetAddress.getByAddress(new byte[]{0, 0, 0, 0});
} catch(UnknownHostException onlyIfAddressLengthIsIllegal) {
throw new RuntimeException(onlyIfAddressLengthIsIllegal);
}
}
/**
* Changes the MulticastSocket used for sending/receiving.
* This must be common among all instances of LimeWire on the subnet.
* It is not synched with the typical gnutella port, because that can
* change on a per-servent basis.
* Only MulticastService should mutate this.
* @param multicastSocket the new listening socket, which must be be the
* return value of newListeningSocket(int). A value of null disables
* Multicast sending and receiving.
*/
@Override
public void setListeningSocket(MulticastSocket multicastSocket) {
//a) Close old socket (if non-null) to alert lock holders...
if(_socket != null) {
LOG.debug("Closing socket");
_socket.close();
}
//b) Replace with new sock. Notify the udpThread.
synchronized (_receiveLock) {
// if the input is null, then the service will shut off ;) .
// leave the group if we're shutting off the service.
if(multicastSocket == null && _socket != null && _group != null) {
try {
if(!_socket.isClosed())
_socket.leaveGroup(_group);
} catch(IOException e) {
LOG.debug("Could not leave multicast group", e);
}
}
_socket = multicastSocket;
_receiveLock.notify();
}
}
/**
* Busy loop that accepts incoming messages sent over the
* multicast socket and dispatches them to their appropriate handlers.
*/
@Override
public void run() {
try {
byte[] datagramBytes = new byte[BUFFER_SIZE];
while (true) {
// prepare to receive
DatagramPacket datagram = new DatagramPacket(datagramBytes,
BUFFER_SIZE);
// when you first can, try to recieve a packet....
// *----------------------------
synchronized (_receiveLock) {
while (_socket == null || _socket.isClosed()) {
try {
_receiveLock.wait();
}
catch (InterruptedException ignored) {
continue;
}
}
LOG.debug("Ready to receive");
try {
_socket.receive(datagram);
}
catch(InterruptedIOException e) {
continue;
}
catch(IOException e) {
LOG.debug("Could not receive packet", e);
continue;
}
}
// ----------------------------*
// process packet....
// *----------------------------
if(!NetworkUtils.isValidAddress(datagram.getAddress())) {
LOG.debug("Received packet with invalid address");
continue;
}
if(!NetworkUtils.isValidPort(datagram.getPort())) {
LOG.debug("Received packet with invalid port");
continue;
}
byte[] data = datagram.getData();
try {
// we do things the old way temporarily
InputStream in = new ByteArrayInputStream(data);
Message message = messageFactory.read(in, Network.MULTICAST, HEADER_BUF, datagram.getSocketAddress());
if(message == null) {
LOG.debug("Received a null message");
continue;
}
LOG.debug("Received a multicast message");
messageDispatcher.get().dispatchMulticast(message, (InetSocketAddress)datagram.getSocketAddress());
}
catch (IOException e) {
LOG.debug("Could not parse packet", e);
continue;
}
catch (BadPacketException e) {
LOG.debug("Could not parse packet", e);
continue;
}
// ----------------------------*
}
} catch(Throwable t) {
ErrorService.error(t);
}
}
/**
* Sends the <tt>Message</tt> using UDPService to the multicast
* address/port.
*
* @param msg the <tt>Message</tt> to send
*/
@Override
public synchronized void send(Message msg) {
// only send the msg if we've initialized the port.
if(_port == -1) {
LOG.debug("Socket not ready for writing");
} else {
LOG.debug("Sending a multicast message");
udpService.get().send(msg, _group, _port);
}
}
/**
* Returns whether or not the Multicast socket is listening for incoming
* messsages.
*
* @return <tt>true</tt> if the Multicast socket is listening for incoming
* Multicast messages, <tt>false</tt> otherwise
*/
@Override
public boolean isListening() {
int port = -1;
if(_socket != null)
port = _socket.getLocalPort();
if(port == -1) {
LOG.debug("Not listening");
return false;
}
return true;
}
/**
* Overrides Object.toString to give more informative information
* about the class.
*
* @return the <tt>MulticastSocket</tt> data
*/
@Override
public String toString() {
return "MulticastService\r\nsocket: "+_socket;
}
}