// Commented for the Learning branch
package com.limegroup.bittorrent;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.LinkedList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.io.ChannelWriter;
import com.limegroup.gnutella.io.InterestWriteChannel;
import com.limegroup.gnutella.io.NIODispatcher;
import com.limegroup.bittorrent.statistics.BTMessageStat;
import com.limegroup.bittorrent.statistics.BTMessageStatBytes;
import com.limegroup.bittorrent.statistics.BandwidthStat;
import com.limegroup.bittorrent.messages.BTMessage;
/**
* A BTMessageWriter object holds and sends BitTorrent messages to the remote computer.
* It joins a chain of writers in LimeWire's NIO design.
*
* The BTConnection constructor makes a new BTMessageWriter, and saves it as _writer.
* Then, it gives it messages we want to send the remote computer.
* The BTMessageWriter has a queue that can hold up to 10 messages.
* When NIO calls handleWrite(), it sends one.
*
* BTMessageWriter implements the ChannelWriter interface.
* This means a BTMessageWriter object has a channel it can write to, and the methods setWriteChannel() and getWriteChannel().
* This also means NIO can tell a BTMessageWriter object when it should get data from its source and write to its channel.
* NIO does this by calling the BTMessageWriter object's handleWrite() method.
*/
public class BTMessageWriter implements ChannelWriter {
/** A debugging log we can write lines of text to as the program runs. */
private static final Log LOG = LogFactory.getLog(BTMessageWriter.class);
/**
* 10, the enqueue() method won't allow more than 10 messages in _queue.
*
* TODO: Add a separate limit for Piece messages so we don't buffer too much data.
*/
private static final int MAX_QUEUE_SIZE = 10;
/**
* The channel this BTMessageWriter will write to.
* We'll do this when NIO calls our handleWrite() method.
* _channel is the next object in a chain of writers that leads to the socket actually connected to the remote computer.
*
* _channel is an InterestWriteChannel.
* The interest part means we can tell NIO if we're interested in writing or not.
* NIO will only call our handleWrite() method if we've set our interest to on.
*/
private InterestWriteChannel _channel;
/** The data of a single message, the one we'll send next. */
private ByteBuffer _out = null;
/**
* The list of BitTorrent messages this BTMessageWriter will send to the remote computer.
* _queue is a LinkedList of objects that extend BTMessage, like BTChoke and BTPiece.
*
* Lock on this object before accessing _queue.
*/
private final LinkedList _queue;
/** A reference up to the BTConnection object that made this BTMessageWriter to send the remote computer messages. */
private final BTConnection _connection;
/** True if this object is shut down, and shouldn't perform any more network activity. */
private volatile boolean shutdown;
/**
* Make a new BTMessageWriter that will hold and send BitTorrent messages to the remote computer.
* Only the BTConnection constructor makes a new BTMessageWriter object.
*
* @param connection The BTConnection object that is doing this
*/
public BTMessageWriter(BTConnection connection) {
// Initialize member variables to null and empty
_channel = null;
_queue = new LinkedList();
// Save the given reference
_connection = connection;
}
/**
* Send a BitTorrent message to the remote computer.
*
* The "NIODispatch" thread will call this handleWrite() method when this BTMessageWriter can send message data to the next object in the chain of writers.
* The ChannelWriter interface requirs this method.
*
* Removes a single message from _queue, turns it into data in _out, and then writes that into the channel.
*
* @return True if we filled the channel and still have more data to send.
* False if we wrote everything we had and are empty.
*/
public boolean handleWrite() throws IOException {
// Don't do anything if this BTMessage writer has been shut down
if (shutdown) return false;
// Make a note we're here
if (LOG.isDebugEnabled()) LOG.debug("entering handleWrite call to " + _connection);
// Record the number of bytes we write
int written = 0;
do {
// Prepare _out with the data of a single BitTorrent message from _queue for us to send
if (_out == null || // There's no _out buffer of data to send at all, or
_out.remaining() == 0) { // There is, but it doesn't have the data of a BitTorrent message in it
// Move one message from _queue to data in the _out buffer
if (!sendNextMessage()) { // sendNextMessage() returns false if _queue and _out are empty
// Tell our channel that we're not interested in giving it data at this time
if (LOG.isDebugEnabled()) LOG.debug("no more messages to send to " + _connection);
_channel.interest(this, false);
// Return false, we wrote everything we had and are empty
return false;
}
}
// Send the data of the BitTorrent message into the channel
written = _channel.write(_out);
// Record the number of bytes we sent
if (written > 0) {
count(written);
if (LOG.isDebugEnabled()) LOG.debug("wrote " + written + " bytes");
}
// Do that again until write() returns 0, either because _out is empty or _channel is full
} while (written > 0);
// Return true, we filled the channel and still have more to send
return true;
}
/**
* Send a BitTorrent message to the remote computer.
*
* Adds the given message to the queue of up to 10 of them this BTMessageWriter keeps.
* When NIO calls handleWrite(), we'll send the data of one to the next object in the chain of writers.
* Eventually, it will make it to the remote computer.
*
* @param m The object that extends BTMessage, like BTHave or BTBitField, to send.
* @return True if we added the message to our queue of messages to send.
* False if we didn't because our queue already has 10 messages in it.
*/
public boolean enqueue(BTMessage m) {
// If we already have too many messages, return false
if (_queue.size() > MAX_QUEUE_SIZE) return false; // TODO: _queue could grow to hold 11, the > should be >=
// Add the given message last in the queue
_queue.addLast(m); // We'll send it after the messages that are already there
if (LOG.isDebugEnabled()) LOG.debug("enqueing message of type " + m.getType() + " to " + _connection.toString() + " : " + m.toString());
// Tell the object we send data to that we have some data for it, so it should call our handleWrite() method when it wants some
if (_channel != null) _channel.interest(this, true);
// Return true, we added the message to our queue and it will get sent
return true;
}
/**
* Java threw the "NIODispatch" thread an IOException while it was doing something for us.
* Passes it up to the BTConnection object.
* The ChannelWriter interface requires this method.
*
* @param iox The IOException
*/
public void handleIOException(IOException iox) {
// Pass it up to the BTConnection object that made this BTMessageWriter
_connection.handleIOException(iox);
}
/**
* Make this BTMessageWriter stop all network activity.
* The ChannelWriter interface requires this method.
*
* Has the "NIODispatch" thread remove all the messages from our queue, and tell the object we write to we have no data for it.
*/
public void shutdown() {
// Only do this once, and record it has been done
if (shutdown) return;
shutdown = true;
// Have the "NIODispatch" thread run the code in this run() method
NIODispatcher.instance().invokeLater(new Runnable() {
// The "NIODispatch" thread will call this run() method
public void run() {
// Remove all the messages from our queue
_queue.clear();
// Tell the object we write to we have no data for it
_channel.interest(BTMessageWriter.this, false);
}
});
}
/**
* Give this BTMessageWriter a channel it can write data to.
* This is how it will send data to the remote computer.
*
* @param newChannel An InterestWriteChannel this object can write data to
*/
public void setWriteChannel(InterestWriteChannel newChannel) {
// Save the given channel
_channel = newChannel;
// Tell it we're interested in finding out when we can write to it
_channel.interest(this, true); // It will call our handleWrite() method when it wants data from us
}
/**
* Get the channel this BTMessageWriter writes data to.
* This is how it sends data to the remote computer.
*
* @return The InterestWriteChannel this object sends data to
*/
public InterestWriteChannel getWriteChannel() {
// Return the channel we've been using
return _channel;
}
/**
* Count that we've sent more bytes.
*
* @param written The number of bytes we sent
*/
public void count(int written) {
// Add the number to bandwidth statistics, and give it to the BTConnection object
BandwidthStat.BITTORRENT_MESSAGE_UPSTREAM_BANDWIDTH.addData(written);
_connection.wroteBytes(written);
}
/**
* Determine if this BTMessageWriter object has messages in its queue waiting to go out.
*
* @return True if the queue is empty, this object has nothing to send right now.
* False if there are messages in the queue, and this object will send them.
*/
public boolean isIdle() {
// Return true if the queue is empty
return _queue.isEmpty();
}
/**
* Remove one message from _queue, and turn it into data in the ByteBuffer _out.
* Only handleWrite() calls this method.
*
* @return True if _out contains a message for handleWrite() to write.
* False if _out is empty because there were no messages in _queue.
*/
private boolean sendNextMessage() {
// If our queue of messages to send is empty, return false, we have nothing to write
if (_queue.isEmpty()) return false;
// Get the first message from the queue
BTMessage message = (BTMessage)_queue.removeFirst();
// Make a note that we're going to send this message
if (LOG.isDebugEnabled()) LOG.debug("sending message " + message + " to " + _connection);
// Convert the message from an object into data
ByteBuffer payload = message.getPayload(); // Get the payload of the message, the part beyond "LLLLT" the length and type
_out = ByteBuffer.allocate(payload.remaining() + 5); // Make _out a ByteBuffer that can hold the 5 bytes of "LLLLT" and then the payload
_out.order(ByteOrder.BIG_ENDIAN); // Have the _out ByteBuffer write the ints we give it in big endian order
_out.putInt(payload.remaining() + 1); // Write the "LLLL" part, add 1 for the "T" type byte
_out.put(message.getType()); // Write the "T" type byte
_out.put(payload); // Write the payload, this fills the buffer and moves position all the way to limit at the end
_out.flip(); // Move position back to the start so the buffer is ready for reading
// Count this message in statistics
countMessage(message, _out.remaining());
// If there are no more messages in our queue, tell our connection we need more (do)
if (_queue.isEmpty()) _connection.readyForWriting();
// Return true, the _out buffer has a message to send to the remote computer
return true;
}
/**
* Count that we sent a message in statistics.
*
* @param message The object that represents the message
* @param length The size of the message
*/
private void countMessage(BTMessage message, int length) {
// Sort by message type
switch (message.getType()) {
case BTMessage.CHOKE:
BTMessageStat.OUTGOING_CHOKE.incrementStat(); // Count that we've sent another Choke message
BTMessageStatBytes.OUTGOING_CHOKE.addData(length); // Count that we've sent length more bytes of Choke message data
break;
case BTMessage.UNCHOKE:
BTMessageStat.OUTGOING_UNCHOKE.incrementStat();
BTMessageStatBytes.OUTGOING_UNCHOKE.addData(length);
break;
case BTMessage.INTERESTED:
BTMessageStat.OUTGOING_INTERESTED.incrementStat();
BTMessageStatBytes.OUTGOING_INTERESTED.addData(length);
break;
case BTMessage.NOT_INTERESTED:
BTMessageStat.OUTGOING_NOT_INTERESTED.incrementStat();
BTMessageStatBytes.OUTGOING_NOT_INTERESTED.addData(length);
break;
case BTMessage.HAVE:
BTMessageStat.OUTGOING_HAVE.incrementStat();
BTMessageStatBytes.OUTGOING_HAVE.addData(length);
break;
case BTMessage.BITFIELD:
BTMessageStat.OUTGOING_BITFIELD.incrementStat();
BTMessageStatBytes.OUTGOING_BITFIELD.addData(length);
break;
case BTMessage.CANCEL:
BTMessageStat.OUTGOING_CANCEL.incrementStat();
BTMessageStatBytes.OUTGOING_CANCEL.addData(length);
break;
case BTMessage.PIECE:
BTMessageStat.OUTGOING_PIECE.incrementStat();
BTMessageStatBytes.OUTGOING_PIECE.addData(length);
break;
case BTMessage.REQUEST:
BTMessageStat.OUTGOING_REQUEST.incrementStat();
BTMessageStatBytes.OUTGOING_REQUEST.addData(length);
}
}
}