/******************************************************************************
* Copyright (c) 2006 Remy Suen. All rights reserved. This program and the
* accompanying materials are made available under the terms of the Eclipse
* Public License v1.0, which accompanies this distribution and is available at
* http://www.eclipse.org/legal/epl-v10.html, and also the MIT license, which
* also accompanies this distribution. This dual licensing scheme allows a
* developer to choose either license for use when developing applications with
* this code.
*
* Contributors:
* Remy Suen <remy.suen@gmail.com> - initial API and implementation
******************************************************************************/
package org.eclipse.bittorrent.internal.net;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.Random;
import java.util.Vector;
/**
* A pool of threads to be allocated when a connection needs to be made with a
* peer.
*/
class ConnectionPool {
/**
* A shared <code>Random</code> instance to randomly select a piece to
* request from peers.
*/
static final Random RANDOM = new Random();
/**
* The number of seconds to wait before rotating optimistic unchokes.
*/
private static final int OPTIMISTIC_UNCHOKE_ROTATION_TIME = 30;
/**
* The maximum number of peers that are allowed to be unchoked on a
* persistent basis.
*/
private static final int MAX_UNCHOKED_PEERS = 4;
private final Vector connections;
private final LinkedList targets;
/**
* The manager associated with this pool.
*/
private final TorrentManager manager;
private Thread unchokingThread;
/**
* The maximum number of connections that this pool should be managing. This
* can be set with the {@link #setMaxConnections(int)} method, but should be
* increased with caution as it may cause TCP congestions.
*/
private int maxConnections = 50;
/**
* The number of active connections.
*/
private int size = 0;
/**
* The number of unchoked peers.
*/
private int unchokedPeers = 0;
private boolean connected = false;
ConnectionPool(TorrentManager manager) {
this.manager = manager;
connections = new Vector(maxConnections);
targets = new LinkedList();
}
private synchronized void unchoke() {
if (size <= MAX_UNCHOKED_PEERS || unchokedPeers < MAX_UNCHOKED_PEERS) {
return;
}
int unchoke = RANDOM.nextInt(size);
PeerConnection connection = (PeerConnection) connections.get(unchoke);
while (connection.isChoking()) {
unchoke = RANDOM.nextInt(size);
connection = (PeerConnection) connections.get(unchoke);
}
connection.queueUnchokeMessage();
int choke = RANDOM.nextInt(size);
connection = (PeerConnection) connections.get(choke);
while (choke != unchoke && !connection.isChoking()) {
choke = RANDOM.nextInt(size);
connection = (PeerConnection) connections.get(choke);
}
connection.queueChokeMessage();
}
/**
* Creates a connection to the specified ip at the given port. If the
* current number of active threads is equal to the maximum number of
* allowed connections, <code>false</code> will be returned.
*
* @param ip
* the IP of the peer
* @param port
* the port that the peer is listening on
* @throws UnsupportedEncodingException
* If the <code>ISO-8859-1</code> encoding is not supported
*/
void connectTo(String ip, int port) throws UnsupportedEncodingException {
if (unchokingThread == null) {
unchokingThread = new OptimisticUnchokingThread();
unchokingThread.start();
} else if (size == maxConnections) {
return;
}
connected = true;
synchronized (this) {
for (int i = 0; i < connections.size(); i++) {
PeerConnection connection = (PeerConnection) connections.get(i);
if (connection.isInitialized()
&& connection.isConnectedTo(ip, port)) {
return;
}
}
targets.add(new ConnectionInfo(ip, port));
for (int i = 0; i < connections.size(); i++) {
if (!((PeerConnection) connections.get(i)).isInitialized()) {
notify();
return;
}
}
PeerConnection connection = new PeerConnection(this, manager);
connections.add(connection);
connection.start();
}
}
void connectTo(SocketChannel channel) throws UnsupportedEncodingException {
if (unchokingThread == null) {
unchokingThread = new OptimisticUnchokingThread();
unchokingThread.start();
} else if (size == maxConnections) {
return;
}
connected = true;
Socket socket = channel.socket();
String ip = socket.getLocalAddress().getHostAddress();
int port = socket.getLocalPort();
synchronized (this) {
for (int i = 0; i < connections.size(); i++) {
PeerConnection connection = (PeerConnection) connections.get(i);
if (connection.isConnectedTo(ip, port)) {
return;
}
}
targets.add(new ConnectionInfo(channel));
for (int i = 0; i < connections.size(); i++) {
if (!((PeerConnection) connections.get(i)).isInitialized()) {
notify();
return;
}
}
PeerConnection connection = new PeerConnection(this, manager);
connections.add(connection);
connection.start();
}
}
/**
* Closes all of the channels that are currently active.
*/
synchronized void close() {
connected = false;
for (int i = 0; i < connections.size(); i++) {
((PeerConnection) connections.get(i)).close();
}
targets.clear();
notifyAll();
}
synchronized boolean isConnected() {
return connected;
}
ConnectionInfo dequeue() {
synchronized (targets) {
return targets.isEmpty() ? null : (ConnectionInfo) targets
.removeFirst();
}
}
/**
* Disconnects all connections to peers that are seeds. This is called after
* the downloading has completed successfully since it is no longer
* necessary to be connected to seeds since no pieces will be requested.
*/
void disconnectSeeds() {
for (int i = 0; i < connections.size(); i++) {
PeerConnection connection = (PeerConnection) connections.get(i);
if (connection.isInitialized() && connection.isSeed()) {
connection.close();
}
}
}
/**
* Called to inform this pool that one of the unchoked connections has been
* now been choked. This allows for another peer to be unchoked permanently
* during the next rotation.
*/
void unchokedPeerCleared() {
unchokedPeers--;
}
/**
* Sets the maximum number of connections that this pool should manage.
*
* @param maxConnections
* the maximum amount of connections to manage
*/
synchronized void setMaxConnections(int maxConnections) {
if (this.maxConnections < maxConnections) {
connections.ensureCapacity(maxConnections);
} else if (this.maxConnections > maxConnections) {
// push back all active connections
for (int i = 0; i < maxConnections; i++) {
if (!((PeerConnection) connections.get(i)).isInitialized()) {
for (int j = connections.size(); j > i; j--) {
PeerConnection conn = (PeerConnection) connections
.get(j);
if (conn.isInitialized()) {
connections.remove(i);
connections.add(i, connections.remove(j - 1));
}
}
}
}
// close all extraneous connections
for (int i = maxConnections; i < this.maxConnections; i++) {
((PeerConnection) connections.get(i)).close();
}
}
this.maxConnections = maxConnections;
}
synchronized boolean checkUnchoke() {
if (unchokedPeers == MAX_UNCHOKED_PEERS) {
return false;
}
unchokedPeers++;
return true;
}
void connectionCreated() {
size++;
}
/**
* Indicates to the pool that a connection has ended. This means that one of
* the {@link PeerConnection}s are no longer running.
*/
void connectionClosed() {
size--;
if (size == 0 && unchokingThread != null) {
unchokingThread.interrupt();
unchokingThread = null;
}
}
void connectionDestroyed(PeerConnection connection) {
connections.remove(connection);
}
/**
* Retrieves the current number of active connections.
*
* @return the number of active connections of this pool
*/
int getConnected() {
return size;
}
/**
* Instructs all running threads to send a have message of the specified
* piece to the connected peer.
*
* @param piece
* the number of the piece that the have message should
* correspond to
*/
void queueHaveMessage(int piece) {
for (int i = 0; i < connections.size(); i++) {
PeerConnection connection = (PeerConnection) connections.get(i);
if (connection.isInitialized()) {
connection.queueHaveMessage(piece);
}
}
}
boolean isEmpty() {
return connections.isEmpty();
}
private class OptimisticUnchokingThread extends Thread {
public OptimisticUnchokingThread() {
super("Optimistic Unchoking Thread "
+ manager.getTorrentFile().getName());
}
public void run() {
while (true) {
for (int i = 0; i < OPTIMISTIC_UNCHOKE_ROTATION_TIME; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return;
}
for (int j = 0; j < connections.size(); j++) {
PeerConnection conn = (PeerConnection) connections
.get(j);
if (conn.isInitialized()) {
conn.queueSpeeds();
}
}
}
unchoke();
}
}
}
}