/*
* PeerCoordinator which uses BASS strategy to download pieces. The file is
* split up in windows (each consising of a number of pieces depending on ALFA
* and the bitrate), the pieces from the current window are downloaded
* from the server.
*/
package p2pp;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import org.klomp.snark.BitField;
import org.klomp.snark.CoordinatorListener;
import org.klomp.snark.MetaInfo;
import org.klomp.snark.Peer;
import org.klomp.snark.PeerCoordinator;
import org.klomp.snark.Storage;
import org.klomp.snark.TrackerClient;
public class BassPeerCoordinator extends PeerCoordinator {
// Window in seconds.
private final int ALFA = 20;
// Window in pieces.
private int windowSize;
// Current piece position of window.
private int windowPosition = 0;
// Missing pieces in the current window, which are being
// downloaded from the server.
private ArrayList<Integer> missingWindowPieces;
// Current requested pieces.
private List<Integer> requestedPieces = new ArrayList<Integer>();
private WebSeed webSeed;
/**
* Creates new BASS coordinator.
* @param bitrate The bitrate of the media file in bits per second (bit/s).
* @param serverUrl The full to the media server. E.g. 'http://ge.tt/#6aYJ5'.
* @param useName True if the web seed should use the name attribute from
* the torrent file to construct the url.
* @throws MalformedURLException If the url is invalid.
*/
public BassPeerCoordinator(byte[] id, MetaInfo metainfo, int bitrate,
Storage storage, CoordinatorListener listener,
String serverUrl, boolean useName) throws MalformedURLException {
// Call the super constructor.
super(id, metainfo, storage, listener);
// Create a new WebSeed object. If the server does not support
// range requests throw exception.
this.webSeed = new WebSeed(serverUrl, metainfo, useName);
if(!webSeed.supportsRange())
throw new UnsupportedOperationException("The server does not support range requests!");
this.windowSize = ((bitrate * ALFA) >> 3) / metainfo.getPieceLength(0) + 1;
missingPieces(windowPosition, windowSize);
log.log(Level.INFO, "BASS started with window size " + windowSize);
// Sort the wantedPieces list.
Collections.sort(wantedPieces);
}
/**
* Called when a piece has been finished downloading. Return false is the
* piece is not valid according to the hash in the torrent.
* @param peer The peer which uploaded us the piece.
* @param piece The downloaded piece.
* @param data The raw piece data.
* @return Returns if the piece is valid.
*/
@SuppressWarnings("unchecked")
@Override
public boolean gotPiece(Peer peer, int piece, byte[] data)
throws IOException {
// Always call the super.gotPiece method. It stores the piece
// and checks if it's valid.
boolean valid = super.gotPiece(peer, piece, data);
//
synchronized(missingWindowPieces) {
// If the server uploaded the piece it will be present in the
// missing window list. Remove it from the list.
missingWindowPieces.remove(new Integer(piece));
// Completed download of a window. Prepare the next window and
// start downloading from the server.
if(missingWindowPieces.isEmpty()) {
do {
windowPosition += windowSize;
missingPieces(windowPosition, windowSize);
} while(missingWindowPieces.isEmpty() && !wantedPieces.isEmpty());
// Start downloading of pieces from server.
Thread webDownload = new Thread(serverDownload(
(List<Integer>) missingWindowPieces.clone(), windowPosition));
webDownload.start();
}
}
return valid;
}
/**
* Override the default wantPiece strategy (the default is to use random
* piece selection). The method is called when a peer is willing to upload
* to us and we haven't started a download from that peer.
* @param peer The ready peer.
* @param have A bitfield with all the pieces the peers has.
* @return Returns the wanted piece or -1 if we don't want a piece from
* this peer.
*/
@Override
public int wantPiece(Peer peer, BitField have) {
// Only request pieces which are in the newt window and beyond.
// We are already downloading the pieces from this window from the server.
int fromPiece = windowPosition + windowSize;
synchronized(wantedPieces) {
// Prioritize closest pieces.
for(int i = fromPiece; i < getMetaInfo().getPieces(); i++)
// If the peer has the piece and the piece is not already
// requested we request the piece.
if(have.get(i) && !requestedPieces.contains(i)) {
requestedPieces.add(i);
// Move the wanted piece to end of the list.
wantedPieces.remove(new Integer(i));
wantedPieces.add(i);
// Notify the download listener that piece is requsted.
synchronized(downloadProgress) {
if(this.downloadProgress != null)
downloadProgress.pieceRequested(peer, i);
}
return i;
}
// Return -1 if we the peers doesn't have any interesting pieces.
return -1;
}
}
/**
* Sets tracker to be used for peer list updated.
* Starts the server download of the first window.
*/
@Override
public void setTracker(TrackerClient client) {
super.setTracker(client);
@SuppressWarnings("unchecked")
Thread webDownload = new Thread(serverDownload(
(List<Integer>) missingWindowPieces.clone(), windowPosition));
webDownload.start();
}
/**
* Starts downloading pieces from the server. The pieces are downloaded
* in the order they apear in the list.
* @param pieces List of pieces to download.
* @param window Which window position. Used for logging.
* @return A runnable object whos run method starts the download
* of pieces from server.
*/
private Runnable serverDownload(final List<Integer> pieces, final int window) {
return new Runnable() {
@Override
public void run() {
log.log(Level.INFO, "Server download session started [window=" +
window + "]");
Iterator<Integer> it = pieces.iterator();
while(it.hasNext()) {
int piece = it.next();
// Check if we still need this piece.
boolean stillWanted = wantedPieces.contains(piece);
if(stillWanted) {
// Download data from server.
byte[] data = getServerPiece(piece);
// If data is null error occurred. What to do? Panic?
if(data != null)
try {
// if gotPiece returns false the downloaded
// piece does not correspond to the hash in
// the torrent. This should not happen!
if(!gotPiece(null, piece, data))
log.log(Level.SEVERE, "Downloaded bad piece " +
piece + " from web seed " + webSeed);
} catch (IOException e) {
// Ignore. Thrown when trying to abort Snark
// if an error during writing to disk occurred.
}
}
}
log.log(Level.INFO, "Server download session completed [window=" +
window + "]");
}
};
}
/**
* Finds the missing pieces in window. It simply runs through wantedPieces
* and when ever it runs into a piece s.t. pos <= piece < pos + size, it
* puts that piece in the missing window list.
* @param pos The window position.
* @param size The window size.
*/
private void missingPieces(int pos, int size) {
if(missingWindowPieces == null)
missingWindowPieces = new ArrayList<Integer>();
else
missingWindowPieces.clear();
synchronized(wantedPieces) {
Iterator<Integer> it = wantedPieces.iterator();
while(it.hasNext()) {
int piece = it.next();
if(pos <= piece && piece < pos + size) {
missingWindowPieces.add(piece);
}
}
}
Collections.sort(missingWindowPieces);
}
/**
* Retrieves a whole piece from the server.
* @param piece Piece to download.
* @return Returns the raw byte of that piece.
*/
private byte[] getServerPiece(int piece) {
MetaInfo info = getMetaInfo();
// Calculate start index.
long index = piece * info.getPieceLength(0);
try {
// Notify the download listener that we are requesting
// a piece from the server.
synchronized(downloadProgress) {
if(this.downloadProgress != null)
downloadProgress.pieceRequested(null, piece);
}
// Return the bytes.
return webSeed.getBlock(index, info.getPieceLength(piece));
}
catch (IOException e) {
log.log(Level.SEVERE, "Error while reading block from server.", e);
}
// Return null if not able to download block.
return null;
}
}