Java tutorial
/** * Copyright (C) 2011-2012 Turn, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.turn.ttorrent.client; import com.google.common.base.Function; import com.google.common.collect.Maps; import com.turn.ttorrent.client.io.PeerMessage; import com.turn.ttorrent.client.io.PeerServer; import com.turn.ttorrent.client.peer.Instrumentation; import com.turn.ttorrent.client.peer.PeerActivityListener; import com.turn.ttorrent.client.peer.PeerConnectionListener; import com.turn.ttorrent.client.peer.PeerExistenceListener; import com.turn.ttorrent.client.peer.PeerHandler; import com.turn.ttorrent.client.peer.PieceHandler; import com.turn.ttorrent.client.peer.Rate; import com.turn.ttorrent.client.peer.RateComparator; import com.turn.ttorrent.protocol.TorrentUtils; import com.turn.ttorrent.protocol.tracker.Peer; import com.turn.ttorrent.tracker.client.PeerAddressProvider; import io.netty.channel.Channel; import io.netty.util.internal.PlatformDependent; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicIntegerArray; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.CheckForNull; import javax.annotation.Nonnegative; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manages swarm-level coordination as a periodically-executed Runnable. * * <p> * This class implements several interfaces that all share a 1:1 ratio for * object lifetime. Known peers in the swarm are tracked with a simple map, but * connected peers have {@link PeerHandler} objects created for them, and these * objects allow direct control of the wire protocol. Most of the interfaces * implemented by this class are then used by PeerHandlers to learn about the * torrent status, the peer's addresses, and other peers, as well as to handle * various peer and piece state changes. Pieces of the torrent are similarly * tracked in a simple array, but in progress pieces have {@link PieceHandler} * objects allocated that handle reading and writing blocks on the wire. The * SwarmHandler is used by PieceHandlers to maintain piece consistency. * </p> * * <p> * The SwarmHandler for a torrent is also used by the {@link PeerClent} as a * listener that handles new connections and by the {@link TrackerHandler} s a * place to register peers returned by a tracker. * </p> * * @author mpetazzoni */ public class SwarmHandler implements Runnable, PeerAddressProvider, PeerPieceProvider, PeerExistenceListener, PeerConnectionListener, PeerActivityListener { private static final Logger LOG = LoggerFactory.getLogger(SwarmHandler.class); private static class PeerInformation { // Yes, the reference is volatile, not the data. @CheckForNull private volatile byte[] remotePeerId; private volatile long keepAliveTime; private volatile long reconnectTime; public void setReconnectTime(@Nonnull Random r, long reconnectTime) { // 5000 estimates that we won't have more than 5 valid IPs for a given target. this.reconnectTime = reconnectTime + r.nextInt(5000); } @Override public String toString() { return "RemotePeerId=" + TorrentUtils.toHexOrNull(remotePeerId) + ", reconnectTime=" + reconnectTime; } } /** Peers unchoking frequency, in seconds. Current BitTorrent specification * recommends 10 seconds to avoid choking fibrilation. */ private static final long UNCHOKE_DELAY = TimeUnit.SECONDS.toMillis(10); /** Optimistic unchokes are done every 2 loop iterations, i.e. every * 2*UNCHOKING_FREQUENCY seconds. */ private static final long OPTIMISTIC_UNCHOKE_DELAY = TimeUnit.SECONDS.toMillis(32); private static final int MAX_DOWNLOADERS_UNCHOKE = 4; private static final long RECONNECT_DELAY_TEMPORARY = TimeUnit.MINUTES.toMillis(1); private static final long RECONNECT_DELAY_PERMANENT = TimeUnit.MINUTES.toMillis(10); /** End-game trigger ratio. * * <p> * End-game behavior (requesting already requested pieces from available * and ready peers to try to speed-up the end of the transfer) will only be * enabled when the ratio of completed pieces over total pieces in the * torrent is over this value. * </p> */ private static final float END_GAME_COMPLETION_RATIO = 0.95f; private final TorrentHandler torrent; // Keys are InetSocketAddress or HexPeerId private final ConcurrentMap<SocketAddress, PeerInformation> knownPeers = PlatformDependent .newConcurrentHashMap(); private final ConcurrentMap<String, PeerHandler> connectedPeers = PlatformDependent.newConcurrentHashMap(); private final AtomicLong uploaded = new AtomicLong(0); private final AtomicLong downloaded = new AtomicLong(0); private final AtomicIntegerArray availablePieces; @GuardedBy("lock") private final Set<PieceHandler.AnswerableRequestMessage> partialPieces = new HashSet<PieceHandler.AnswerableRequestMessage>(); // We only care about global rarest pieces for peer selection or opportunistic unchoking. // private final BitSet rarestPieces; // private int rarestPiecesAvailability = 0; @GuardedBy("future") private Future<?> future; @GuardedBy("lock") private long unchokeTime = 0; @GuardedBy("lock") private long optimisticUnchokeTime = 0; private long tickTime = 0; private final Object lock = new Object(); SwarmHandler(@Nonnull TorrentHandler torrent) { this.torrent = torrent; this.availablePieces = new AtomicIntegerArray(torrent.getPieceCount()); // this.rarestPieces = new BitSet(torrent.getPieceCount()); } @Nonnull public Client getClient() { return torrent.getClient(); } @Override public byte[] getLocalPeerId() { return getClient().getEnvironment().getLocalPeerId(); } @Override public String getLocalPeerName() { return getClient().getEnvironment().getLocalPeerName(); } @Override public Set<? extends SocketAddress> getLocalAddresses() { PeerServer server = getClient().getPeerServer(); // This can happen if we try to seed PEX before calling Client.start(). if (server == null) return Collections.emptySet(); return server.getLocalAddresses(); } @Override public Instrumentation getInstrumentation() { return getClient().getEnvironment().getInstrumentation(); } @Nonnull private Random getRandom() { return getClient().getEnvironment().getRandom(); } @Nonnegative public int getPeerCount() { return knownPeers.size(); } @Override public Map<? extends SocketAddress, ? extends byte[]> getPeers() { return Maps.transformValues(knownPeers, new Function<PeerInformation, byte[]>() { @Override public byte[] apply(PeerInformation input) { return input.remotePeerId; } }); } private static boolean isInetAddress(@Nonnull SocketAddress socketAddress, @Nonnull Class<? extends InetAddress> type) { if (!(socketAddress instanceof InetSocketAddress)) return false; InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; return type.isInstance(inetSocketAddress.getAddress()); } // @Nonnull private void addPeer(@Nonnull SocketAddress peerAddress, @CheckForNull byte[] peerId, long now) { // if (!isInetAddress(peerAddress, Inet4Address.class)) return; PeerInformation peerInformation = new PeerInformation(); peerInformation.setReconnectTime(getRandom(), now); PUT: { PeerInformation tmp = knownPeers.putIfAbsent(peerAddress, peerInformation); if (tmp != null) peerInformation = tmp; } if (peerId != null) peerInformation.remotePeerId = peerId; // TODO: Update stats about 'reported', 'connected', etc. // return peerInformation; } @Override public void addPeers(@Nonnull Map<? extends SocketAddress, ? extends byte[]> peers, @Nonnull String source) { if (LOG.isDebugEnabled()) LOG.debug("{}: Adding peers from {}: {}", new Object[] { getLocalPeerName(), source, peers }); // PeerServer server = getClient().getPeerServer(); Set<? extends SocketAddress> localAddresses = getLocalAddresses(); long now = System.currentTimeMillis(); for (Map.Entry<? extends SocketAddress, ? extends byte[]> e : peers.entrySet()) { SocketAddress remoteAddress = e.getKey(); if (false && localAddresses.contains(remoteAddress)) { // TODO: Probably ignore silently as it's bound to happen and it's not actionable. LOG.warn("Attempted to add local address " + remoteAddress + " to known peer set."); continue; } addPeer(remoteAddress, e.getValue(), now); } // run(); // If you want very low latency, call run() manually after calling this. } @Nonnull public Iterable<? extends PeerHandler> getConnectedPeers() { return connectedPeers.values(); } @Nonnegative public int getConnectedPeerCount() { return connectedPeers.size(); } /** * Get the number of bytes uploaded for this torrent. */ @Nonnegative public long getUploaded() { return uploaded.get(); } /** * Get the number of bytes downloaded for this torrent. * * <p> * <b>Note:</b> this could be more than the torrent's length, and should * not be used to determine a completion percentage. * </p> */ @Nonnegative public long getDownloaded() { return downloaded.get(); } @Nonnegative public int getAvailablePieceCount() { int count = 0; for (int i = 0; i < torrent.getPieceCount(); i++) { if (this.availablePieces.get(i) > 0) count++; } return count; } public int setAvailablePiece(@Nonnegative int piece, boolean available) { if (available) { return availablePieces.incrementAndGet(piece); } else { // Implement an unsigned CAS. for (;;) { int current = availablePieces.get(piece); if (current <= 0) return 0; int next = current - 1; if (availablePieces.compareAndSet(piece, current, next)) return next; } } } /** * Return a BitSet describing the currently requested pieces. */ @Nonnull public BitSet getRequestedPieces() { BitSet requestedPieces = new BitSet(torrent.getPieceCount()); for (PeerHandler peerHandler : connectedPeers.values()) { for (PieceHandler.AnswerableRequestMessage request : peerHandler.getRequestsSent()) { requestedPieces.set(request.getPiece()); } } return requestedPieces; } @Nonnegative public int getRequestedPieceCount() { return getRequestedPieces().cardinality(); } /* public boolean isRequestedPiece(int index) { synchronized (lock) { return requestedPieces.get(index); } } */ private void andNotRequestedPieces(@Nonnull BitSet b) { for (PeerHandler peerHandler : connectedPeers.values()) { for (PieceHandler.AnswerableRequestMessage request : peerHandler.getRequestsSent()) { b.clear(request.getPiece()); } } } /** * Connect to the given peer and perform the BitTorrent handshake. * * <p> * Submits an asynchronous connection task to the outbound connections * executor to connect to the given peer. * </p> * * @param peer The peer to connect to. */ public void connect(SocketAddress address) { if (LOG.isDebugEnabled()) LOG.debug("{}: Attempting to connect to {} for {}", new Object[] { getLocalPeerName(), address, torrent }); getClient().getPeerClient().connect(this, torrent.getInfoHash(), address); } public void start() { synchronized (lock) { long ms = Rate.INTERVAL_MS; ms = Math.min(ms, UNCHOKE_DELAY); ms = Math.min(ms, OPTIMISTIC_UNCHOKE_DELAY); ms = Math.min(ms, RECONNECT_DELAY_TEMPORARY); future = getClient().getEnvironment().getEventService().scheduleWithFixedDelay(this, 0, ms, TimeUnit.MILLISECONDS); } } public void stop() { synchronized (lock) { if (future != null) { future.cancel(true); future = null; } } } // TODO: Periodic / step function. @Override public void run() { // Stopwatch stopwatch = Stopwatch.createStarted(); if (LOG.isTraceEnabled()) LOG.trace("{}: Run: peers={}, connected={}, completed={}/{}", new Object[] { getLocalPeerName(), knownPeers.keySet(), getConnectedPeers(), torrent.getCompletedPieceCount(), torrent.getPieceCount() }); boolean unchoke = false; boolean optimisticUnchoke = false; boolean tick = false; long now = System.currentTimeMillis(); synchronized (lock) { if (unchokeTime + UNCHOKE_DELAY < now) { unchokeTime = now; unchoke = true; } if (optimisticUnchokeTime + OPTIMISTIC_UNCHOKE_DELAY < now) { optimisticUnchokeTime = now; optimisticUnchoke = true; } if (tickTime + Rate.INTERVAL_MS < now) { tickTime = now; tick = true; } } if (!torrent.isComplete()) { // Attempt to connect to the peer if and only if: // - We're not already connected or connecting to it; // - We're not a seeder (we leave the responsibility // of connecting to peers that need to download // something). for (Map.Entry<SocketAddress, PeerInformation> e : knownPeers.entrySet()) { PeerInformation peerInformation = e.getValue(); if (LOG.isTraceEnabled()) LOG.trace("{}: At {}, considering {} -> {}", new Object[] { getLocalPeerName(), now, e.getKey(), peerInformation }); if (peerInformation.reconnectTime > now) continue; byte[] remotePeerId = peerInformation.remotePeerId; if (remotePeerId != null) { if (connectedPeers.containsKey(TorrentUtils.toHex(remotePeerId))) continue; if (Arrays.equals(remotePeerId, getLocalPeerId())) continue; } peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); connect(e.getKey()); } } if (unchoke) unchokePeers(optimisticUnchoke); for (PeerHandler peer : getConnectedPeers()) { try { // This is the call which is likely to cause the most trouble. peer.run("swarm tick"); } catch (IOException e) { LOG.error(getLocalPeerName() + ": Peer " + peer + " threw.", e); } if (tick) peer.tick(); } // LOG.debug("{}: Swarm tick took {}", getLocalPeerName(), stopwatch); } /** * Retrieve a peer comparator. * * <p> * Returns a peer comparator based on either the download rate or the * upload rate of each peer depending on our state. While sharing, we rely * on the download rate we get from each peer. When our download is * complete and we're only seeding, we use the upload rate instead. * </p> * * @return A SharingPeer comparator that can be used to sort peers based on * the download or upload rate we get from them. */ @Nonnull private Comparator<PeerHandler> getPeerRateComparator() { switch (torrent.getState()) { case SHARING: return new RateComparator.DLRateComparator(); case SEEDING: return new RateComparator.ULRateComparator(); default: throw new IllegalStateException( "Client is neither sharing nor " + "seeding, we shouldn't be comparing peers at this point."); } } /** * Unchoke connected peers. * * <p> * This is one of the "clever" places of the BitTorrent client. Every * OPTIMISTIC_UNCHOKING_FREQUENCY seconds, we decide which peers should be * unchoked and authorized to grab pieces from us. * </p> * * <p> * Reciprocation (tit-for-tat) and upload capping is implemented here by * carefully choosing which peers we unchoke, and which peers we choke. * </p> * * <p> * The four peers with the best download rate and are interested in us get * unchoked. This maximizes our download rate as we'll be able to get data * from the four "best" peers quickly, while allowing these peers to * download from us and thus reciprocate their generosity. * </p> * * <p> * Peers that have a better download rate than these four downloaders but * are not interested get unchoked too, we want to be able to download from * them to get more data more quickly. If one becomes interested, it takes * a downloader's place as one of the four top downloaders (i.e. we choke * the downloader with the worst upload rate). * </p> * * @param optimistic Whether to perform an optimistic unchoke as well. */ private void unchokePeers(boolean optimistic) { // Build a set of all connected peers, we don't care about peers we're // not connected to. List<PeerHandler> candidates = new ArrayList<PeerHandler>(); for (PeerHandler peer : getConnectedPeers()) { // TODO: Panic-check that it's still connected? if (peer.isInterested()) candidates.add(peer); else peer.choke(); } if (LOG.isTraceEnabled()) LOG.trace("{}: Running unchokePeers() on {} connected peers.", getLocalPeerName(), candidates.size()); // Collections.shuffle(candidates); // Make the sort unstable, so if we have no downloaders, we select at random. Collections.sort(candidates, getPeerRateComparator()); // LOG.info("{}: Candidates are {}", getLocalPeerName(), candidates); int downloaders = 0; // We're interested in the top downloaders first, so use a descending set. int i = 0; for (/* */; i < candidates.size(); i++) { if (++downloaders > MAX_DOWNLOADERS_UNCHOKE) break; PeerHandler peer = candidates.get(i); peer.unchoke(); } // Actually choke all chosen peers (if any), except the eventual // optimistic unchoke. int nchoked = candidates.size() - i; if (nchoked > 0) { int unchoke; if (optimistic) { unchoke = i + getRandom().nextInt(nchoked); if (LOG.isTraceEnabled()) { PeerHandler peer = candidates.get(unchoke); LOG.trace("Optimistic unchoke of {}.", peer); } } else { unchoke = -1; } for (/* */; i < candidates.size(); i++) { if (i == unchoke) candidates.get(i).unchoke(); else candidates.get(i).choke(); } } } /** * Computes the set of rarest pieces from the interesting set. */ private int computeRarestPieces(@Nonnull BitSet rarest, @Nonnull BitSet interesting) { rarest.clear(); int rarestAvailability = Integer.MAX_VALUE; for (int i = interesting.nextSetBit(0); i >= 0; i = interesting.nextSetBit(i + 1)) { int availability = availablePieces.get(i); // This looks weird, but: The bit wouldn't be set in interesting if availability was 0. // So we got a miscount somewhere (entirely possible) and we patch up here. if (availability == 0) availability = 1; // Now, !interesting.isEmpty() -> !rarest.isEmpty() if (availability > rarestAvailability) continue; if (availability < rarestAvailability) { rarestAvailability = availability; rarest.clear(); } rarest.set(i); } return rarestAvailability; } @Override public int getPieceCount() { return torrent.getPieceCount(); } @Override public int getPieceLength(int index) { return torrent.getPieceLength(index); } @Override public int getBlockLength() { return torrent.getBlockLength(); } @Override public BitSet getCompletedPieces() { return torrent.getCompletedPieces(); } @Override public boolean isCompletedPiece(int index) { return torrent.isCompletedPiece(index); } @Override public void andNotCompletedPieces(BitSet out) { torrent.andNotCompletedPieces(out); } @Override public Iterable<PieceHandler.AnswerableRequestMessage> getNextPieceHandler(@Nonnull PeerHandler peer, @Nonnull BitSet peerInteresting) { int peerAvailable = peer.getAvailablePieceCount(); // LOG.debug("Peer interesting is {}", peerInteresting); // TODO: We hold this lock for a LONG time. :-( // I'm fairly sure our lock acquisition order is peer then torrent. // We can't drop the lock earlier, else two peers will get the // same DownloadingPiece, and we don't reference those. synchronized (lock) { PARTIAL: { List<PieceHandler.AnswerableRequestMessage> piece = new ArrayList<PieceHandler.AnswerableRequestMessage>(); Iterator<PieceHandler.AnswerableRequestMessage> it = partialPieces.iterator(); while (it.hasNext()) { if (piece.size() > 20) break; PieceHandler.AnswerableRequestMessage request = it.next(); if (peerInteresting.get(request.getPiece())) { // An endgame might have requested it elsewhere. if (!isCompletedPiece(request.getPiece())) { if (LOG.isDebugEnabled()) LOG.debug("{}: Peer {} retrying request {}", new Object[] { getLocalPeerName(), peer, request }); piece.add(request); } it.remove(); } } // LOG.info("Looking for partials generated " + piece); if (!piece.isEmpty()) return piece; } // TODO: Should this be before or after PARTIAL? BitSet interesting = (BitSet) peerInteresting.clone(); this.andNotRequestedPieces(interesting); // If we didn't find interesting pieces, we need to check if we're in // an end-game situation. If yes, we request an already requested piece // to try to speed up the end. if (interesting.isEmpty()) { if (torrent.getCompletedPieceCount() < END_GAME_COMPLETION_RATIO * torrent.getPieceCount()) { if (LOG.isTraceEnabled()) LOG.trace("{}: Not far along enough to warrant end-game mode.", getLocalPeerName()); return null; } interesting = (BitSet) peerInteresting.clone(); torrent.andNotCompletedPieces(interesting); if (LOG.isTraceEnabled()) LOG.trace("{}: Possible end-game, we're about to request a piece " + "that was already requested from another peer.", getLocalPeerName()); } if (interesting.isEmpty()) { if (LOG.isTraceEnabled()) LOG.trace("{}: No interesting piece from {}!", getLocalPeerName(), peer); return null; } BitSet rarestPieces = new BitSet(interesting.length()); computeRarestPieces(rarestPieces, interesting); // Since interesting is nonempty, rarestPieces should be nonempty. // We can violate that if we have a miscount of 0 in availablePieces. // Pick a random piece from the rarest pieces from this peer. int rarestIndex = getRandom().nextInt(rarestPieces.cardinality()); SEARCH: { // This loop will NEVER terminate "normally" because rarestIndex < rarestPieces.cardinality(); for (int i = rarestPieces.nextSetBit(0); i >= 0; i = rarestPieces.nextSetBit(i + 1)) { if (rarestIndex-- == 0) { rarestIndex = i; break SEARCH; } } // NOTREACHED LOG.error("{}: No rare piece from {}!", getLocalPeerName(), peer); return null; } if (LOG.isTraceEnabled()) LOG.trace("{}: Peer {} has {}/{}/{} interesting piece(s); requesting {}, requests {}", new Object[] { getLocalPeerName(), peer, interesting.cardinality(), peerAvailable, torrent.getPieceCount(), rarestIndex, getRequestedPieces() }); // TODO: We don't keep track of these, so it is possible to have more than one // PieceHandler for a given piece. We make some attempt with rejected or timed out // requests, but it isn't great. return new PieceHandler(/* this, */this, rarestIndex); } } @Override public int addRequestTimeout(Iterable<? extends PieceHandler.AnswerableRequestMessage> requests) { int count = 0; synchronized (lock) { for (PieceHandler.AnswerableRequestMessage request : requests) { if (!isCompletedPiece(request.getPiece())) { partialPieces.add(request); count++; } } } return count; } @Nonnegative private int getPartialPieceCount() { synchronized (lock) { return partialPieces.size(); } } private void ioBlock(@Nonnull ByteBuffer block, @Nonnegative int piece, @Nonnegative int offset, boolean completed) throws IOException { int rawLength = torrent.getTorrent().getPieceLength(piece); if (offset + block.remaining() > rawLength) throw new IllegalArgumentException("Offset " + offset + "+" + block.remaining() + " too large for piece " + piece + " of length " + rawLength); if (completed && !isCompletedPiece(piece)) throw new IllegalArgumentException("Attempt to read piece " + piece + " which is not complete."); } @Override public void readBlock(ByteBuffer block, int piece, int offset) throws IOException { ioBlock(block, piece, offset, true); long rawOffset = torrent.getTorrent().getPieceOffset(piece) + offset; torrent.getBucket().read(block, rawOffset); } @Override public void writeBlock(ByteBuffer block, int piece, int offset) throws IOException { ioBlock(block, piece, offset, false); long rawOffset = torrent.getTorrent().getPieceOffset(piece) + offset; torrent.getBucket().write(block, rawOffset); } @Override public boolean validateBlock(ByteBuffer block, int piece) throws IOException { // LOG.trace("Validating data for {}...", this); return torrent.getTorrent().isPieceValid(piece, block); } /** PeerConnectionListener handler(s). ********************************/ /** * Retrieves a {@link PeerHandler} object from the given peer specification. * * <p> * This function tries to retrieve an existing peer object based on the * provided peer specification or otherwise instantiates a new one and adds * it to our peer repository. * </p> * * This method takes two {@link Nonnull} arguments instead of a {@link Peer}, * because Peer has a {@link CheckForNull} on {@link Peer#getPeerId()}. */ @Override public PeerHandler handlePeerConnectionCreated(Channel channel, byte[] remotePeerId, byte[] remoteReserved) { SocketAddress remoteAddress = channel.remoteAddress(); // This is almost always an ephemeral outgoing port so don't add it here. // If the peer supports PeerExtendedMessage.HandshakeMessage, we will get its id then. // addPeer(remoteAddress, remotePeerId, System.currentTimeMillis()); PeerInformation peerInformation = knownPeers.get(remoteAddress); if (peerInformation != null) peerInformation.remotePeerId = remotePeerId; if (Arrays.equals(remotePeerId, getClient().getLocalPeerId())) throw new IllegalArgumentException("Cannot connect to self."); String remoteHexPeerId = TorrentUtils.toHex(remotePeerId); PeerHandler peerHandler = connectedPeers.get(remoteHexPeerId); if (peerHandler != null) { if (LOG.isTraceEnabled()) LOG.trace("{}: Found existing peer for {}: {}.", new Object[] { getLocalPeerName(), remoteAddress, peerHandler }); return null; } peerHandler = new PeerHandler(channel, remotePeerId, remoteReserved, this, this, this, this, this); if (LOG.isTraceEnabled()) LOG.trace("{}: Created new peer: {}.", getLocalPeerName(), peerHandler); return peerHandler; } /** * Chooses, deterministically, between two PeerHandlers, such that both * ends of a connection will make the same deterministic choice. * * Attempts to prefer link-local IPv6 addresses. */ private static class PeerConnectionComparator implements Comparator<PeerHandler> { public static final PeerConnectionComparator INSTANCE = new PeerConnectionComparator(); private static int score(@Nonnull InetAddress a) { if (a.isLinkLocalAddress()) return 2; // Most beloved, if we can get it. if (a.isLoopbackAddress()) return 10; // Only if we can't get something better. if (a.isAnyLocalAddress()) return 100; // Avoid. if (a.isMulticastAddress()) return 200; // Never. return 4; // Regular address. } private static int compare(@Nonnull InetAddress a1, @Nonnull InetAddress a2) { byte[] b1 = a1.getAddress(); byte[] b2 = a2.getAddress(); // Use the longer address. int cmp = -Integer.compare(b1.length, b2.length); if (cmp != 0) return cmp; // Use the lower address. for (int i = 0; i < b1.length; i++) { cmp = b2[i] - b1[i]; if (cmp != 0) return cmp; } return 0; } @Nonnull private static InetAddress choose(@Nonnull InetAddress a1, @Nonnull InetAddress a2) { int cmp = compare(a1, a2); return cmp < 0 ? a1 : a2; } @Override public int compare(PeerHandler o1, PeerHandler o2) { SocketAddress s1 = o1.getRemoteAddress(); SocketAddress s2 = o2.getRemoteAddress(); if (!(s1 instanceof InetSocketAddress)) { if (s2 instanceof InetSocketAddress) return 1; return 0; } else if (!(s2 instanceof InetSocketAddress)) { return -1; } InetAddress a1 = ((InetSocketAddress) s1).getAddress(); InetAddress a2 = ((InetSocketAddress) s2).getAddress(); // Longer addresses are preferred: IPv6. int cmp = -Integer.compare(a1.getAddress().length, a2.getAddress().length); if (cmp != 0) return cmp; cmp = Integer.compare(score(a1), score(a2)); if (cmp != 0) return cmp; // OK, we ran out of smart ideas. Let's do something very deterministic. InetAddress e1 = choose(a1, ((InetSocketAddress) o1.getLocalAddress()).getAddress()); InetAddress e2 = choose(a2, ((InetSocketAddress) o2.getLocalAddress()).getAddress()); return compare(e1, e2); } } /** * Handle a new peer connection. * * <p> * This handler is called once the connection has been successfully * established and the handshake exchange made. This generally simply means * binding the peer to the socket, which will put in place the communication * logic with this peer. * </p> * * @param peer The connected remote peer. Note * that if the peer somehow rejected our handshake reply, this socket might * very soon get closed, but this is handled down the road. * * @see PeerHandler */ @Override public void handlePeerConnectionReady(PeerHandler peer) { try { if (LOG.isDebugEnabled()) LOG.debug("{}: New peer connection with {} [{}/{}].", new Object[] { getLocalPeerName(), peer, getConnectedPeerCount(), getPeerCount() }); peer.getRemoteAddress(); for (;;) { // See whether we are already connected. PeerHandler prev = connectedPeers.putIfAbsent(peer.getHexRemotePeerId(), peer); // Simple success. if (prev == null) break; // Some weird race which is simple success, but can't happen. if (prev == peer) break; // If so, choose a connection deterministically. int cmp = PeerConnectionComparator.INSTANCE.compare(prev, peer); // We didn't like the new connection. if (cmp <= 0) { if (LOG.isDebugEnabled()) LOG.debug("{}: Closing duplicate peer connection {} : {} [{}/{}]", new Object[] { getLocalPeerName(), peer, prev, getConnectedPeerCount(), getPeerCount() }); peer.close("duplicate connection"); return; } // Try to use the new connection. // TODO: Do we just keep the old (active) one? if (connectedPeers.replace(peer.getHexRemotePeerId(), prev, peer)) { if (LOG.isDebugEnabled()) LOG.debug("{}: Closing superceded peer connection {} : {} [{}/{}]", new Object[] { getLocalPeerName(), prev, peer, getConnectedPeerCount(), getPeerCount() }); prev.close("superceded connection"); break; } // We preferred the new connection, but replace() failed. Try again. } // Give the peer a chance to send a bitfield message. peer.run("new connection"); } catch (Exception e) { LOG.warn("Could not handle new peer connection " + "with {}: {}", peer, e.getMessage()); } } /** * Handle a failed peer connection. * * <p> * If an outbound connection failed (could not connect, invalid handshake, * etc.), remove the peer from our known peers. * </p> * * @param peer The peer we were trying to connect with. * @param cause The exception encountered when connecting with the peer. */ @Override public void handlePeerConnectionFailed(SocketAddress remoteAddress, Throwable cause) { String reason = (cause == null) ? "(cause not specified)" : cause.getMessage(); LOG.warn("{}: Could not connect to {}: {}.", new Object[] { getLocalPeerName(), remoteAddress, reason }); // No need to clean up the connectedPeers map here - // PeerHandler is only created in PeerHandshakeHandler. PeerInformation peerInformation = knownPeers.get(remoteAddress); if (peerInformation != null) { long reconnectDelay; if (cause != null) reconnectDelay = RECONNECT_DELAY_PERMANENT; else reconnectDelay = RECONNECT_DELAY_TEMPORARY; peerInformation.setReconnectTime(getRandom(), System.currentTimeMillis() + reconnectDelay); } LOG.debug("{}: PeerInformation {} -> {}", new Object[] { getLocalPeerName(), remoteAddress, peerInformation }); } /** PeerActivityListener handler(s). *************************************/ /** * Peer choked handler. * * <p> * When a peer chokes, the requests made to it are cancelled and we need to * mark the eventually piece we requested from it as available again for * download tentative from another peer. * </p> * * @param peer The peer that choked. */ @Override public void handlePeerChoking(PeerHandler peer) { if (LOG.isTraceEnabled()) LOG.trace("Peer {} choked, we now have {} outstanding " + "request(s): {}", new Object[] { peer, getRequestedPieceCount(), getRequestedPieces() }); } /** * Peer ready handler. * * <p> * When a peer becomes ready to accept piece block requests, select a piece * to download and go for it. * </p> * * @param peer The peer that became ready. */ @Override public void handlePeerUnchoking(PeerHandler peer) { if (LOG.isTraceEnabled()) LOG.trace("{}: Peer {} is ready and has {}/{} piece(s).", new Object[] { getLocalPeerName(), peer, peer.getAvailablePieceCount(), torrent.getPieceCount() }); } /** * Piece availability handler. * * <p> * Handle updates in piece availability from a peer's HAVE message. When * this happens, we need to mark that piece as available from the peer. * </p> * * @param peer The peer we got the update from. * @param piece The piece that became available. */ @Override public void handlePieceAvailability(PeerHandler peer, int piece) { setAvailablePiece(piece, true); if (LOG.isTraceEnabled()) LOG.trace( "{}: Peer {} contributes {}/{} piece(s) " + "[completed={}, available={}/{}] " + "[connected={}/{}]", new Object[] { getLocalPeerName(), peer, peer.getAvailablePieceCount(), torrent.getPieceCount(), torrent.getCompletedPieceCount(), getAvailablePieceCount(), torrent.getPieceCount(), getConnectedPeerCount(), getPeerCount() }); } /** * Bit field availability handler. * * <p> * Handle updates in piece availability from a peer's BITFIELD message. * When this happens, we need to mark in all the pieces the peer has that * they can be reached through this peer, thus augmenting the global * availability of pieces. * </p> * * @param peer The peer we got the update from. * @param availablePieces The pieces availability bit field of the peer. */ @Override public void handleBitfieldAvailability(PeerHandler peer, BitSet prevAvailablePieces, BitSet currAvailablePieces) { // Record that the peer no longer has all the pieces it previously told us it had. for (int i = prevAvailablePieces.nextSetBit(0); i >= 0; i = prevAvailablePieces.nextSetBit(i + 1)) { if (!currAvailablePieces.get(i)) setAvailablePiece(i, false); } // Record that the peer has all the pieces it told us it had. for (int i = currAvailablePieces.nextSetBit(0); i >= 0; i = currAvailablePieces.nextSetBit(i + 1)) { if (!prevAvailablePieces.get(i)) setAvailablePiece(i, true); } // Determine if the peer is interesting for us or not, and notify it. BitSet interesting = currAvailablePieces; torrent.andNotCompletedPieces(interesting); this.andNotRequestedPieces(interesting); /* if (interesting.isEmpty()) peer.notInteresting(); else peer.interesting(); */ if (LOG.isTraceEnabled()) LOG.trace( "{}: Peer {} contributes {} piece(s) ({} interesting) " + "[completed={}; available={}/{}] " + "[connected={}/{}]", new Object[] { getLocalPeerName(), peer, currAvailablePieces.cardinality(), interesting.cardinality(), torrent.getCompletedPieceCount(), getAvailablePieceCount(), torrent.getPieceCount(), getConnectedPeerCount(), getPeerCount() }); // Fast unchoking: TODO: Move to somewhere more useful. if (connectedPeers.size() < MAX_DOWNLOADERS_UNCHOKE) { if (LOG.isDebugEnabled()) LOG.debug("{}: Fast-unchoking {}.", new Object[] { getLocalPeerName(), peer }); peer.unchoke(); } } /** * Block upload handler. * * <p> * When a block has been sent to a peer, we just record that we sent that * many bytes. If the piece is valid on the peer's side, it will send us a * HAVE message and we'll record that the piece is available on the peer at * that moment (see <code>handlePieceAvailability()</code>). * </p> * * @param peer The peer we got this piece from. * @param piece The piece in question. */ @Override public void handleBlockSent(PeerHandler peer, int piece, int offset, int length) { this.uploaded.addAndGet(length); } @Override public void handleBlockReceived(PeerHandler peer, int piece, int offset, int length) { this.downloaded.addAndGet(length); } /** * Piece download completion handler. * * <p> * When a piece is completed, and valid, we announce to all connected peers * that we now have this piece. * </p> * * <p> * We use this handler to identify when all of the pieces have been * downloaded. When that's the case, we can start the seeding period, if * any. * </p> * * @param peer The peer we got the piece from. * @param piece The piece in question. */ @Override public void handlePieceCompleted(PeerHandler peer, int piece, PieceHandler.Reception reception) throws IOException { // Regardless of validity, record the number of bytes downloaded and // mark the piece as not requested anymore synchronized (lock) { // TODO: Not sure if this is required. for (Iterator<PieceHandler.AnswerableRequestMessage> it = partialPieces.iterator(); it .hasNext(); /**/) { PieceHandler.AnswerableRequestMessage request = it.next(); if (request.getPiece() == piece) it.remove(); } } if (reception == PieceHandler.Reception.VALID) { // Make sure the piece is marked as completed in the torrent // Note: this is required because the order the // PeerActivityListeners are called is not defined, and we // might be called before the torrent's piece completion // handler is. // Do this before we print the log message, else the counts are misleading. torrent.setCompletedPiece(piece); } if (LOG.isDebugEnabled()) LOG.debug( "{}: Completed download of piece {} from {} ({}). We now have {}/{} pieces and {} outstanding requests: {}", new Object[] { getLocalPeerName(), piece, peer, reception, torrent.getCompletedPieceCount(), torrent.getPieceCount(), getRequestedPieceCount(), getRequestedPieces() }); if (reception == PieceHandler.Reception.VALID) { // Send a HAVE message to all connected peers PeerMessage have = new PeerMessage.HaveMessage(piece); for (PeerHandler remote : getConnectedPeers()) remote.send(have, true); } else { LOG.warn("{}, Downloaded piece#{} from {} was not valid: {} ;-(", new Object[] { getLocalPeerName(), piece, peer, reception }); } // It's possible for more than one thread to get here simultaneously. if (torrent.isComplete()) { if (LOG.isDebugEnabled()) LOG.debug("{}: {}: Last piece ({}) validated and completed, finishing download.", new Object[] { getLocalPeerName(), torrent, piece }); // Cancel all remaining outstanding requests for (PeerHandler remote : getConnectedPeers()) remote.cancelRequestsSent("torrent completed"); torrent.finish(); } } /** * Peer disconnection handler. * * <p> * When a peer disconnects, we need to mark in all of the pieces it had * available that they can't be reached through this peer anymore. * </p> * * @param peer The peer we got this piece from. */ @Override public void handlePeerDisconnected(PeerHandler peer) { BitSet peerAvailablePieces = peer.getAvailablePieces(); for (int i = peerAvailablePieces.nextSetBit(0); i >= 0; i = peerAvailablePieces.nextSetBit(i + 1)) { setAvailablePiece(i, false); } peer.rejectRequestsSent("peer disconnected"); if (LOG.isDebugEnabled()) LOG.debug( "{}: Peer {} went away with {} piece(s) " + "[completed={}; available={}/{}] " + "[connected={}/{}]", new Object[] { getLocalPeerName(), peer, peer.getAvailablePieceCount(), torrent.getCompletedPieceCount(), getAvailablePieceCount(), torrent.getPieceCount(), getConnectedPeerCount(), getPeerCount() }); connectedPeers.remove(peer.getHexRemotePeerId(), peer); long now = System.currentTimeMillis(); PeerInformation peerInformation = knownPeers.get(peer.getRemoteAddress()); if (peerInformation != null) peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); else for (Map.Entry<SocketAddress, PeerInformation> e : knownPeers.entrySet()) { peerInformation = e.getValue(); if (Arrays.equals(peerInformation.remotePeerId, peer.getRemotePeerId())) peerInformation.setReconnectTime(getRandom(), now + RECONNECT_DELAY_TEMPORARY); } } @Override public void handleIOException(PeerHandler peer, IOException ioe) { LOG.warn("I/O error while exchanging data with " + peer + ", " + "closing connection with it!", ioe); peer.close("I/O error"); // This should be done by handlePeerDisconnected but let's double up. connectedPeers.remove(peer.getHexRemotePeerId(), peer); } public void info(boolean verbose) { double dl = 0; double ul = 0; for (PeerHandler peer : getConnectedPeers()) { dl += peer.getDLRate().getRate(TimeUnit.SECONDS); ul += peer.getULRate().getRate(TimeUnit.SECONDS); } LOG.info("{} {} {} {}/{} pieces ({}%) [req {}/{} partial {}] with {}/{} peers at {}/{} kB/s.", new Object[] { getLocalPeerName(), torrent.getState().name(), TorrentUtils.toHex(torrent.getInfoHash()), torrent.getCompletedPieceCount(), getPieceCount(), String.format("%.2f", torrent.getCompletion()), getRequestedPieceCount(), getAvailablePieceCount(), getPartialPieceCount(), getConnectedPeerCount(), getPeerCount(), String.format("%.2f", dl / 1024.0), String.format("%.2f", ul / 1024.0) }); if (verbose) for (PeerHandler peer : getConnectedPeers()) LOG.debug(" | {}", peer); } }