/*
* PeerState - Keeps track of the Peer state through connection callbacks.
* Copyright (C) 2003 Mark J. Wielaard
*
* This file is part of Snark.
*
* 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, 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., 59 Temple
* Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.klomp.snark;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
class PeerState
{
final Peer peer;
final PeerListener listener;
final MetaInfo metainfo;
// Interesting and choking describes whether we are interested in or
// are choking the other side.
boolean interesting = false;
boolean choking = true;
// Interested and choked describes whether the other side is
// interested in us or choked us.
boolean interested = false;
boolean choked = true;
// Package local for use by Peer.
long downloaded;
long uploaded;
BitField bitfield;
// Package local for use by Peer.
final PeerConnectionIn in;
final PeerConnectionOut out;
// Outstanding request
private final List<Request> outstandingRequests = new ArrayList<Request>();
private Request lastRequest = null;
// If we have te resend outstanding requests (true after we got choked).
private boolean resend = false;
private final static int MAX_PIPELINE = 5;
private final static int PARTSIZE = 16384; // 16K
PeerState (Peer peer, PeerListener listener, MetaInfo metainfo,
PeerConnectionIn in, PeerConnectionOut out)
{
this.peer = peer;
this.listener = listener;
this.metainfo = metainfo;
this.in = in;
this.out = out;
}
// NOTE Methods that inspect or change the state synchronize (on this).
void keepAliveMessage ()
{
log.log(Level.FINEST, peer + " rcv alive");
/* XXX - ignored */
}
void chokeMessage (boolean choke)
{
log.log(Level.FINEST, peer + " rcv " + (choke ? "" : "un") + "choked");
choked = choke;
if (choked) {
resend = true;
}
listener.gotChoke(peer, choke);
if (!choked && interesting) {
request();
}
}
void interestedMessage (boolean interest)
{
log.log(Level.FINEST, peer + " rcv " + (interest ? "" : "un")
+ "interested");
interested = interest;
listener.gotInterest(peer, interest);
}
void haveMessage (int piece)
{
log.log(Level.FINEST, peer + " rcv have(" + piece + ")");
// Sanity check
if (piece < 0 || piece >= metainfo.getPieces()) {
// XXX disconnect?
log.log(Level.FINER, "Got strange 'have: " + piece
+ "' message from " + peer);
return;
}
synchronized (this) {
// Can happen if the other side never send a bitfield message.
if (bitfield == null) {
bitfield = new BitField(metainfo.getPieces());
}
bitfield.set(piece);
}
if (listener.gotHave(peer, piece)) {
setInteresting(true);
}
}
void bitfieldMessage (byte[] bitmap)
{
synchronized (this) {
log.log(Level.FINEST, peer + " rcv bitfield");
if (bitfield != null) {
// XXX - Be liberal in what you accept?
log.log(Level.FINER, "Got unexpected bitfield message from "
+ peer);
return;
}
// XXX - Check for weird bitfield and disconnect?
bitfield = new BitField(bitmap, metainfo.getPieces());
}
setInteresting(listener.gotBitField(peer, bitfield));
}
void requestMessage (int piece, int begin, int length)
throws IOException
{
log.log(Level.FINEST, peer + " rcv request(" + piece + ", " + begin
+ ", " + length + ") ");
if (choking) {
log.log(Level.FINER, "Request received, but choking " + peer);
return;
}
// Sanity check
if (piece < 0 || piece >= metainfo.getPieces() || begin < 0
|| begin > metainfo.getPieceLength(piece) || length <= 0
|| length > 4 * PARTSIZE) {
// XXX - Protocol error -> disconnect?
log.log(Level.FINER, "Got strange 'request: " + piece + ", "
+ begin + ", " + length + "' message from " + peer);
return;
}
byte[] pieceBytes = listener.gotRequest(peer, piece);
if (pieceBytes == null) {
// XXX - Protocol error-> diconnect?
log.log(Level.FINER, "Got request for unknown piece: " + piece);
return;
}
// More sanity checks
if (begin >= pieceBytes.length || begin + length > pieceBytes.length) {
// XXX - Protocol error-> disconnect?
log.log(Level.FINER, "Got out of range 'request: " + piece + ", "
+ begin + ", " + length + "' message from " + peer);
return;
}
log.log(Level.FINEST, "Sending (" + piece + ", " + begin + ", "
+ length + ")" + " to " + peer);
out.sendPiece(piece, begin, length, pieceBytes);
// Tell about last subpiece delivery.
if (begin + length == pieceBytes.length) {
log.log(Level.FINEST, "Send p" + piece + " " + peer);
}
}
/**
* Called when some bytes have left the outgoing connection. XXX - Should
* indicate whether it was a real piece or overhead.
*/
void uploaded (int size)
{
uploaded += size;
listener.uploaded(peer, size);
}
/**
* Called when a partial piece request has been handled by PeerConnectionIn.
*/
void pieceMessage (Request req)
throws IOException
{
int size = req.len;
downloaded += size;
listener.downloaded(peer, size);
// Last chunk needed for this piece?
if (getFirstOutstandingRequest(req.piece) == -1) {
if (listener.gotPiece(peer, req.piece, req.bs)) {
log.log(Level.FINEST, "Got " + req.piece + ": " + peer);
} else {
log.log(Level.FINEST, "Got BAD " + req.piece + " from " + peer);
// XXX ARGH What now !?!
downloaded = 0;
}
}
}
synchronized private int getFirstOutstandingRequest (int piece)
{
for (int i = 0; i < outstandingRequests.size(); i++) {
if ((outstandingRequests.get(i)).piece == piece) {
return i;
}
}
return -1;
}
/**
* Called when a piece message is being processed by the incoming
* connection. Returns null when there was no such request. It also
* requeues/sends requests when it thinks that they must have been lost.
*/
Request getOutstandingRequest (int piece, int begin, int length)
{
log.log(Level.FINEST, "getChunk(" + piece + "," + begin + "," + length
+ ") " + peer);
int r = getFirstOutstandingRequest(piece);
// Unrequested piece number?
if (r == -1) {
log.log(Level.FINER, "Unrequested 'piece: " + piece + ", " + begin
+ ", " + length + "' received from " + peer);
downloaded = 0; // XXX - punishment?
return null;
}
// Lookup the correct piece chunk request from the list.
Request req;
synchronized (this) {
req = outstandingRequests.get(r);
while (req.piece == piece && req.off != begin
&& r < outstandingRequests.size() - 1) {
r++;
req = outstandingRequests.get(r);
}
// Something wrong?
if (req.piece != piece || req.off != begin || req.len != length) {
log.log(Level.FINER, "Unrequested or unneeded 'piece: " + piece
+ ", " + begin + ", " + length + "' received from " + peer);
downloaded = 0; // XXX - punishment?
return null;
}
// Report missing requests.
if (r != 0) {
String errmsg = "Some requests dropped, got " + req +
", wanted:";
for (int i = 0; i < r; i++) {
Request dropReq = outstandingRequests.remove(0);
outstandingRequests.add(dropReq);
// We used to rerequest the missing chunks but that mostly
// just confuses the other side. So now we just keep
// waiting for them. They will be rerequested when we get
// choked/unchoked again.
/*
* if (!choked) out.sendRequest(dropReq);
*/
errmsg += " " + dropReq;
}
errmsg += " " + peer;
log.log(Level.FINER, errmsg);
}
outstandingRequests.remove(0);
}
// Request more if necessary to keep the pipeline filled.
addRequest();
return req;
}
void cancelMessage (int piece, int begin, int length)
{
log.log(Level.FINEST, "Got cancel message (" + piece + ", " + begin
+ ", " + length + ")");
out.cancelRequest(piece, begin, length);
}
void unknownMessage (int type, byte[] bs)
{
log.log(Level.WARNING, "Ignoring unknown message type: " + type
+ " length: " + bs.length);
}
void havePiece (int piece)
{
log.log(Level.FINEST, "Tell " + peer + " havePiece(" + piece + ")");
synchronized (this) {
// Tell the other side that we are no longer interested in any of
// the outstanding requests for this piece.
if (lastRequest != null && lastRequest.piece == piece) {
lastRequest = null;
}
Iterator it = outstandingRequests.iterator();
while (it.hasNext()) {
Request req = (Request)it.next();
if (req.piece == piece) {
it.remove();
// Send cancel even when we are choked to make sure that it
// is
// really never ever send.
out.sendCancel(req);
}
}
}
// Tell the other side that we really have this piece.
out.sendHave(piece);
// Request something else if necessary.
addRequest();
synchronized (this) {
// Is the peer still interesting?
if (lastRequest == null) {
setInteresting(false);
}
}
}
// Starts or resumes requesting pieces.
private void request ()
{
// Are there outstanding requests that have to be resend?
if (resend) {
out.sendRequests(outstandingRequests);
resend = false;
}
// Add/Send some more requests if necessary.
addRequest();
}
/**
* Adds a new request to the outstanding requests list.
*/
private void addRequest ()
{
boolean more_pieces = true;
while (more_pieces) {
synchronized (this) {
more_pieces = outstandingRequests.size() < MAX_PIPELINE;
}
// We want something and we don't have outstanding requests?
if (more_pieces && lastRequest == null) {
more_pieces = requestNextPiece();
} else if (more_pieces) // We want something
{
int pieceLength;
boolean isLastChunk;
synchronized (this) {
pieceLength = metainfo.getPieceLength(lastRequest.piece);
isLastChunk = lastRequest.off + lastRequest.len == pieceLength;
}
// Last part of a piece?
if (isLastChunk) {
more_pieces = requestNextPiece();
} else {
synchronized (this) {
int nextPiece = lastRequest.piece;
int nextBegin = lastRequest.off + PARTSIZE;
byte[] bs = lastRequest.bs;
int maxLength = pieceLength - nextBegin;
int nextLength = maxLength > PARTSIZE ? PARTSIZE
: maxLength;
Request req = new Request(nextPiece, bs, nextBegin,
nextLength);
outstandingRequests.add(req);
if (!choked) {
out.sendRequest(req);
}
lastRequest = req;
}
}
}
}
//log.log(Level.FINEST, peer + " requests " + outstandingRequests);
}
// Starts requesting first chunk of next piece. Returns true if
// something has been added to the requests, false otherwise.
private boolean requestNextPiece ()
{
// Check that we already know what the other side has.
if (bitfield != null) {
int nextPiece = listener.wantPiece(peer, bitfield);
log.log(Level.FINEST, peer + " want piece " + nextPiece);
synchronized (this) {
if (nextPiece != -1
&& (lastRequest == null || lastRequest.piece != nextPiece)) {
int piece_length = metainfo.getPieceLength(nextPiece);
byte[] bs = new byte[piece_length];
int length = Math.min(piece_length, PARTSIZE);
Request req = new Request(nextPiece, bs, 0, length);
outstandingRequests.add(req);
if (!choked) {
out.sendRequest(req);
}
lastRequest = req;
return true;
}
}
}
return false;
}
synchronized void setInteresting (boolean interest)
{
log.log(Level.FINEST, peer + " setInteresting(" + interest + ")");
if (interest != interesting) {
interesting = interest;
out.sendInterest(interest);
if (interesting && !choked) {
request();
}
}
}
synchronized void setChoking (boolean choke)
{
log.log(Level.FINEST, peer + " setChoking(" + choke + ")");
if (choking != choke) {
choking = choke;
out.sendChoke(choke);
}
}
/** The Java logger used to process our log events. */
protected static final Logger log = Logger.getLogger("org.klomp.snark.peer");
}