com.bittorrent.mpetazzoni.client.ConnectionHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.bittorrent.mpetazzoni.client.ConnectionHandler.java

Source

/**
 * 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.bittorrent.mpetazzoni.client;

import com.bittorrent.mpetazzoni.common.Torrent;
import com.bittorrent.mpetazzoni.client.peer.SharingPeer;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Incoming peer connections service.
 *
 * <p>
 * Every BitTorrent client, BitTorrent being a peer-to-peer protocol, listens
 * on a port for incoming connections from other peers sharing the same
 * torrent.
 * </p>
 *
 * <p>
 * This ConnectionHandler implements this service and starts a listening socket
 * in the first available port in the default BitTorrent client port range
 * 6881-6889. When a peer connects to it, it expects the BitTorrent handshake
 * message, parses it and replies with our own handshake.
 * </p>
 *
 * <p>
 * Outgoing connections to other peers are also made through this service,
 * which handles the handshake procedure with the remote peer. Regardless of
 * the direction of the connection, once this handshake is successful, all
 * {@link IncomingConnectionListener}s are notified and passed the connected
 * socket and the remote peer ID.
 * </p>
 *
 * <p>
 * This class does nothing more. All further peer-to-peer communication happens
 * in the {@link com.bittorrent.mpetazzoni.client.peer.PeerExchange PeerExchange}
 * class.
 * </p>
 *
 * @author mpetazzoni
 * @see <a href="http://wiki.theory.org/BitTorrentSpecification#Handshake">BitTorrent handshake specification</a>
 */
public class ConnectionHandler implements Runnable {

    private static final Logger logger = LoggerFactory.getLogger(ConnectionHandler.class);

    public static final int PORT_RANGE_START = 6881;
    public static final int PORT_RANGE_END = 6889;

    private static final int OUTBOUND_CONNECTIONS_POOL_SIZE = 20;
    private static final int OUTBOUND_CONNECTIONS_THREAD_KEEP_ALIVE_SECS = 10;

    private static final int CLIENT_KEEP_ALIVE_MINUTES = 3;

    private SharedTorrent torrent;
    private String id;
    private ServerSocketChannel channel;
    private InetSocketAddress address;

    private Set<IncomingConnectionListener> listeners;
    private ExecutorService executor;
    private Thread thread;
    private boolean stop;

    /**
     * Create and start a new listening service for out torrent, reporting
     * with our peer ID on the given address.
     *
     * <p>
     * This binds to the first available port in the client port range
     * PORT_RANGE_START to PORT_RANGE_END.
     * </p>
     *
     * @param torrent The torrent shared by this client.
     * @param id This client's peer ID.
     * @param address The address to bind to.
     * @throws IOException When the service can't be started because no port in
     * the defined range is available or usable.
     */
    ConnectionHandler(SharedTorrent torrent, String id, InetAddress address) throws IOException {
        this.torrent = torrent;
        this.id = id;

        // Bind to the first available port in the range
        // [PORT_RANGE_START; PORT_RANGE_END].
        for (int port = ConnectionHandler.PORT_RANGE_START; port <= ConnectionHandler.PORT_RANGE_END; port++) {
            InetSocketAddress tryAddress = new InetSocketAddress(address, port);

            try {
                this.channel = ServerSocketChannel.open();
                this.channel.socket().bind(tryAddress);
                this.channel.configureBlocking(false);
                this.address = tryAddress;
                break;
            } catch (IOException ioe) {
                // Ignore, try next port
                logger.warn("Could not bind to {}, trying next port...", tryAddress);
            }
        }

        if (this.channel == null || !this.channel.socket().isBound()) {
            throw new IOException("No available port for the BitTorrent client!");
        }

        logger.info("Listening for incoming connections on {}.", this.address);

        this.listeners = new HashSet<IncomingConnectionListener>();
        this.executor = null;
        this.thread = null;
    }

    /**
     * Return the full socket address this service is bound to.
     */
    public InetSocketAddress getSocketAddress() {
        return this.address;
    }

    /**
     * Register a new incoming connection listener.
     *
     * @param listener The listener who wants to receive connection
     * notifications.
     */
    public void register(IncomingConnectionListener listener) {
        this.listeners.add(listener);
    }

    /**
     * Start accepting new connections in a background thread.
     */
    public void start() {
        if (this.channel == null) {
            throw new IllegalStateException("Connection handler cannot be recycled!");
        }

        this.stop = false;

        if (this.executor == null || this.executor.isShutdown()) {
            this.executor = new ThreadPoolExecutor(OUTBOUND_CONNECTIONS_POOL_SIZE, OUTBOUND_CONNECTIONS_POOL_SIZE,
                    OUTBOUND_CONNECTIONS_THREAD_KEEP_ALIVE_SECS, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<Runnable>(), new ConnectorThreadFactory());
        }

        if (this.thread == null || !this.thread.isAlive()) {
            this.thread = new Thread(this);
            this.thread.setName("bt-serve");
            this.thread.start();
        }
    }

