/****************************************************************************** * 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.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Arrays; import org.eclipse.bittorrent.TorrentConfiguration; import org.eclipse.bittorrent.TorrentFile; import org.eclipse.bittorrent.internal.encode.Decode; import org.eclipse.bittorrent.internal.encode.Encode; import org.eclipse.bittorrent.internal.torrent.Piece; /** * An extension of the <code>Thread</code> class to manage a connection with a * peer. */ class PeerConnection extends Thread { /** * A byte array that represents a "keep alive" message to ensure that the * connection does not drop. This is synonymous with a 'ping'. */ private static final byte[] KEEP_ALIVE = { 0x00, 0x00, 0x00, 0x00 }; private static final byte[] CHOKE = { 0x00, 0x00, 0x00, 0x01, 0x00 }; private static final byte[] UNCHOKE = { 0x00, 0x00, 0x00, 0x01, 0x01 }; private static final byte[] INTERESTED = { 0x00, 0x00, 0x00, 0x01, 0x02 }; /** * A byte array that is sent to indicate that the current user is not * interested in any of the pieces that the connected peer possesses. */ private static final byte[] NOT_INTERESTED = { 0x00, 0x00, 0x00, 0x01, 0x03 }; /** * This is a specially formed string created by a byte array to represent * the string literal "BitTorrent protocol" led by a value of '19' along * with a string of eight zeros and is used during the handshaking process * with a peer. */ private static final String PROTOCOL_STRING = new String(new byte[] { 19, 66, 105, 116, 84, 111, 114, 114, 101, 110, 116, 32, 112, 114, 111, 116, 111, 99, 111, 108, 0, 0, 0, 0, 0, 0, 0, 0 }); /** * The amount of space allocated for the <code>ByteBuffer</code>s. The * value is 1024. */ private static final int BUFFER_MAXIMUM = 1024; /** * The <code>ByteBuffer</code> that is used to read data from the peer. */ private final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_MAXIMUM); /** * The <code>ByteBuffer</code> that is for sending data to the peer. */ private final ByteBuffer sendBuffer = ByteBuffer.allocate(BUFFER_MAXIMUM); private final ConnectionPool pool; private final TorrentManager manager; private final long[] downloads = new long[20]; private final long[] uploads = new long[20]; private final byte[] handshake; private final byte[] have = { 0x00, 0x00, 0x00, 0x05, 0x04, 0x00, 0x00, 0x00, 0x00 }; private final byte[] request = { 0x00, 0x00, 0x00, 0x0d, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00 }; private final byte[] blockInfo = { 0x00, 0x00, 0x00, 0x09, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; private final byte[] cancel = { 0x00, 0x00, 0x00, 0x0d, 0x08, 0x00, 0x00, 0x00, 0x00 }; private final boolean[] haveMessages; private SocketChannel channel; private InetSocketAddress address; /** * An array of <code>boolean</code>s that keeps track of what pieces this * peer has. */ private boolean[] peerPieces; /** * The name and version of the BitTorrent client that this peer is currently * using or <code>"Unknown"</code> if it is not known. */ private String clientName; /** * The peer's IP address. */ private String ip; /** * The amount of bytes that has been downloaded from this peer. */ private long downloaded = 0; /** * The amount of bytes that has been uploaded to this peer. */ private long uploaded = 0; private long lastDownloaded = 0; private long lastUploaded = 0; /** * The port that this peer is listening on. */ private int port; /** * A counter for {@link #downloads} and {@link #uploads} to store the amount * of data that has been downloaded and uploaded per second. */ private int queuePosition = 0; /** * Whether the client is currently choking this peer. This value is * <code>true</code> in the beginning. */ private boolean isChoking = true; /** * Whether this client is interested in a piece that this peer currently * has. This value is <code>false</code> in the beginning. */ private boolean isInterested = false; /** * Whether the peer is currently choking this client. This value is * <code>true</code> in the beginning. */ private boolean peerIsChoking = true; /** * Whether the peer is interested in a piece that this client currently has. * This value is <code>false</code> in the beginning. */ private boolean peerIsInterested = false; /** * Identifies whether this peer is a seed or not. */ private boolean peerIsSeed = false; private boolean initialized = true; /** * Indicates whether a choke message should be sent to the peer. */ private boolean sendChoke = false; /** * Indicates whether an unchoke message should be sent to the peer. */ private boolean sendUnchoke = false; PeerConnection(ConnectionPool pool, TorrentManager manager) throws UnsupportedEncodingException { this.pool = pool; this.manager = manager; TorrentFile torrent = manager.getTorrentFile(); StringBuffer buffer = new StringBuffer(PROTOCOL_STRING); synchronized (buffer) { buffer.append(torrent.getInfoHash()); buffer.append(manager.getPeerID()); } handshake = buffer.toString().getBytes("ISO-8859-1"); peerPieces = new boolean[torrent.getNumPieces()]; haveMessages = new boolean[peerPieces.length]; Arrays.fill(peerPieces, false); Arrays.fill(haveMessages, false); } void setAddress(String ip, int port) { address = new InetSocketAddress(ip, port); this.ip = ip; this.port = port; } void setChannel(SocketChannel channel) { this.channel = channel; Socket socket = channel.socket(); this.ip = socket.getLocalAddress().getHostAddress(); this.port = socket.getLocalPort(); } /** * Connect to the peer and exchange handshakes as necessary. A new * connection may be created depending on whether an incoming or outgoing * connection is being established. */ private void connect() { try { if (channel == null) { call(); } else { answer(); } } catch (IOException e) { String message = e.getMessage(); TorrentConfiguration.debug("The connection with " + ip + ":" + port + " has been closed" + (message == null ? "." : ": " + message)); } catch (RuntimeException e) { close(); throw e; } close(); } private void call() throws IOException { try { channel = SocketChannel.open(address); address = null; } catch (SocketException e) { TorrentConfiguration.debug("Unable to connect to " + ip + ":" + port + " - " + e.getMessage()); return; } TorrentConfiguration.debug("Established outgoing connection with " + ip + ":" + port); sendHandshake(); int read = 0; int ret = 0; // a return handshake is 68 bytes long, read until at least that many // bytes has been retrieved while (read < 68) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has been reached " + "with " + ip + ":" + port); return; } read += ret; } TorrentConfiguration.debug("Received [BT_HANDSHAKE] message from " + ip + ":" + port); byte[] client = new byte[20]; System.arraycopy(buffer.array(), 48, client, 0, 20); processClientName(new String(client)); // check to see if data besides the return handshake was sent if (read != 68) { // keep reading until four additional bytes are read while (read < 72) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has been " + "reached with " + ip + ":" + port); return; } read += ret; } if (buffer.get(71) != 0) { while (read < 73) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has been " + "reached with " + ip + ":" + port); return; } read += ret; } // check to see if part of what was read is a bitfield if (buffer.get(72) == 5) { int length = 68 + 4 + buffer.get(71); // keep reading until the entire bitfield has been read in while (read < length) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has " + "been reached with " + ip + ":" + port); return; } read += ret; } buffer.flip(); byte[] array = new byte[read]; buffer.get(array, 0, read); processBitfield(array, 73, 72 + array[71]); if (read == length) { sendBitfield(); } } else if (buffer.get(71) == 1) { byte[] bytes = null; buffer.get(bytes, 0, read); byte[] array = new byte[5]; System.arraycopy(bytes, 68, array, 0, 5); if (!processMessage(array)) { return; } } } else if (buffer.get(78) == 0 && buffer.get(79) == 0 && buffer.get(80) == 0) { TorrentConfiguration .debug("Received [BT_KEEPALIVE] message from " + ip + ":" + port); } else { TorrentConfiguration .debug("Received an unidentifiable message from " + ip + ":" + port); return; } } else { sendBitfield(); } buffer.clear(); // enter the main loop query(); } private void answer() throws IOException { TorrentConfiguration.debug("Established incoming connection from " + ip + ":" + port); sendHandshake(); sendBitfield(); query(); } private void query() throws IOException { TorrentConfiguration.debug("Entering the main loop with " + ip + ":" + port); byte[] array = null; int read = 0; int ret = 0; while (channel != null && channel.isConnected()) { buffer.clear(); sendQueuedMessages(); if (read != 0) { byte[] bufferArray = buffer.array(); byte[] bytes = new byte[bufferArray.length]; System.arraycopy(bufferArray, read, bytes, 0, bufferArray.length - read); buffer.put(bytes); buffer.position(read); } int request = sendRequest(manager.request(peerPieces)); int length = request == -1 ? 4 : request; resetBuffer(); while (read < 4) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has been " + "reached with " + ip + ":" + port); return; } manager.updateDownloadRequestSpeed(ret); limitBuffer(manager.getDownloadRequestSpeed()); read += ret; } buffer.limit(BUFFER_MAXIMUM); buffer.rewind(); if (array == null) { array = new byte[read]; buffer.get(array, 0, read); } else { byte[] bytes = new byte[array.length + read]; System.arraycopy(array, 0, bytes, 0, array.length); buffer.get(bytes, array.length, read); array = bytes; } if (array[0] == 0 && array[1] == 0 && array[2] == 0 && array[3] == 0) { TorrentConfiguration.debug("Received [BT_KEEPALIVE] from " + ip + ":" + port); array = truncate(array, 4); read -= 4; } while (array != null && array.length > 4) { if (0 <= array[4] && array[4] <= 3) { if (!processMessage(array)) { return; } array = truncate(array, 5); read -= 5; } else if (array[3] == 5 && array[4] == 4) { if (array.length < 9) { buffer.clear(); while (read < 9) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has " + "been reached with " + ip + ":" + port); return; } read += ret; } byte[] bytes = new byte[read]; System.arraycopy(array, 0, bytes, 0, array.length); buffer.flip(); buffer.get(bytes, array.length, read - array.length); array = bytes; } processHaveMessage(array); array = truncate(array, 9); read -= 9; } else if (array[4] == 5) { length = 4 + array[3]; while (read < length) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has " + "been reached with " + ip + ":" + port); return; } read += ret; } if (array.length < read) { byte[] bytes = new byte[read]; System.arraycopy(array, 0, bytes, 0, array.length); buffer.flip(); buffer.get(bytes, array.length, read - array.length); array = bytes; } processBitfield(array, 5, length); array = truncate(array, length); read -= length; } else if (array[3] == 13 && array[4] == 6) { if (array.length < 17) { buffer.clear(); while (read < 17) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has " + "been reached with " + ip + ":" + port); return; } read += ret; } byte[] bytes = new byte[read]; System.arraycopy(array, 0, bytes, 0, array.length); buffer.flip(); buffer.get(bytes, array.length, read - array.length); array = bytes; } if (!processRequest(array)) { // an improper request was sent close(); return; } array = truncate(array, 17); read -= 17; } else if (array[4] == 7) { byte[] bufferArray = buffer.array(); length = Decode.decodeFourByteNumber(array, 0) + 4; int offset = array.length; if (offset < length) { byte[] bytes = new byte[length]; System.arraycopy(array, 0, bytes, 0, offset); array = bytes; } resetBuffer(); buffer.clear(); int start = read; while (read < length) { if (channel == null) { return; } ret = channel.read(buffer); if (ret == -1) { TorrentConfiguration.debug("End of stream has " + "been reached with " + ip + ":" + port); return; } // expand the array's length if necessary if (offset + ret > array.length) { byte[] bytes = new byte[offset + ret]; System.arraycopy(array, 0, bytes, 0, array.length); array = bytes; } System.arraycopy(bufferArray, 0, array, offset, ret); manager.updateDownloadRequestSpeed(ret); buffer.rewind(); limitBuffer(manager.getDownloadRequestSpeed()); offset += ret; read += ret; } buffer.limit(BUFFER_MAXIMUM); if (array.length < read) { byte[] bytes = new byte[read]; System.arraycopy(array, 0, bytes, 0, array.length); buffer.position(start); buffer.get(bytes, array.length, read - array.length); array = bytes; } processPiece(array); array = truncate(array, length); read -= length; } else if (array[4] == 8) { // TODO: implement the processing of BT_CANCEL messages array = truncate(array, 17); read -= 17; } else if (array[4] == 9) { // TODO: implement the processing of BT_PORT messages array = truncate(array, 9); read -= 9; } else { TorrentConfiguration.debug("An ID of " + array[4] + " has been encountered. Closing connection with " + ip + ":" + port); return; } } } } private byte[] truncate(byte[] array, int length) { if (array.length == length) { return null; } else { byte[] bytes = new byte[array.length - length]; System.arraycopy(array, length, bytes, 0, bytes.length); return bytes; } } private void resetBuffer() { long maximum = manager.getDownloadRequestSpeed(); if (maximum == -1) { return; } buffer.limit(0); limitBuffer(maximum); } private void limitBuffer(long maximum) { if (maximum != 0) { int currentLimit = buffer.limit(); if (maximum + currentLimit > BUFFER_MAXIMUM) { buffer.limit(BUFFER_MAXIMUM); } else { buffer.limit(currentLimit + (int) maximum); } } else { try { buffer.limit(0); // sleep for a short while here since if it's zero, chances are // that the next rotation will return a zero also, causing an // unnecessarily long loop Thread.sleep(100); } catch (InterruptedException e) { // ignored } } } private void processClientName(String peerID) { switch (peerID.charAt(0)) { case '-': String client = peerID.substring(1, 3); String version = peerID.charAt(3) + "." + peerID.charAt(4) + "." + peerID.charAt(5) + "." + peerID.charAt(6); if (client.equals("AR")) { clientName = "Arctic " + version; } else if (client.equals("AX")) { clientName = "BitPump " + version; } else if (client.equals("AZ")) { clientName = "Azureus " + version; } else if (client.equals("BB")) { clientName = "BitBuddy " + version; } else if (client.equals("BC")) { clientName = "BitComet " + version; } else if (client.equals("BS")) { clientName = "BTSlave " + version; } else if (client.equals("BX")) { clientName = "Bittorrent X " + version; } else if (client.equals("CD")) { clientName = "Enhanced CTorrent " + version; } else if (client.equals("CT")) { clientName = "CTorrent " + version; } else if (client.equals("LP")) { clientName = "Lphant " + version; } else if (client.equals("LT")) { clientName = "libtorrent " + version; } else if (client.equals("lt")) { clientName = "libTorrent " + version; } else if (client.equals("MP")) { clientName = "MooPolice " + version; } else if (client.equals("MT")) { clientName = "MoonlightTorrent " + version; } else if (client.equals("QT")) { clientName = "QT 4 Torrent example " + version; } else if (client.equals("RT")) { clientName = "Retriever " + version; } else if (client.equals("SB")) { clientName = "Swiftbit " + version; } else if (client.equals("SS")) { clientName = "SwarmScope " + version; } else if (client.equals("SZ")) { clientName = "Shareaza " + version; } else if (client.equals("TN")) { clientName = "TorrentDotNet " + version; } else if (client.equals("TR")) { clientName = "Transmission " + version; } else if (client.equals("TS")) { clientName = "TorrentStorm " + version; } else if (client.equals("UT")) { clientName = "µTorrent " + version; } else if (client.equals("XT")) { clientName = "XanTorrent " + version; } else if (client.equals("ZT")) { clientName = "ZipTorrent " + version; } else { clientName = "Unknown"; } break; case 'A': clientName = "ABC " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; case 'M': clientName = "Mainline " + peerID.charAt(1) + "." + peerID.charAt(3) + "." + peerID.charAt(5); break; case 'O': clientName = "Osprey Permaseed " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; case 'R': clientName = "Tribler " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; case 'S': clientName = "Shadow's client " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; case 'T': clientName = "BitTornado " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; case 'U': clientName = "UPnP NAT Bit Torrent " + peerID.charAt(1) + "." + peerID.charAt(2) + "." + peerID.charAt(3); break; default: clientName = "Unknown"; break; } } private boolean processMessage(byte[] array) throws IOException { switch (array[4]) { case 0: TorrentConfiguration.debug("Received [BT_CHOKE] message from " + ip + ":" + port); peerIsChoking = true; break; case 1: TorrentConfiguration.debug("Received [BT_UNCHOKE] message from " + ip + ":" + port); peerIsChoking = false; break; case 2: TorrentConfiguration.debug("Received [BT_INTERESTED] message from " + ip + ":" + port); if (peerIsInterested) { break; } peerIsInterested = true; if (pool.checkUnchoke()) { sendUnchoke(); } break; case 3: TorrentConfiguration .debug("Received [BT_NOT_INTERESTED] message from " + ip + ":" + port); if (!peerIsInterested) { break; } peerIsInterested = false; if (!isChoking) { pool.unchokedPeerCleared(); } sendChoke(); break; default: return false; } return true; } private void processBitfield(byte[] array, int offset, int end) { boolean[] hasPiece = new boolean[(end - offset) * 8]; int count = 0; // iterate over the retrieved bytes and keep track of the pieces that // this peer has for (int i = offset; i < end; i++) { int bit = Decode.decodeSignedByte(array[i]); hasPiece[count++] = (bit & 1) != 0; hasPiece[count++] = (bit & 2) != 0; hasPiece[count++] = (bit & 4) != 0; hasPiece[count++] = (bit & 8) != 0; hasPiece[count++] = (bit & 16) != 0; hasPiece[count++] = (bit & 32) != 0; hasPiece[count++] = (bit & 64) != 0; hasPiece[count++] = (bit & 128) != 0; } System.arraycopy(hasPiece, 0, peerPieces, 0, peerPieces.length); TorrentConfiguration.debug("Received [BT_BITFIELD] message from " + ip + ":" + port); manager.addPieceAvailability(peerPieces); for (int i = 0; i < peerPieces.length; i++) { if (!peerPieces[i]) { return; } } peerIsSeed = true; } private void processHaveMessage(byte[] array) { int piece = Decode.decodeFourByteNumber(array, 5); if (!peerPieces[piece]) { peerPieces[piece] = true; manager.updatePieceAvailability(piece); } TorrentConfiguration.debug("Received [BT_HAVE piece #" + piece + "] message from " + ip + ":" + port); for (int i = 0; i < peerPieces.length; i++) { if (!peerPieces[i]) { return; } } peerIsSeed = true; } private void processCancel(byte[] array) { // TODO: unimplemented } private void processPiece(byte[] array) throws IOException { int piece = Decode.decodeFourByteNumber(array, 5); int index = Decode.decodeFourByteNumber(array, 9); int length = Decode.decodeFourByteNumber(array, 0) - 9; manager.write(piece, index, array, 13, length); downloaded += length; TorrentConfiguration.debug("Received [BT_PIECE data for #" + piece + ": " + index + "->" + (length + index - 1) + "] message from " + ip + ":" + port); } private boolean processRequest(byte[] array) throws IOException { int piece = Decode.decodeFourByteNumber(array, 5); int index = Decode.decodeFourByteNumber(array, 9); int length = Decode.decodeFourByteNumber(array, 13); if (isChoking) { TorrentConfiguration.debug("Ignoring [BT_REQUEST piece #" + piece + ": " + index + "->" + (index + length - 1) + "] message from " + ip + ":" + port + " as this peer is currently choked"); return true; } TorrentConfiguration.debug("Received [BT_REQUEST piece #" + piece + ": " + index + "->" + (index + length - 1) + "] message from " + ip + ":" + port); if (length > 131072) { TorrentConfiguration.debug("The requesting of " + length + " bytes violates the standard maximum amount of " + "131072, the connection to " + ip + ":" + port + " will be closed."); return false; } byte[] block = manager.getPieceData(piece, index, length); if (block == null) { return false; } Encode.putIntegerAsFourBytes(blockInfo, length + 9, 0); Encode.putIntegerAsFourBytes(blockInfo, piece, 5); Encode.putIntegerAsFourBytes(blockInfo, index, 9); sendBuffer.put(blockInfo); int blockLength = block.length; int offset = 0; int remaining = sendBuffer.remaining(); long write = manager.getUploadRequestSpeed(); if (write != 0) { if (write == -1) { // since no limit has been set, simply set the amount to write // as the length of the block write = blockLength; } else { write = write < blockLength ? write : blockLength; } write = remaining < write ? remaining : write; } while (offset < blockLength) { if (write == 0) { try { Thread.sleep(500); } catch (InterruptedException e) { // ignored } } else { sendBuffer.put(block, offset, (int) write); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); offset += write; manager.updateUploadRequestSpeed((int) write); uploaded += write; manager.addToUploaded(write); } write = manager.getUploadRequestSpeed(); if (write != 0) { remaining = blockLength - offset; if (write == -1) { write = remaining; } else { write = write < remaining ? write : remaining; } remaining = sendBuffer.remaining(); write = remaining < write ? remaining : write; } } TorrentConfiguration.debug("Sent [BT_PIECE data for #" + piece + ": " + index + "->" + (length + index - 1) + "] message to " + ip + ":" + port); return true; } private void sendHandshake() throws IOException { sendBuffer.put(handshake); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); TorrentConfiguration.debug("Sent [BT_HANDSHAKE] message to " + ip + ":" + port); } private void sendBitfield() throws IOException { byte[] bitfield = manager.getBitfield(); boolean hasPiece = false; for (int i = 0; i < bitfield.length; i++) { if (bitfield[i] != 0) { hasPiece = true; break; } } // if we currently have no pieces, there is no need to send a bitfield // to the peer if (!hasPiece) { return; } byte[] header = { 0x00, 0x00, 0x00, 0x00, 0x05 }; Encode.putIntegerAsFourBytes(header, bitfield.length + 1, 0); sendBuffer.put(header); sendBuffer.put(manager.getBitfield()); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); TorrentConfiguration.debug("Sent [BT_BITFIELD] message to " + ip + ":" + port); } private int sendRequest(Piece piece) throws IOException { if (piece == null) { sendNotInterested(); return -1; } else if (peerIsChoking) { sendInterested(); return -1; } int[] information = piece.getRequestInformation(); while (information == null) { piece = manager.request(peerPieces); if (piece == null) { sendNotInterested(); return -1; } information = piece.getRequestInformation(); } Encode.placeRequestInformation(request, information); sendBuffer.put(request); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); TorrentConfiguration.debug("Sent [BT_REQUEST piece #" + information[0] + ": " + information[1] + "->" + (information[1] + information[2] - 1) + "] message to " + ip + ":" + port); return information[2] + 13; } /** * Sends the queued up messages to the connected peer to inform. * * @throws IOException * If an I/O error occurs while sending the messages to the peer */ private synchronized void sendQueuedMessages() throws IOException { for (int i = 0; i < haveMessages.length; i++) { if (haveMessages[i]) { Encode.putIntegerAsFourBytes(have, i, 5); sendBuffer.put(have); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); TorrentConfiguration.debug("Sent [BT_HAVE PIECE #" + i + "] message to " + ip + ":" + port); haveMessages[i] = false; } } if (sendChoke) { sendChoke(); sendChoke = false; } else if (sendUnchoke) { sendUnchoke(); sendUnchoke = false; } } private void sendKeepAlive() throws IOException { sendBuffer.put(KEEP_ALIVE); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); } /** * Sends a message to the peer that this client is interested in something * that the peer has to offer. * * @throws IOException * If an I/O error occurs while sending the message */ private void sendInterested() throws IOException { if (!isInterested) { sendBuffer.put(INTERESTED); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); isInterested = true; TorrentConfiguration.debug("Sent [BT_INTERESTED] message to " + ip + ":" + port); } } /** * Sends a message to the peer that this client is not interested in * anything that the peer currently has to offer. * * @throws IOException * If an I/O error occurs while sending the message */ private void sendNotInterested() throws IOException { if (isInterested) { sendBuffer.put(NOT_INTERESTED); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); isInterested = false; TorrentConfiguration.debug("Sent [BT_NOT_INTERESTED] message to " + ip + ":" + port); } } /** * Sends a choke message to the peer which indicates to them that any piece * requests will be ignored and discarded. * * @throws IOException * If an I/O error occurs while writing the message to the peer */ private void sendChoke() throws IOException { if (!isChoking) { sendBuffer.put(CHOKE); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); isChoking = true; TorrentConfiguration.debug("Sent [BT_CHOKE] message to " + ip + ":" + port); } } /** * Sends an unchoke message to the peer to inform them that piece requests * will now be honoured. * * @throws IOException * If an I/O error occurs whilst writing the message to the peer */ private void sendUnchoke() throws IOException { if (isChoking) { sendBuffer.put(UNCHOKE); sendBuffer.flip(); channel.write(sendBuffer); sendBuffer.clear(); isChoking = false; TorrentConfiguration.debug("Sent [BT_UNCHOKE] message to " + ip + ":" + port); } } /** * Resets instance values so that a new connection that is bridged by this * thread will not have remnants of information from the last connection. */ private void reset() { queuePosition = 0; downloaded = 0; uploaded = 0; Arrays.fill(downloads, 0); Arrays.fill(uploads, 0); isInterested = false; isChoking = true; peerIsInterested = false; peerIsChoking = true; peerIsSeed = false; Arrays.fill(peerPieces, false); sendChoke = false; sendUnchoke = false; Arrays.fill(haveMessages, false); clientName = "Unknown"; } private void cleanup() { pool.connectionClosed(); if (!isChoking) { pool.unchokedPeerCleared(); } initialized = false; } /** * Closes this connection. Any <code>IOException</code>s that may be * thrown will closing the connection with the peer will be ignored. */ void close() { if (channel != null) { reset(); try { channel.close(); } catch (IOException e) { // ignored } channel = null; manager.removePieceAvailability(peerPieces); } } boolean isChoking() { return isChoking; } boolean isConnectedTo(String ip, int port) { return port == this.port && ip.equals(this.ip); } /** * Returns whether the connected peer is a seed or not. This is used to * identify whether this connection should be cut after a download has * completed since there is no need for a seed to be connected to another * seed. * * @return <code>true</code> if the connected peer is a seed, * <code>false</code> otherwise */ boolean isSeed() { return peerIsSeed; } /** * Queues up the specified piece as needing a corresponding HAVE message to * be sent to the connected peer. * * @param number * the number of the piece that has just been completed * @throws IllegalArgumentException * If a negative piece number or a piece number that is over the * number of available pieces has been set */ void queueHaveMessage(int number) throws IllegalArgumentException { if (number < 0) { throw new IllegalArgumentException("The piece number cannot be " + "negative"); } else if (number >= peerPieces.length) { throw new IllegalArgumentException("The piece number is greater " + "than the number of pieces"); } haveMessages[number] = true; } void queueChokeMessage() { sendUnchoke = true; } void queueUnchokeMessage() { sendChoke = true; } long getDownloaded() { return downloaded; } long getUploaded() { return uploaded; } double getDownSpeed() { double totalDown = 0; for (int j = 0; j < 20; j++) { totalDown += downloads[j]; } return totalDown / 20; } double getUpSpeed() { double totalUp = 0; for (int j = 0; j < 20; j++) { totalUp += uploads[j]; } return totalUp / 20; } void queueSpeeds() { if (queuePosition == 20) { queuePosition = 0; } downloads[queuePosition] = downloaded - lastDownloaded; uploads[queuePosition] = uploaded - lastUploaded; lastDownloaded = downloaded; lastUploaded = uploaded; queuePosition++; } String getClientName() { return clientName; } boolean isInitialized() { return initialized; } protected void finalize() { close(); } public void run() { ConnectionInfo info = pool.dequeue(); if (info == null) { pool.connectionDestroyed(this); return; } while (true) { try { initialized = true; if (info.isChannel()) { setChannel(info.getChannel()); } else { setAddress(info.getIP(), info.getPort()); } pool.connectionCreated(); connect(); cleanup(); if (!pool.isConnected()) { return; } try { synchronized (pool) { pool.wait(); } } catch (InterruptedException e) { pool.connectionDestroyed(this); return; } info = pool.dequeue(); if (info == null) { pool.connectionDestroyed(this); return; } } catch (RuntimeException e) { cleanup(); pool.connectionDestroyed(this); throw e; } } } }