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.MulticastSocket;
import java.net.SocketException;
import java.net.InetSocketAddress;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* This class handles 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
*/
public final class MulticastService implements Runnable {
/**
* Constant for the single <tt>MulticastService</tt> instance.
*/
private final static MulticastService INSTANCE = new MulticastService();
/**
* 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 ErrorCallback _err = new ErrorCallbackImpl();
/**
* Instance accessor.
*/
public static MulticastService instance() {
return INSTANCE;
}
/**
* Constructs a new <tt>UDPAcceptor</tt>.
*/
private MulticastService() {
MULTICAST_THREAD = new ManagedThread(this, "MulticastService");
MULTICAST_THREAD.setDaemon(true);
}
/**
* Starts the Multicast service.
*/
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.
*/
MulticastSocket newListeningSocket(int port, InetAddress group) throws IOException {
try {
MulticastSocket sock = new MulticastSocket(port);
sock.setTimeToLive(3);
sock.joinGroup(group);
_port = port;
_group = group;
return sock;
}
catch (SocketException se) {
throw new IOException("socket could not be set on port: "+port);
}
catch (SecurityException se) {
throw new IOException("security exception on port: "+port);
}
}
/**
* 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.
*/
void setListeningSocket(MulticastSocket multicastSocket)
throws IOException {
//a) Close old socket (if non-null) to alert lock holders...
if (_socket != null)
_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 {
_socket.leaveGroup(_group);
} catch(IOException ignored) {
// ideally we would check if the socket is closed,
// which would prevent the exception from happening.
// but that's only available on 1.4 ...
}
}
_socket = multicastSocket;
_receiveLock.notify();
}
}
/**
* Busy loop that accepts incoming messages sent over the
* multicast socket and dispatches them to their appropriate handlers.
*/
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) {
try {
_receiveLock.wait();
}
catch (InterruptedException ignored) {
continue;
}
}
try {
_socket.receive(datagram);
}
catch(InterruptedIOException e) {
continue;
}
catch(IOException e) {
continue;
}
}
// ----------------------------*
// process packet....
// *----------------------------
if(!NetworkUtils.isValidAddress(datagram.getAddress()))
continue;
if(!NetworkUtils.isValidPort(datagram.getPort()))
continue;
byte[] data = datagram.getData();
try {
// we do things the old way temporarily
InputStream in = new ByteArrayInputStream(data);
Message message = Message.read(in, Message.N_MULTICAST, HEADER_BUF);
if(message == null)
continue;
MessageDispatcher.instance().dispatchMulticast(message, (InetSocketAddress)datagram.getSocketAddress());
}
catch (IOException e) {
continue;
}
catch (BadPacketException 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
*/
public synchronized void send(Message msg) {
// only send the msg if we've initialized the port.
if( _port != -1 ) {
UDPService.instance().send(msg, _group, _port, _err);
}
}
/**
* 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
*/
public boolean isListening() {
if(_socket == null) return false;
return (_socket.getLocalPort() != -1);
}
/**
* Overrides Object.toString to give more informative information
* about the class.
*
* @return the <tt>MulticastSocket</tt> data
*/
public String toString() {
return "MulticastService\r\nsocket: "+_socket;
}
private class ErrorCallbackImpl implements ErrorCallback {
public void error(Throwable t) {}
public void error(Throwable t, String msg) {}
}
}