/*
* Mojito Distributed Hash Table (Mojito DHT)
* Copyright (C) 2006-2007 LimeWire LLC
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.limewire.mojito.io;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.mojito.Context;
import org.limewire.mojito.messages.DHTMessage;
import org.limewire.mojito.messages.MessageFormatException;
import org.limewire.mojito.settings.NetworkSettings;
import org.limewire.mojito.util.CryptoUtils;
import org.limewire.security.SecureMessage;
import org.limewire.security.SecureMessageCallback;
import org.limewire.security.Verifier;
/**
* This is a stand alone/reference implementation of <code>MessageDispatcher</code>.
*/
public class MessageDispatcherImpl extends MessageDispatcher implements Runnable {
private static final Log LOG = LogFactory.getLog(MessageDispatcherImpl.class);
/**
* The maximum time to wait on 'lock'
*/
private static final long WAIT_ON_LOCK = 5000L;
/**
* The receive buffer size for the Socket.
*/
private static final int RECEIVE_BUFFER_SIZE
= NetworkSettings.RECEIVE_BUFFER_SIZE.getValue();
/**
* The send buffer size for the Socket.
*/
private static final int SEND_BUFFER_SIZE
= NetworkSettings.SEND_BUFFER_SIZE.getValue();
/**
* Sleep timeout of the Selector
*/
private static final long SELECTOR_SLEEP = 50L;
/**
* A flag whether or not this MessageDispatcher is running.
*/
private volatile boolean running = false;
/**
* A flag whether or not this MessageDispatcher is accepting incoming
* Requests and Responses.
*/
private volatile boolean accepting = false;
/**
* The DatagramChannel's Selector.
*/
private Selector selector;
/**
* The DatagramChanel
*/
private DatagramChannel channel;
/**
* The DatagramChannel lock Object.
*/
private final Object lock = new Object();
/**
* The Thread this MessageDispatcher is running on.
*/
private Thread thread;
/**
* Buffer for incoming Messages.
*/
private final ByteBuffer receiveBuffer;
/**
* Lists of tasks we've to execute.
*/
private List<Runnable> tasks = new ArrayList<Runnable>();
/**
* Queue of things we have to send.
*/
private List<Tag> outputQueue = new LinkedList<Tag>();
/**
* Whether or not a new ByteBuffer should be allocated for
* every message we receive.
*/
private volatile boolean allocateNewByteBuffer
= NetworkSettings.ALLOCATE_NEW_BUFFER.getValue();
public MessageDispatcherImpl(Context context) {
super(context);
receiveBuffer = ByteBuffer.allocate(RECEIVE_BUFFER_SIZE);
}
/**
* Sets whether or not a new ByteBuffer should be allocated.
*/
public void setAllocateNewByteBuffer(boolean allocateNewByteBuffer) {
this.allocateNewByteBuffer = allocateNewByteBuffer;
}
/**
* Returns whether or not a new ByteBuffer is allocated for
* every message.
*/
public boolean getAllocateNewByteBuffer() {
return allocateNewByteBuffer;
}
@Override
public void bind(SocketAddress address) throws IOException {
synchronized (lock) {
if (isBound()) {
throw new IOException("DatagramChannel is already bound");
}
channel = DatagramChannel.open();
channel.configureBlocking(false);
selector = Selector.open();
channel.register(selector, SelectionKey.OP_READ);
DatagramSocket socket = channel.socket();
socket.setReuseAddress(false);
socket.setReceiveBufferSize(RECEIVE_BUFFER_SIZE);
socket.setSendBufferSize(SEND_BUFFER_SIZE);
socket.bind(address);
}
}
/**
* Returns true if the DatagramChannel is open.
*/
public boolean isOpen() {
synchronized (lock) {
return channel != null && channel.isOpen();
}
}
@Override
public boolean isBound() {
synchronized (lock) {
return channel != null && channel.socket().isBound();
}
}
/**
* Returns the DatagramChannel.
*/
public DatagramChannel getDatagramChannel() {
synchronized (lock) {
return channel;
}
}
/**
* Returns the DatagramChannel Socket's local SocketAddress.
*/
public SocketAddress getLocalSocketAddress() {
synchronized (lock) {
if (channel != null && channel.isOpen()) {
return channel.socket().getLocalSocketAddress();
}
return null;
}
}
@Override
public void start() {
synchronized (lock) {
if (!isBound()) {
throw new IllegalStateException("MessageDispatcher is not bound");
}
if (!running) {
accepting = true;
running = true;
thread = context.getDHTExecutorService().getThreadFactory().newThread(this);
thread.setName(context.getName() + "-MessageDispatcherThread");
thread.setDaemon(Boolean.getBoolean("com.limegroup.mojito.io.MessageDispatcherIsDaemon"));
thread.start();
Runnable startup = new Runnable() {
public void run() {
synchronized(lock) {
try {
MessageDispatcherImpl.super.start();
} finally {
lock.notifyAll();
}
}
}
};
process(startup);
try {
lock.wait(WAIT_ON_LOCK);
} catch (InterruptedException err) {
LOG.error("InterruptedException", err);
}
}
}
}
@Override
protected boolean submit(final Tag tag) {
Runnable task = new Runnable() {
public void run() {
outputQueue.add(tag);
interestWrite(true);
}
};
process(task);
return true;
}
/**
* Writes all Messages (if possible) from the output
* queue to the Network and returns whether or not some
* Messages were left in the output queue.
*/
private void handleWrite() throws IOException {
Tag tag = null;
while (!outputQueue.isEmpty()) {
tag = outputQueue.get(0);
if (tag.isCancelled()) {
outputQueue.remove(0);
continue;
}
try {
SocketAddress dst = tag.getSocketAddress();
ByteBuffer data = tag.getData();
if (send(dst, data)) {
// Wohoo! Message was sent!
outputQueue.remove(0);
register(tag);
} else {
// Dang! Re-Try next time!
break;
}
} catch (IOException err) {
LOG.error("IOException", err);
outputQueue.remove(0);
handleError(tag, err);
}
}
interestWrite(!outputQueue.isEmpty());
}
@Override
public void stop() {
synchronized (lock) {
// Do not accept any new incoming Requests or Responses
accepting = false;
if (isRunning()) {
Runnable shutdown = new Runnable() {
public void run() {
synchronized (lock) {
try {
running = false;
MessageDispatcherImpl.super.stop();
} finally {
lock.notifyAll();
}
}
}
};
process(shutdown);
try {
lock.wait(WAIT_ON_LOCK);
} catch (InterruptedException err) {
LOG.error("InterruptedException", err);
}
if (thread != null) {
thread.interrupt();
thread = null;
}
tasks.clear();
outputQueue.clear();
}
}
}
@Override
public void close() {
super.close();
synchronized (lock) {
assert !isRunning(); // the call above should stop this.
if (selector != null) {
try {
selector.close();
selector = null;
} catch (IOException err) {
LOG.error("IOException", err);
}
}
if (channel != null) {
try {
channel.close();
channel = null;
} catch (IOException err) {
LOG.error("IOException", err);
}
}
}
}
@Override
public boolean isAccepting() {
return accepting;
}
@Override
public boolean isRunning() {
return running;
}
/**
* Reads all available Message from Network and processes them.
*/
private void handleRead() throws IOException {
while(isRunning()) {
DHTMessage message = null;
try {
message = readMessage();
} catch (MessageFormatException err) {
LOG.error("Message Format Exception: ", err);
continue;
}
if (message == null) {
break;
}
handleMessage(message);
}
// We're always interested in reading!
interestRead(true);
}
/**
* Reads and returns a single DHTMessage from Network or null
* if no Messages were in the input queue.
*/
private DHTMessage readMessage() throws MessageFormatException, IOException {
SocketAddress src = receive((ByteBuffer)receiveBuffer.clear());
if (src != null) {
receiveBuffer.flip();
ByteBuffer data = null;
if (getAllocateNewByteBuffer()) {
int length = receiveBuffer.remaining();
data = ByteBuffer.allocate(length);
data.put(receiveBuffer);
data.rewind();
} else {
data = receiveBuffer.slice();
}
DHTMessage message = deserialize(src, data/*.asReadOnlyBuffer()*/);
return message;
}
return null;
}
@Override
protected void process(Runnable runnable) {
synchronized (lock) {
if (isRunning()) {
tasks.add(runnable);
selector.wakeup();
}
}
}
@Override
protected void verify(SecureMessage secureMessage, SecureMessageCallback smc) {
// Verifying the signature is an expensive Task and should
// be done on a different Thread than MessageDispatcher's
// Executor Thread. On the other hand are the chances slim to none
// that a Node will ever receive a SecureMessage. It's a trade off
// at the end if it's really an issue or waste of resources...
// NOTE: LimeDHTMessageDispatcher is using a different implementation!
// This is the stand alone implementation!
final PublicKey pubKey = context.getPublicKey();
if (pubKey == null) {
if (LOG.isInfoEnabled()) {
LOG.info("Dropping SecureMessage "
+ secureMessage + " because PublicKey is not set");
}
return;
}
Verifier verifier = new Verifier(secureMessage, smc) {
@Override
public String getAlgorithm() {
return CryptoUtils.SIGNATURE_ALGORITHM;
}
@Override
public PublicKey getPublicKey() {
return pubKey;
}
};
verify(verifier);
}
/**
* Called by verify(SecureMessage, SecureMessageCallback) to execute
* the Runnable that does the actual verification. You may override
* this method to execute the Runnable on a different Thread.
*/
protected void verify(Runnable verifier) {
// See verify(SecureMessage, SecureMessageCallback)
process(verifier);
}
private void interest(int ops, boolean on) {
try {
SelectionKey sk = channel.keyFor(selector);
if (sk != null && sk.isValid()) {
synchronized(channel.blockingLock()) {
if (on) {
sk.interestOps(sk.interestOps() | ops);
} else {
sk.interestOps(sk.interestOps() & ~ops);
}
}
}
} catch (CancelledKeyException ignore) {}
}
/**
* Called to indicate an interest in reading something from
* the Network. Override this method if you need this functionality!
*/
private void interestRead(boolean on) {
interest(SelectionKey.OP_READ, on);
}
/**
* Called to indicate an interest in writing something to
* the Network. Override this method if you need this functionality!
*/
private void interestWrite(boolean on) {
interest(SelectionKey.OP_WRITE, on);
}
/**
* The raw read-method.
*/
private SocketAddress receive(ByteBuffer dst) throws IOException {
return channel.receive(dst);
}
/**
* The actual send method. Returns true if the data was
* sent or false if there was insufficient space in the
* output buffer (that means you'll have to re-try it later
* again).
* <p>
* IMPORTANT: The expected behavior is the same as
* DatagramChannel.send(BytBuffer,SocketAddress). That means
* if you are not able to send the data return false and
* leave the ByteBuffer untouched!
*/
// We could pass a slice to this method to enforce the expected
// behavior but there's maybe an use-case like Kademlia over TCP
// where it makes sense to send the data piece-by-piece...
private boolean send(SocketAddress dst, ByteBuffer data) throws IOException {
return channel.send(data, dst) > 0;
}
private void processAll() {
List<Runnable> process = null;
synchronized (lock) {
process = tasks;
tasks = new ArrayList<Runnable>();
}
for (Runnable task : process) {
task.run();
}
}
public void run() {
try {
while (true) {
processAll();
if (!isRunning() || !isOpen()) {
break;
}
selector.select(SELECTOR_SLEEP);
try {
// READ
handleRead();
} catch (IOException err) {
LOG.error("IOException-READ", err);
}
try {
// WRITE
handleWrite();
} catch (IOException err) {
LOG.error("IOException-WRITE", err);
}
}
} catch (IOException err) {
// Pass it to the UncaughtExceptionHandler
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), err);
}
}
}