    /**
     * Stop accepting connections.
     *
     * <p>
     * <b>Note:</b> the underlying socket remains open and bound.
     * </p>
     */
    public void stop() {
        this.stop = true;

        if (this.thread != null && this.thread.isAlive()) {
            try {
                this.thread.join();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }

        if (this.executor != null && !this.executor.isShutdown()) {
            this.executor.shutdownNow();
        }

        this.executor = null;
        this.thread = null;
    }

    /**
     * Close this connection handler to release the port it is bound to.
     *
     * @throws IOException If the channel could not be closed.
     */
    public void close() throws IOException {
        if (this.channel != null) {
            this.channel.close();
            this.channel = null;
        }
    }

    /**
     * The main service loop.
     *
     * <p>
     * The service waits for new connections for 250ms, then waits 100ms so it
     * can be interrupted.
     * </p>
     */
    @Override
    public void run() {
        while (!this.stop) {
            try {
                SocketChannel client = this.channel.accept();
                if (client != null) {
                    this.accept(client);
                }
            } catch (SocketTimeoutException ste) {
                // Ignore and go back to sleep
            } catch (IOException ioe) {
                logger.warn("Unrecoverable error in connection handler", ioe);
                this.stop();
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * Return a human-readable representation of a connected socket channel.
     *
     * @param channel The socket channel to represent.
     * @return A textual representation (<em>host:port</em>) of the given
     * socket.
     */
    private String socketRepr(SocketChannel channel) {
        Socket s = channel.socket();
        return String.format("%s:%d%s", s.getInetAddress().getHostName(), s.getPort(),
                channel.isConnected() ? "+" : "-");
    }

    /**
     * Accept the next incoming connection.
     *
     * <p>
     * When a new peer connects to this service, wait for it to send its
     * handshake. We then parse and check that the handshake advertises the
     * torrent hash we expect, then reply with our own handshake.
     * </p>
     *
     * <p>
     * If everything goes according to plan, notify the
     * <code>IncomingConnectionListener</code>s with the connected socket and
     * the parsed peer ID.
     * </p>
     *
     * @param client The accepted client's socket channel.
     */
    private void accept(SocketChannel client) throws IOException, SocketTimeoutException {
        try {
            logger.debug("New incoming connection, waiting for handshake...");
            Handshake hs = this.validateHandshake(client, null);
            int sent = this.sendHandshake(client);
            logger.trace("Replied to {} with handshake ({} bytes).", this.socketRepr(client), sent);

            // Go to non-blocking mode for peer interaction
            client.configureBlocking(false);
            client.socket().setSoTimeout(CLIENT_KEEP_ALIVE_MINUTES * 60 * 1000);
            this.fireNewPeerConnection(client, hs.getPeerId());
        } catch (ParseException pe) {
            logger.info("Invalid handshake from {}: {}", this.socketRepr(client), pe.getMessage());
            IOUtils.closeQuietly(client);
        } catch (IOException ioe) {
            logger.warn("An error occured while reading an incoming " + "handshake: {}", ioe.getMessage());
            if (client.isConnected()) {
                IOUtils.closeQuietly(client);
            }
        }
    }

    /**
     * Tells whether the connection handler is running and can be used to
     * handle new peer connections.
     */
    public boolean isAlive() {
        return this.executor != null && !this.executor.isShutdown() && !this.executor.isTerminated();
    }

    /**
     * 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(SharingPeer peer) {
        if (!this.isAlive()) {
            throw new IllegalStateException("Connection handler is not accepting new peers at this time!");
        }

        this.executor.submit(new ConnectorTask(this, peer));
    }

    /**
     * Validate an expected handshake on a connection.
     *
     * <p>
     * Reads an expected handshake message from the given connected socket,
     * parses it and validates that the torrent hash_info corresponds to the
     * torrent we're sharing, and that the peerId matches the peer ID we expect
     * to see coming from the remote peer.
     * </p>
     *
     * @param channel The connected socket channel to the remote peer.
     * @param peerId The peer ID we expect in the handshake. If <em>null</em>,
     * any peer ID is accepted (this is the case for incoming connections).
     * @return The validated handshake message object.
     */
    private Handshake validateHandshake(SocketChannel channel, byte[] peerId) throws IOException, ParseException {
        ByteBuffer len = ByteBuffer.allocate(1);
        ByteBuffer data;

        // Read the handshake from the wire
        logger.trace("Reading handshake size (1 byte) from {}...", this.socketRepr(channel));
        if (channel.read(len) < len.capacity()) {
            throw new IOException("Handshake size read underrrun");
        }

        len.rewind();
        int pstrlen = len.get();

        data = ByteBuffer.allocate(Handshake.BASE_HANDSHAKE_LENGTH + pstrlen);
        data.put((byte) pstrlen);
        int expected = data.remaining();
        int read = channel.read(data);
        if (read < expected) {
            throw new IOException("Handshake data read underrun (" + read + " < " + expected + " bytes)");
        }

        // Parse and check the handshake
        data.rewind();
        Handshake hs = Handshake.parse(data);
        if (!Arrays.equals(hs.getInfoHash(), this.torrent.getInfoHash())) {
            throw new ParseException("Handshake for unknow torrent "
                    + Torrent.byteArrayToHexString(hs.getInfoHash()) + " from " + this.socketRepr(channel) + ".",
                    pstrlen + 9);
        }

        if (peerId != null && !Arrays.equals(hs.getPeerId(), peerId)) {
            throw new ParseException(
                    "Announced peer ID " + Torrent.byteArrayToHexString(hs.getPeerId())
                            + " did not match expected peer ID " + Torrent.byteArrayToHexString(peerId) + ".",
                    pstrlen + 29);
        }

        return hs;
    }

    /**
     * Send our handshake message to the socket.
     *
     * @param channel The socket channel to the remote peer.
     */
    private int sendHandshake(SocketChannel channel) throws IOException {
        return channel.write(
                Handshake.craft(this.torrent.getInfoHash(), this.id.getBytes(Torrent.BYTE_ENCODING)).getData());
    }

    /**
     * Trigger the new peer connection event on all registered listeners.
     *
     * @param channel The socket channel to the newly connected peer.
     * @param peerId The peer ID of the connected peer.
     */
    private void fireNewPeerConnection(SocketChannel channel, byte[] peerId) {
        for (IncomingConnectionListener listener : this.listeners) {
            listener.handleNewPeerConnection(channel, peerId);
        }
    }

    private void fireFailedConnection(SharingPeer peer, Throwable cause) {
        for (IncomingConnectionListener listener : this.listeners) {
            listener.handleFailedConnection(peer, cause);
        }
    }

    /**
     * A simple thread factory that returns appropriately named threads for
     * outbound connector threads.
     *
     * @author mpetazzoni
     */
    private static class ConnectorThreadFactory implements ThreadFactory {

        private int number = 0;

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setName("bt-connect-" + ++this.number);
            return t;
        }
    }

    /**
     * An outbound connection task.
     *
     * <p>
     * These tasks are fed to the thread executor in charge of processing
     * outbound connection requests. It attempts to connect to the given peer
     * and proceeds with the BitTorrent handshake. If the handshake is
     * successful, the new peer connection event is fired to all incoming
     * connection listeners. Otherwise, the failed connection event is fired.
     * </p>
     *
     * @author mpetazzoni
     */
    private static class ConnectorTask implements Runnable {

        private final ConnectionHandler handler;
        private final SharingPeer peer;

        private ConnectorTask(ConnectionHandler handler, SharingPeer peer) {
            this.handler = handler;
            this.peer = peer;
        }

        @Override
        public void run() {
            InetSocketAddress address = new InetSocketAddress(this.peer.getIp(), this.peer.getPort());
            SocketChannel channel = null;

            try {
                logger.info("Connecting to {}...", this.peer);
                channel = SocketChannel.open(address);
                while (!channel.isConnected()) {
                    Thread.sleep(10);
                }

                logger.debug("Connected. Sending handshake to {}...", this.peer);
                channel.configureBlocking(true);
                int sent = this.handler.sendHandshake(channel);
                logger.debug("Sent handshake ({} bytes), waiting for response...", sent);
                Handshake hs = this.handler.validateHandshake(channel,
                        (this.peer.hasPeerId() ? this.peer.getPeerId().array() : null));
                logger.info("Handshaked with {}, peer ID is {}.", this.peer,
                        Torrent.byteArrayToHexString(hs.getPeerId()));

                // Go to non-blocking mode for peer interaction
                channel.configureBlocking(false);
                this.handler.fireNewPeerConnection(channel, hs.getPeerId());
            } catch (Exception e) {
                if (channel != null && channel.isConnected()) {
                    IOUtils.closeQuietly(channel);
                }
                this.handler.fireFailedConnection(this.peer, e);
            }
        }
    }
}