// Commented for the Learning branch
package com.limegroup.bittorrent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.io.DelayedBufferWriter;
import com.limegroup.gnutella.io.NIODispatcher;
import com.limegroup.gnutella.io.NIOSocket;
import com.limegroup.gnutella.io.ThrottleWriter;
import com.limegroup.bittorrent.statistics.BTMessageStat;
import com.limegroup.bittorrent.messages.*;
import com.limegroup.gnutella.util.IOUtils;
import com.limegroup.gnutella.util.BitSet;
/**
* A BTConnection object represents our connection to a remote computer running BitTorrent software and sharing a torrent we have.
*
* Our ManagedTorrent object, _torrent, keeps a list of BTConnection objects.
* There is one for each remote computer it's sharing this torrent with.
*
* Only BTHandshaker.tryToFinishHandshakes() makes a new BTConnection object.
* It's already done the handshake with a remote computer.
* The code in this class deals with sending and receiving BitTorrent messages.
*
* === Choking and Interest ===
*
* The BTConnection object has 4 member booleans that keep track of our state with this remote computer.
* _isChoked is true when we're choking this remote computer.
* _isInteresting is true when we're interested in this remote computer's data.
* _isChoking is true when this remote computer is choking us.
* _isInterested is true when this remote computer is interested in our data.
* Access these values with the methods isChoked(), isInteresting(), isChoking(), and isInterested().
*
* _isChoked and _isInteresting are about us.
* They're true when we've choked the remote computer, and when we're interested in its data.
* _isChoking and _isInterested are are about the remote computer.
* They're true when it has choked us, and when it is interested in our data.
*
* Connections start out choked and not interested.
* _isChoked and _isChoking are true, and _isInterested and _isInteresting are false.
* The BTConnection constructor sets these defaults.
*
* If this remote computer isn't sending us data fast enough, we'll choke it as punishment.
* We'll send it a Choke message, and set _isChoked to true.
* We won't send it any data, even if it requests some.
* When we're done choking it, we'll send it an Unchoke message and set _isChoked to false.
* To choke and unchoke this computer, call sendChoke() and sendUnchoke().
* Those methods set _isChoked to true and false, and send Choke and Unchoke messages.
*
* If we're not sending this remote computer data fast enough, it will choke us as punishment.
* We'll receive a Choke message from it, and set _isChoking to true.
* We won't ask it for any data.
* When it's done choking us, it will send us an Unchoke message, and we'll set _isChoking to false.
* When we receive a Choke message, handleMessage() sets _isChoking to true.
* It sets it back to false when this remote computer sends us an Unchoke message.
*
* If this remote computer has data that we need, it's interesting to us.
* We'll set _isInteresting to true.
* To tell this remote computer we're interested in its data or we're not, call sendInterested and sendNotInterested().
* Those methods set _isInteresting to true and false, and set Interested and Not Interested messages.
*
* If we have data this remote computer needs, it's interested in us.
* We'll set _isInterested to true.
* When we receive an Interested message, handleMessage() sets _isInterested to true.
* When this remote computer sends us a Not Interested message, handleMessage() sets _isInterested to false.
*
* === Request, Cancel, and Piece messages ===
*
* This BTConnection object keeps 3 lists:
* _toRequest
* _requesting
* _requested
*
* All 3 are HashSet objects that hold BTInterval objects.
* A HashSet is a list that has no duplicates.
* Java calls BTIntervalHashSet.hashCode() to make the HashSet fast.
* The BTConnection constructor makes 3 empty HashSet objects, and saves them.
*
* _toRequest and _requesting are used to ask this remote computer for data.
* _requested is used to keep track of what this remote computer has asked us.
*
* _toRequest is the data ranges we're going to ask this remote computer.
* When we send a Request message, we move the range it asks for from _toRequest to the _requesting list.
* When this remote computer sends us a Piece message that satisifies the request, we remove it from _requesting.
*
* sendRequest() adds a BTInterval to our _toRequest list.
* enqueueRequests() moves BTIntervals to _requesting, and sends Request messages.
* handlePiece() removes a BTInterval from _requesting.
*
* _requested is the data ranges this remote computer has asked us to send.
* When it gives us a Request message, we add the range to the list.
* If it cancels that request with a Cancel message, we remove the range from the list.
* When we satisfy the request with a Piece message, we remove the range from the list.
*
* handleRequest() adds a BTInterval to _requested.
* handleCancel() removes a BTInterval from _requested.
* readyForWriting() removes a BTInterval from _requested, and sends a Piece message.
*/
public class BTConnection {
/** A debugging log we can write lines of text to as the program runs. */
private static final Log LOG = LogFactory.getLog(BTConnection.class);
/**
* 16384 bytes, 16 KB.
*
* At the most, we'll request 16 KB of data at a time.
* This is a 16 KB range of data within a single piece, which is much larger.
*
* This is the largest range a BitTorrent program is supposed to request.
*/
private static final int BLOCK_SIZE = 16384;
/**
* 65536 bytes, 64 KB.
*
* If a remote computer gives us a Request message that asks for more than 64 KB of data, we won't do it.
* This describes a 64 KB range of data within a single piece, which is larger.
*/
private static final int MAX_BLOCK_SIZE = 64 * 1024;
/** 4, we'll only ask for 4 pieces from this remote computer at a time. */
private static final int MAX_REQUESTS = 4;
/**
* 1 minute in milliseconds.
* isWorthRetrying() returns true if it's called more than a minute after this BTConnection object was made.
*/
private static final long MIN_RETRYABLE_LIFE_TIME = 60 * 1000;
/** The NIOSocket object we use to send and receive data with the remote computer this BTConnection represents. */
private final NIOSocket _socket;
/**
* Slices data from this remote computer into BitTorrent messages, and parses them into objects that extend BTMessage.
* _reader joins a chain of readers in LimeWire's NIO design.
*/
private BTMessageReader _reader;
/**
* To send a message, give its data to this BTMessageWriter object.
* _writer joins a chain of writers in LimeWire's NIO design.
*/
private BTMessageWriter _writer;
/**
* Tells which pieces of the torrent this remote computer has, and we need.
* This is what we can get from it.
*
* A BitSet object that has 1 bit for each piece of this torrent.
* If a bit it 0, that means this remote computer doesn't have that piece.
*
* Tells which pieces this remote computer has, and we don't.
* sendHave() only sends this remote computer a Have message if it doesn't have that piece yet.
* If it does already have the piece, sendHave() changes _availableRanges to make it look like it doesn't.
* Then, if _availableRanges is all 0s, sendHave() sends a Not Interested message.
*
* Lock on _availableRanges before using it.
*/
private final BitSet _availableRanges;
/**
* A list of parts of pieces we're going to ask this remote computer for.
* We haven't send Request messages for these intervals yet, but will soon.
*
* A HashSet of BTInterval objects.
* A HashSet is a list that doesn't allow duplicates, and calls BTInterval.hashCode() to be fast.
*/
private final Set _toRequest;
/**
* A list of parts of pieces we've asked this remote computer for.
* We sent Request messages for these, and are waiting for the data.
*
* A HashSet of BTInterval objects.
* A HashSet is a list that doesn't allow duplicates, and calls BTInterval.hashCode() to be fast.
*/
private final Set _requesting;
/**
* A list of parts of pieces this remote computer has asked us to give it.
*
* A HashSet of BTInterval objects.
* A HashSet is a list that doesn't allow duplicates, and calls BTInterval.hashCode() to be fast.
*/
private final Set _requested;
/** The BTMetaInfo object made from the bencoded data in the .torrent file. */
private final BTMetaInfo _info;
/** This remote computer's IP address and port number, and BitTorrent peer ID and extension bytes. */
private final TorrentLocation _endpoint;
/**
* True if we initiated this outgoing connection to this remote computer.
* False if it connected to our listening socket.
*/
private final boolean _outgoing;
/** The ManagedTorrent object that made this BTConnection to share its torrent with this remote computer. */
private final ManagedTorrent _torrent;
/** True while we're choking this remote computer. */
private boolean _isChoked;
/** True while we're interested in this remote computer's data. */
private volatile boolean _isInteresting;
/** True while this remote computer is choking us. */
private volatile boolean _isChoking;
/** True while this remote computer is interested in our data. */
private boolean _isInterested;
/** The time when this BTConnection object was made. */
private final long _startTime;
/**
* The number of pieces we have that this remote computer needs.
* This is the number of pieces it can get from us.
*/
private int numMissing;
/** Not used. */
public BTConnection(NIOSocket sock, BTMetaInfo info, TorrentLocation ep, ManagedTorrent torrent) {
this(sock, info, ep, torrent, false);
}
/**
* Make a new BTConnection object to represent our TCP socket connection to a remote computer.
* The remote computer is running BitTorrent software, and we are sharing our torrent through this connection.
* The connection started with the BitTorrent handshake, and is now carrying BitTorrent messages.
*
* Only BTHandshaker.tryToFinishHandshakes() calls this constructor.
* It's already done the handshake with this remote computer.
*
* Saves the given objects, linking this new BTConnection to the ManagedTorrent that made it.
* Builds the chains of readers and writers according to LimeWire's NIO design.
* Starts us and this remote computer as choking and not interested.
* Sends the remote computer a Bitfield message to tell it which pieces of this torrent we have.
*
* @param socket The NIOSocket object that represents our TCP socket connection to this remote computer
* @param info The BTMetaInfo object we made from the bencoded data inside the .torrent file
* @param ep This remote computer's IP address and port number in a TorrentLocation object
* @param torrent The ManagedTorrent object that is making this BTConnection object
* @param isOutgoing True if we connected to this remote computer, false if it connected to our listening socket
*/
public BTConnection(NIOSocket socket, BTMetaInfo info, TorrentLocation ep, ManagedTorrent torrent, boolean isOutgoing) {
// Save the given NIOSocket object, we'll communicate with this remote computer through it
_socket = socket;
// Save the given TorrentLocation object, it has the IP address and port number of this remote computer
_endpoint = ep;
// Save the given ManagedTorrent object, it's the object that is making this new BTConnection
_torrent = torrent;
// Save the given boolean that tells which computer initiated this connection
_outgoing = isOutgoing;
// Make a new BitSet with one bit for each piece in this torrent, all the bits are 0
_availableRanges = new BitSet(info.getNumBlocks()); // getNumBlocks() returns the number of pieces
// Make 3 new HashSet objects to keep track of requests
_requesting = new HashSet(); // The parts of the file we've sent Request messages to this remote computer for
_toRequest = new HashSet(); // The parts of the file we're going to send Request messages for next
_requested = new HashSet(); // The parts of the file this remote computer has asked us for with Request messages we've received
// Record when this BTConnection object was made
_startTime = System.currentTimeMillis(); // Set _startTime to now, the number of milliseconds since 1970
// Make new BTMessageReader and BTMessageWriter objects
_reader = new BTMessageReader(this); // Give them this so they can link back up to us
_writer = new BTMessageWriter(this);
/*
* we put the throttle on top, this puts the delayer on top
* decide is this what we want? maybe
*/
/*
* The code below builds the chains of readers and writers.
* This is a part of LimeWire's NIO design.
*
* _torrent.getUploadThrottle() gets a reference to the program's single NBThrottle object for BitTorrent.
* The line of code makes a new ThrottleWriter from it, which will control how fast we upload data to this remote computer.
*
* The code also makes a DelayedBufferWriter for this connection.
* It will group 1400 bytes together before sending them all.
*
* The chain of writers for this BTConnection looks like this:
*
* BTMessageWriter -> DelayedBufferWriter -> ThrottleWriter
*/
// Make ThrottleWriter and DelayedBufferWriter objects for this new BTConnection
ThrottleWriter throttle = new ThrottleWriter(_torrent.getUploadThrottle());
DelayedBufferWriter delayer = new DelayedBufferWriter(1400);
// Build this connection's chain of writers
_writer.setWriteChannel(delayer);
delayer.setWriteChannel(throttle);
/*
* When Java gets data from this remote computer, it will tell the NIOSocket object.
* The code below tells the socket object which object to forward the call to.
* We want our BTMessageReader and BTMessageWriter objects to get the notifications from NIO.
*/
// Have our NIOSocket forward NIO notifications to our BTMessageReader and BTMessageWriter
socket.setReadObserver(_reader);
socket.setWriteObserver(_writer);
// Save the given BTMetaInfo object, we'll read the contents of the .torrent file with it
_info = info;
// BitTorrent connections start out choked, and not interested
_isChoked = true; // We're choking this remote computer
_isChoking = true; // As far as we know, this remote computer is choking us
_isInterested = false; // As far as we know, this remote computer is not interested in our data
_isInteresting = false; // We're not interested in this remote computer's data
// Send this remote computer a Bitfield message, telling it which pieces we have
sendBitfield(); // Right after the handshake, BitTorrent programs exchange Bitfield messages
}
/**
* Determine if we're choking this remote computer.
*
* @return True if we sent it a Choke message.
* False if we sent it an Unchoke message.
*/
public boolean isChoked() {
// Return the value sendChoke() or sendUnchoke() set
return _isChoked;
}
/**
* Determine if this remote computer is choking us.
*
* @return True if it sent us a Choke message.
* False if it sent us an Unchoke message.
*/
public boolean isChoking() {
// Return the value that handleMessage() set
return _isChoking;
}
/**
* Determine if this remote computer is interested in our data.
*
* @return True if it sent us an Interested message.
* False if it sent us a Not Interested message.
*/
public boolean isInterested() {
// Return the value that handleMessage() set
return _isInterested;
}
/**
* Determine if we have pieces this remote computer needs.
* If so, this remote computer should be interested in our data.
*
* @return True if we have pieces this remote computer needs
*/
public boolean shouldBeInterested() {
// If numMissing is more than 0, return true
return numMissing > 0;
}
/**
* Determine if we're interested in this remote computer's data.
*
* @return True if we sent it an Interested message.
* False if we sent it a Not Interested message.
*/
public boolean isInteresting() {
// Return the value sendInterested() or sendNotInterested() set
return _isInteresting;
}
/**
* Determine if this remote computer has asked us for anything we haven't given it right now.
*
* @return True if this remote computer has made a request we haven't satisifed yet
*/
public boolean hasRequested() {
// If the _requested list is empty, we've sent all the parts this remote computer has requested
return !_requested.isEmpty();
}
/**
* Determine if we initiated this connection to this remote computer, or if it connected to us.
*
* @return true if this is an outgoing connection we made
*/
public boolean isOutgoing() {
// Return the value the constructor saved
return _outgoing;
}
/**
* Returns true if this BTConnection object is more than a minute old.
*
* Only ManagedTorrent.connectionClosed() calls this, right after disconnecting this connection.
* If we had it for more than a minute, it's worth retrying in the future.
*
* @return True if this connection was open for more than a minute.
* False if we disconnected sooner than that.
*/
public boolean isWorthRetrying() {
/*
* don't retry connections that were aborted immediately after starting
* them, they were most likely terminated for a reason...
*/
// Return true if this BTConnection object is more than a minute old
return System.currentTimeMillis() - _startTime > MIN_RETRYABLE_LIFE_TIME;
}
/**
* Get this remote computer's IP address and port number, and BitTorrent peer ID and extension bytes.
*
* @return The address and BitTorrent handshake information in a TorrentLocation object
*/
public TorrentLocation getEndpoint() {
// Return the TorrentLocation object we saved
return _endpoint;
}
/**
* Close this connection.
*
* Calls shutdownOutput() and shutdownInput() on our Java _socket object.
* Calls shutdown() on our BTMessageReader and BTMessageWriter, closing it with the chains of readers and writers.
* Has the NIODispatcher close our TCP socket connection to the remote computer.
* Tells the ManagedTorrent that made us that we're closed.
*/
public void close() {
// Shut down the Java socket
try { _socket.shutdownOutput(); } catch (IOException ioe1) {}
try { _socket.shutdownInput(); } catch (IOException ioe2) {}
// Shut down our chains of readers and writers
_reader.shutdown(); // Just marks our BTMessageReader as shut down
_writer.shutdown(); // Removes all the messages from its queue, and tells the object it writes to it has no data for it
// Have the NIODispatcher close our TCP socket connection to the remote computer
try { _socket.close(); } catch (IOException ioe) {}
// Tell the ManagedTorrent that made us we're closed
_torrent.connectionClosed(this);
}
/**
* Get the BTMessageReader object this BTConnection uses.
* It slices data from this remote computer into BitTorrent messages, and parses them into objects that extend BTMessage.
*
* @return A reference to our BTMessageReader object
*/
public BTMessageReader getMessageReader() {
// Return the object the constructor made
return _reader;
}
/**
* Determine if our socket is connected to this remote computer.
* Calls isConnected on the Java socket inside this BTConnection's NIOSocket object.
*
* @return True if we're still connected to this remote computer
*/
public boolean isConnected() {
// Call NIOSocket.isConnected(), which leads to java.net.Socket.isConnected()
return _socket.isConnected();
}
/**
* Record that we received a number of bytes of file data.
* Tells this torrent's BTDownloader.
* This method is named "readBytes", but should be spoken "redBytes", using the past tense of read.
*
* @param read The number of bytes of file data we received from this remote computer
*/
public void readBytes(int read) {
// Tell our torrent's BTDownloader
_torrent.getDownloader().readBytes(read);
}
/**
* Record that we sent a number of bytes of file data.
* Tells this torrent's BTUploader.
*
* @param written The number of bytes of file data we sent to this remote computer
*/
public void wroteBytes(int written) {
// Tell our torrent's BTUploader
_torrent.getUploader().wroteBytes(written);
}
/**
* Close this connection because we got an IOException.
*
* This method handles IOExceptions for this connection.
* If NIO gets an exception while transferring data for our socket, it will give the exception to this method.
*
* @param iox The exception we got
*/
public void handleIOException(IOException iox) {
// If this exception is about a bad BitTorrent message, count that we got another one
if (iox instanceof BadBTMessageException) BTMessageStat.INCOMING_BAD.incrementStat();
// Make a note about this exception in the debugging log
if (LOG.isDebugEnabled()) LOG.debug(iox);
// Close our connection to this remote computer
close();
}
/**
* Read and respond to a BitTorent message this remote computer has sent us.
*
* Only BTMessageReader.handleRead() calls this method.
* Just calls handleMessage(message).
*
* @param message The BitTorrent message from this remote computer
*/
public void processMessage(final BTMessage message) {
// Make a note in the debugging log, and pass the message to handleMessage()
if (LOG.isDebugEnabled()) LOG.debug("received message: " + message + " from " + _endpoint);
handleMessage(message);
}
/**
* Choke this remote computer.
* Sends it a Choke message, and sets _isChoked to true.
*/
void sendChoke() {
// Forget about all the Request messages this remote computer has sent us
_requested.clear();
// Only do something if we're not choking this remote computer already
if (!_isChoked) {
// Send a Choke message to this remote computer, and record that we're choking it
_writer.enqueue(BTChoke.createMessage());
_isChoked = true;
}
}
/**
* Unchoke this remote computer.
* Sends it an Unchoke message, and sets _isChoked to false.
*/
void sendUnchoke() {
// Only do something if we're choking this remote computer right now
if (_isChoked) {
// Send an Unchoke message to this remote computer, and record that we're not choking it
_writer.enqueue(BTUnchoke.createMessage());
_isChoked = false;
}
}
/**
* Tell this remote computer we're interested in its data.
* Sends it an Interested message, and sets _isInteresting to true.
*/
void sendInterested() {
// Only do something if this remote computer thinks we're not interested in its data
if (!_isInteresting) {
// Send it an Interested message, and record that we told it
LOG.debug("sending interested message");
_writer.enqueue(BTInterested.createMessage());
_isInteresting = true;
}
}
/**
* Tell this remote computer we're not interested in its data.
* Sends it a Not Interested message, and sets _isInteresting to false.
*/
void sendNotInterested() {
// Only do something if this remote computer thanks we're interested in its data
if (_isInteresting) {
// Send it a Not Interested message, and record that we told it
_writer.enqueue(BTNotInterested.createMessage());
_isInteresting = false;
}
}
/**
* Send a Have message to this remote computer, telling it we have a piece.
*
* Only ManagedTorrent.notifyOfComplete() calls this method.
* We've just received a complete piece, and checked that its hash is correct.
* notifyOfComplete() loops for each of the ManagedTorrent's connections.
* It calls sendHave() to send a Have message to this remote computer.
* The Have message tells this remote computer that we have this piece.
*
* Make sure to call this method only for complete pieces.
*
* @param have The Have message which tells that we have a complete piece
*/
void sendHave(BTHave have) {
// Read the piece number from the given Have message
int pieceNum = have.getPieceNum(); // This is the piece we have
/*
* As a minor optimization we will not inform the remote host of any
* pieces that it already has
* Else, simply remove the chunk from the available ranges, to optimize
* requesting ranges...
*/
// If this remote computer doesn't have the piece we just got
if (!_availableRanges.get(pieceNum)) {
// Record that there's one more piece this remote computer needs
numMissing++;
// Send the Have message to this remote computer to tell it that it can get the piece from us
_writer.enqueue(have);
// This remote computer already has the piece we just got
} else {
// Change our record of which pieces this remote computer has to make it look like the remote computer does not have this piece
_availableRanges.clear(pieceNum); // This makes our code of requesting ranges a little faster
}
/*
* we should indicate that we are not interested anymore, so we are
* not unchoked when we do not want to request anything.
*/
// If this remote computer doesn't have anything we need, send it a Not Interested message
if (_availableRanges.isEmpty()) sendNotInterested();
// If we removed a BTInterval from _requesting or _toRequest, the loops below will set modified to true
boolean modified = false;
// Loop for each BTInterval in _requesting, we sent this remote computer Request messages to ask for these pieces
for (Iterator iter = _requesting.iterator(); iter.hasNext(); ) {
BTInterval req = (BTInterval)iter.next();
// If this request was for data within the piece we now have
if (req.getId() == pieceNum) {
// Remove it from the list
iter.remove();
// Send the remote computer a Cancel message, we don't need it anymore
sendCancel(req);
// Yes, we modified the _requesting or _toRequest lists
modified = true;
}
}
// Loop for each BTInterval in _toRequest, the intervals we're going to request from this remote computer
for (Iterator iter = _toRequest.iterator(); iter.hasNext(); ) {
BTInterval req = (BTInterval)iter.next();
// If this pending request was for data within the piece we now have
if (req.getId() == pieceNum) {
// Remove it from the list
iter.remove();
// Yes, we modified the _requesting or _toRequest lists
modified = true;
}
}
/*
* if we removed any ranges, choose some more rages to request...
*/
// If we need data and _toRequest is empty, have the ManagedTorrent send a Request message
if (!_torrent.isComplete() && // If we still need parts of this torrent, and
modified && // The piece we just got cancelled some requests we made this remote computer, or were going to make, and
_isInteresting && // This remote computer still has data we need, and
_toRequest.isEmpty()) { // Our _toRequest list of things to ask for is empty
// Have the ManagedTorrent object pick a range to ask for, and send a Request message
_torrent.request(this);
}
}
/**
* Send a Bitfield message to this remote computer, showing it which pieces of the torrent we have and which we need.
* BitTorrent programs exchange Bitfield messages right after the BitTorrent handshake.
*
* Only the BTConnection constructor calls this method.
* The constructor runs right after we've finished the BitTorrent handshake with this remote computer.
*/
void sendBitfield() {
// Make a Bitfield message, and send it to this remote computer
_writer.enqueue(BTBitField.createMessage(_info)); // _info links to the torrent's VerifyingFolder, which knows which pieces we have
}
/**
* Add a given piece number to those we'll request, and send this remote computer a Request message.
* Only ManagedTorrent.request() calls this method.
*
* @param in A BTInterval object with the piece number and data range within that piece we want to ask this remote computer for
*/
void sendRequest(BTInterval in) {
/*
* we do not request any pieces larger than BLOCK_SIZE!
*/
// Loop, moving i forward 16 KB within this piece
for (long i = in.low; // Start i at the distance into the piece the range we want starts
i < in.high; // If i reaches high, the last byte in the range, don't run the loop this time
i += BLOCK_SIZE) { // Move i forward 16 KB within this piece
/*
* watch out, all Intervals are inclusive on both ends...
* safe cast, length is always <= BLOCK_SIZE
*/
// Calculate length, the number of bytes we'll ask for
int length = (int)Math.min( // Make sure it isn't bigger than the size of a piece
in.high - i + 1, // The length of the data from i to the end of the interval
BLOCK_SIZE); // The maximum size of our request, 16 KB
// Make a new BTInterval object that has the range within a single piece
BTInterval toReq = new BTInterval(
i, // The data we want starts at i
i + length - 1, // The last byte we want
in.getId()); // The given piece number
// If we haven't already asked this remote computer for this range, add it to the _toRequest list
if (!_requesting.contains(toReq)) _toRequest.add(toReq);
}
// Send this remote computer 4 Request messages from the BTIntervals in the _toRequest list
enqueueRequests();
}
/**
* Send this remote computer a Cancel message.
*
* We previously sent it a Request message.
* Our Cancel message will have the same piece number and range information.
* This is how the remote computer will know which of our requests to cancel.
*
* @param in The BTInterval with the piece number and range within that piece that we don't want anymore.
*/
private void sendCancel(BTInterval in) {
// Make a new Cancel message from the given BTInterval, and send it to this remote computer
_writer.enqueue(new BTCancel(in));
}
/**
* Send a Cancel message for each Request message we've sent this remote computer that it hasn't fulfilled yet.
*
* Only MangedTorrent.saveCompleteFiles() calls this method.
* We've finished downloading the entire torrent, and will move it from the "Incomplete" folder to the "Shared" folder.
*/
void cancelAllRequests() {
// Loop for each BTInterval object in our _requesting list, these are the Request messages we've sent this remote computer
Iterator iter = _requesting.iterator();
while (iter.hasNext()) {
BTInterval request = (BTInterval)iter.next();
// Send a Cancel message with the same piece number and data range information
sendCancel(request);
}
// Clear the _toRequest and _requesting lists, and tells the piece numbers to the VerifyingFolder
clearRequests();
}
/**
* Get a request from our _requested list, and send this remote computer a Piece message with the part of this torrent it asked for.
* This method removes a BTInterval from the _requested list.
*
* BTMessageWriter.sendNextMessage() and BTConnection.handleRequest() call this method.
*
* This method is called readyForWriting() because the program can call it when the program is ready to write data to this remote computer.
* The methd writes data to the remote computer, sending it a Piece message with the file data it previously asked for.
*/
void readyForWriting() {
// If we're choking this remote computer, or it hasn't sent us any Request messages we haven't satisified yet
if (_isChoked || _requested.size() <= 0) {
// We can't send this remote computer data, make a note and leave without doing anything
if (LOG.isDebugEnabled()) LOG.debug("cannot write while choked, requested size is " + _requested);
return;
}
/*
* If control makes it here, this remote computer has sent us Request messages, and we can send Piece messages in response.
*/
// Get a single BTInterval from the _requested list
Iterator iter = _requested.iterator();
BTInterval in = (BTInterval)iter.next(); // The BTInterval has the piece number and range the remote computer wants from us
iter.remove(); // Remove it from the _requested list
if (LOG.isDebugEnabled()) LOG.debug("requesting disk read for " + in);
try {
// Have the torrent's VerifyingFolder object send this remote computer the data it wants in a Piece message
_info.getVerifyingFolder().sendPiece(in, this);
// There was an error with the disk
} catch (IOException bad) {
// Close this connection
close();
}
}
/**
* Make a new Piece message, and send it to this remote computer.
* Only VerifyingFolder.SendJob.run() calls this method.
*
* @param in The piece number and range within that piece of the data to send
* @param data The data to send
*/
void pieceRead(final BTInterval in, final byte[] data) {
// Have the "NIODispatch" thread call this run() method
Runnable pieceSender = new Runnable() {
public void run() {
// Make a new Piece message, and send it to this remote computer
if (LOG.isDebugEnabled()) LOG.debug("disk read done for " + in);
_writer.enqueue(new BTPiece(in, data));
}
};
NIODispatcher.instance().invokeLater(pieceSender);
}
/**
* Get a BitSet that tells which pieces of the torent this remote computer has, and we need.
* This is what we can get from it.
*
* A BitSet object that has 1 bit for each piece of this torrent.
* If a bit it 0, that means this remote computer doesn't have that piece.
*
* @return A BitSet with bits representing pieces
*/
BitSet getAvailableRanges() {
// Return the BitSet that tells which pieces we can get from this remote computer
return _availableRanges;
}
/**
* Call addAvailablePiece(pieceNumber) when we find out this remote computer has a piece.
* Tells the torrent's VerifyingFolder we have a connection to a computer that has the given piece number.
* If we need this piece, sends this remote computer an Intersted message.
*
* handleHave() and handleBitField() call this method.
*
* @param pieceNum The piece number this remote computer says it has
*/
private void addAvailablePiece(int pieceNum) {
// Tell the torrent's VerifyingFolder that a remote computer has that piece
VerifyingFolder v = _info.getVerifyingFolder();
_availableRanges.set(pieceNum); // But, it won't know to ask this remote computer for it
/*
* tell the remote host we are interested if we don't have that range!
*/
// We and this remote computer both have the piece
if (v.hasBlock(pieceNum)) {
// Count one fewer piece we have that this remote computer needs
numMissing--;
// We need the piece, and can get it from this remote computer
} else {
// If we haven't already done so, send this remote computer an Interested message
sendInterested();
}
}
/**
* Clear the _toRequest and _requesting lists, and tells the piece numbers to the VerifyingFolder.
*
* Loops through all the BTInterval objects in our _toRequest and _requesting lists.
* These are the intervals we were going to request from this remote computer, and have requests.
* Gets the piece number each is in, adn tells our torrent's VerifyingFolder to release those pieces.
* This tells the VerifyingFolder that those pices can be requested again.
* Clears the _toRequest and _requesting lists.
*/
private void clearRequests() {
// Loop for each BTInterval in the _toRequest list
for (Iterator iter = _toRequest.iterator(); iter.hasNext(); ) {
// Tell our torrent's VerifyingFolder to release the piece number this interval is in
clearRequest((BTInterval)iter.next());
}
// Loop for each BTInterval in the _requesting list
for (Iterator iter = _requesting.iterator(); iter.hasNext(); ) {
// Tell our torrent's VerifyingFolder to release the piece number this interval is in
clearRequest((BTInterval)iter.next());
}
// Make both lists empty
_toRequest.clear();
_requesting.clear();
}
/**
* Tell our torrent's VerifyingFolder that we're not requesting a piece number any longer.
*
* @param in A BTInterval that describes a range of data within a piece.
* This method just uses the piece number, in.getId().
*/
private void clearRequest(BTInterval in) {
// Tell our torrent's VerifyingFolder that we're not requesting the given piece number anymore
_info.getVerifyingFolder().releaseChunk(in.getId());
}
/**
* Send this remote computer 4 Request messages from the BTIntervals in the _toRequest list.
*
* Randomly picks 4 BTInterval ranges from the _toRequest list.
* Composes BitTorrent Request messages from them, and sends them to this remote computer.
* Moves the BTInterval objects from the _toRequest list to the _requesting list.
*
* Stops if we have 4 requests pending with this remote computer, or if we have 10 messages waiting to go out to it.
*/
private void enqueueRequests() {
/*
* the reason we randomize the list of requests to be sent is that we
* are receiving far too many ranges multiple times when the download
* is about to finish.
*/
// Copy all the BTInterval ranges we're going to ask this remote computer for into a new list, and shuffle it
List random = new ArrayList();
random.addAll(_toRequest);
Collections.shuffle(random);
// Loop for each BTInterval object in the _toRequest list, moving through them in random order
for (Iterator iter = random.iterator();
// Only run this loop if all of the following things are true
_requesting.size() < MAX_REQUESTS && // We haven't asked this computer for 4 pieces yet, and
iter.hasNext() && // We're not through the whole _toRequest list yet, and
!_isChoking; ) { // This remote computer isn't choking us right now
// Get the BTInterval for this run of the loop
BTInterval toReq = (BTInterval)iter.next();
// Send this remote computer a Request message that will ask for the toReq range
if (!_writer.enqueue(new BTRequest(toReq))) return; // enqueue() returns false if _writer already has 10 messages
// The message will be sent, move toReq from _toRequest to _requesting
_toRequest.remove(toReq);
_requesting.add(toReq);
}
}
/**
* Read and respond to a BitTorent message this remote computer has sent us.
*
* Only BTConnection.processMessage() calls this method.
* Here's what's happened so far:
* BTMessageReader.handleRead() sliced a BitTorrent message from the data this remote computer sent us.
* It parsed it into a type-specific object that extends BTMessage, and passed it to the next method.
* BTConnection.processMessage() just calls this method.
*
* @param message The BitTorrent message from this remote computer
*/
private void handleMessage(BTMessage message) {
// Sort by the type of message this remote computer sent us
switch (message.getType()) {
// This remote computer sent us a Choke message, it's not going to send us any data
case BTMessage.CHOKE:
/*
* we queue all requests up again, maybe we get unchoked
* again, - if not this will be resolved in endgame mode
*/
// Now that we're choked, move all the intervals we requested back from _requesting to _toRequest
for (Iterator iter = _requesting.iterator(); iter.hasNext(); ) {
_toRequest.add(iter.next());
iter.remove();
}
// Record that this remote computer is choking us
_isChoking = true;
break;
// This remote computer sent us an Unchoke message, it is willing to send us data
case BTMessage.UNCHOKE:
// Record that this remote computer isn't choking us any longer
_isChoking = false;
// If we're interested in this remote computer's data right now
if (_isInteresting) { // If it has parts of this torrent's file that we need
// Randomly pick 4 ranges to request from this remote computer
enqueueRequests(); // We'll send the Request messages as soon as we are unchoked
// If we still need parts of this torrent, and our list of intervals to ask for is empty
if (!_torrent.isComplete() && // If we don't have this whole torrent yet, and
_toRequest.isEmpty()) { // Our list of pieces to ask for is empty
// Have the ManagedTorrent object pick a range to ask for, and send a Request message
_torrent.request(this);
}
}
break;
// This remote computer sent us an Interested message, it is interested in our data
case BTMessage.INTERESTED:
// This remote computer is interested in our data
_isInterested = true;
// If we're not choking this remote computer, send each of this torrent's connections a Choke or Unchoke message now
if (!_isChoked) _torrent.rechoke();
break;
// This remote computer sent us a Not Interested message, it doesn't need any of the data we have
case BTMessage.NOT_INTERESTED:
// This remote computer doesn't need any of the data we have for this torrent
_isInterested = false;
// If we're not choking this remote computer, send each of this torrent's connections a Choke or Unchoke message now
if (!_isChoked) _torrent.rechoke();
/*
* connections that aren't interested any more are choked instantly
*/
// In response to this remote computer's Not Interested message, send it a Choke message to choke it
sendChoke();
/*
* if we have all pieces and the remote is not interested,
* disconnect, - they have obviously completed their download, too
*/
// If we have the whole file and this remote computer doesn't need any data, close our connection to it
if (_torrent.isComplete()) close();
break;
// This remote computer sent us a Bitfield message, it's showing us which pieces it has and which pieces it needs
case BTMessage.BITFIELD:
// Tell the torrent's VerifyingFolder which pieces this remote computer has, and figure out what we can get from it
handleBitField((BTBitField)message);
break;
// This remote computer sent us a Have message, it just received and validated a numbered piece
case BTMessage.HAVE:
// Tell the torrent's VerifyingFolder we have a connection to a remote computer that has this piece
handleHave((BTHave)message);
break;
// This remote computer sent us a Piece message, it's sending us a range of file data within a numbered piece
case BTMessage.PIECE:
// Save the file data inside a Piece message to disk, and send more Request messages to ask for more
handlePiece((BTPiece)message);
break;
// This remote computer sent us a Request message, it wants us to send some file data in a Piece message
case BTMessage.REQUEST:
// Respond to a Request message from this remote computer by sending a Piece message with the file data it asked for
handleRequest((BTRequest)message);
break;
// This remote computer sent us a Cancel message, it's cancelling a previous request it made in a Request message
case BTMessage.CANCEL:
// Remove a request this remote computer made from our list of them
handleCancel((BTCancel)message);
break;
}
}
/**
* Remove a request this remote computer made from our list of them, if we find one that matches a given Cancel message this remote computer sent us.
*
* Only BTConnection.handleMessage() calls this method.
* This remote computer has sent us a Cancel message, cancelling a request for data made by a Request message it previously sent us.
*
* @param message A Cancel message from this remote computer
*/
private void handleCancel(BTCancel message) {
// Read the given Cancel message, getting the piece number and range within that piece that it's cancelling a previous request for
BTInterval in = message.getInterval();
/*
* removes the range from the list of requests. If we are already
* sending the piece, there is nothing we can do about it
*/
// Loop through the BTInterval objects in our _requested list, these are the requests this remote computer has asked for
for (Iterator iter = _requested.iterator(); iter.hasNext(); ) {
BTInterval current = (BTInterval) iter.next();
// If the Cancel message matches or exceeds this request in the _requested list
if (in.getId() == current.getId() && // If the piece numbers are the same, and
(in.low <= current.high && current.low <= in.high)) { // The range the Cancel message clips out is the same or bigger than the range of the request
// Remove the BTInterval from the _requested list
iter.remove();
}
}
}
/**
* Respond to a Request message from this remote computer by sending a Piece message with the file data it asked for.
*
* Only BTConnection.handleMessage() calls this method.
* This remote computer has sent us a Request message, asking for a part of the torrent we're sharing.
*
* @param message The Request message from this remote computer
*/
private void handleRequest(BTRequest message) {
/*
* we do not accept request from choked connections, if we just choked
* a connection, we may still receive some requests, though
*/
// If we're choking this remote computer
if (_isChoked) {
// We won't send this remote computer data while we're choking it, make a note and leave
if (LOG.isDebugEnabled()) LOG.debug("got request while choked");
return;
}
// Read the Request message to find out what piece number and data range within that piece this remote computer is asking us for
BTInterval in = message.getInterval();
if (LOG.isDebugEnabled()) LOG.debug("got request for " + in);
/*
* ignore, that's a buggy client sending this request (didn't manage to
* find out which one) - we could also throw an exception causing us to
* disconnect...
*/
// If the Request message is asking for a piece number beyond the end of the file
if (in.getId() > _info.getNumBlocks()) {
// Make a note about a bad request and leave without doing anything
if (LOG.isDebugEnabled()) LOG.debug("got bad request " + message);
return;
}
/*
* we skip all requests for ranges larger than MAX_BLOCK_SIZED as
* proposed by the BitTorrent spec.
*/
// Make sure the data range isn't bigger than our limit for how much remote computers can ask us for
if (in.high - in.low + 1 > MAX_BLOCK_SIZE) {
// Requests can only clip out data in a single piece, make a note and leave without doing anything
if (LOG.isDebugEnabled()) LOG.debug("got long request");
return;
}
/*
* skip the message if we don't have that range
*/
// If our VerifyingFolder says we have that data
if (_info.getVerifyingFolder().hasBlock(in.getId())) {
// Add the range to our _requested list of ranges this remote computer has asked us for
_requested.add(in);
}
// If we have a pending request from this remote computer, and no other messages to send it, send it a Piece message
if (!_requested.isEmpty() && // If we have requests from this remote computer waiting, either from right now, or from before, and
_writer.isIdle()) { // We don't have any messages waiting to send this remote computer right now
// Send this remote computer a Piece message with one of the ranges in the _requested list
readyForWriting();
}
}
/**
* Save the file data inside a Piece message to disk, and send more Request messages to ask for more.
*
* Only handleMessage() above calls this method.
* This remote computer has sent us a Piece message with file data inside.
*
* @param message The Piece message this remote computer sent us
*/
private void handlePiece(BTPiece message) {
// Get where the data goes, and the data, from the Piece message this remote computer sent us
final BTInterval in = message.getInterval(); // Tells the piece number, and the range within that piece
final byte[] data = message.getData(); // A byte array with the file data
// Record that we received this much more file data for this torrent
readBytes(data.length);
// Remove the range of data we got from our lists of what to ask this remote computer
if (!_requesting.remove(in) && // Remove the interval we got from those we're going to ask for, if it wasn't in that list
!_toRequest.remove(in)) { // Try removing it from the list of intervals we did ask for, if it wasn't there either
// Make a note that this remote computer sent us something we didn't ask for, and leave
if (LOG.isDebugEnabled()) LOG.debug("received unexpected range " + in + " from " + _socket.getInetAddress() + " expected " + _requesting + " " + _toRequest);
return;
}
try {
// Get the torrent's VerifyingFolder object, which can save data to disk
VerifyingFolder v = _info.getVerifyingFolder();
// If we already have this piece, leave
if (v.hasBlock(in.getId())) return;
// Give the file data to the VerifyingFolder, which will save it to disk
_info.getVerifyingFolder().writeBlock(in, data);
// writeBlock() threw an exception, there was a problem writing the data to disk
} catch (IOException ioe) {
// Inform the user and stop the torrent
IOUtils.handleException(ioe, null);
_torrent.stop();
}
// Request more ranges if we don't have enough requests right now
if (!_torrent.isComplete() && // If we haven't downloaded this whole torrent yet, and
_toRequest.size() + _requesting.size() < MAX_REQUESTS) { // We don't have 4 requests to make right now
// Have the ManagedTorrent object pick a range to ask for, and send a Request message
_torrent.request(this);
}
// Send this remote computer 4 Request messages from the BTIntervals in the _toRequest list
enqueueRequests();
}
/**
* Tell the torrent's VerifyingFolder which pieces this remote computer has, and figure out what we can get from it.
*
* Only BTConnection.handleMessage() calls this method.
* This remote computer has sent us a Bitfield message.
* BitTorrent programs exchange Bitfield messages right after the BitTorrent handshake.
*
* Gives each piece number this remote computer has to the torrent's VerifyingFolder.
* The VerifyingFolder knows that we have a connection to a computer that has the piece, but doesn't know which computer has it.
* This lets the VerifyingFolder determine which pieces are rare and which are common.
*
* Puts together _availableRanges, the BitSet which tells which pieces we need and this remote computer has.
* This tells what we can get from it.
* Sets numMissing, the number of pieces we have that this remote computer needs.
* This tells what it can get from us.
*
* @param message The Bitfield message from this remote computer
*/
private void handleBitField(BTBitField message) {
// Get the bit field, a byte array with a bit representing each piece this remote computer has or needs
byte[] field = message.getBitField();
// Find out how many pieces this torrent has
int numBits = _info.getNumBlocks();
// Calculate the number of bytes the bit field for that many pieces should be
int bitFieldLength = (numBits + 7) / 8;
// Make sure the bit field the remote computer gave us is the right length
if (field.length != bitFieldLength) {
// It's not, throw an IOException as NIO would
handleIOException(new BadBTMessageException("bad bitfield received! " + _endpoint.toString()));
}
// Loop for each bit in the bit field
for (int i = 0; i < numBits; i++) {
byte mask = (byte)(0x80 >>> (i % 8));
if ((mask & field[i / 8]) == mask) {
/*
* now, addAvailablePiece() sends this remote computer an Interested message as soon as i is something we need.
* change this, do not send interested until all are added.
*/
// Tell the torrent's VerifyingFolder we have a connection to a remote compuer that has this piece
addAvailablePiece(i);
}
}
// Set numMissing to the number of pieces we have that this remote computer needs
numMissing = _info.getVerifyingFolder().getNumMissing(_availableRanges);
}
/**
* Tell the torrent's VerifyingFolder we have a connection to a remote computer that has this piece.
*
* @param message the Have message from this remote computer
*/
private void handleHave(BTHave message) {
// Tell the torrent's VerifyingFolder we have a connection to a remote computer that has this piece
int pieceNum = message.getPieceNum();
addAvailablePiece(pieceNum);
}
/**
* Determine if this BTConnection object is the same as a given one.
* Compares their IP addresses and BitTorrent peer IDs.
*
* @return true if they are the same, false if different
*/
public boolean equals(Object o) {
// Make sure the given object is also a BTConnection
if (o instanceof BTConnection) {
BTConnection other = (BTConnection)o;
// Compare their TorrentLocation _endpoint members
return other._endpoint.equals(_endpoint); // Matches the IP addresses and peer IDs only
}
// Different
return false;
}
/**
* Express this BTConnection as text.
* Composes a String like:
*
* 1.2.3.4:5:LIMEguidguidguidguid:base32extensionbytes
*
* The parts are separated by ":"
* First are the IP address and port number.
* After that is the 20-byte peer ID, converted into a String.
* The vendor code at the start will be readable, while the guid may not be.
* Last are the extension bytes, written in base 32 encoding.
*
* @return A String
*/
public String toString() {
// Turn the TorrentLocation that has the address of this remote computer into text
return _endpoint.toString();
}
}