Всем привет! В данной статье я расскажу о том, что из себя представляет библиотека Bt, чем она хороша и почему стоит иметь ее в виду, если для решения каких-то задач планируется использовать BitTorrent. Затем, в качестве демонстрации базовых функций и основного API, мы реализуем простейший консольный торрент-клиент.


Отличительные особенности Bt


Bt — это современная полнофункциональная реализация протокола BitTorrent на Java 8 [1]. По сравнению с существующими open-source аналогами [2][3], Bt обладает следующими преимуществами:


  • Модульный дизайн, обеспечивающий простоту доработки и расширения. В основе архитектуры находятся IoC-контейнер Guice и стандартный механизм java.util.ServiceLoader. Guice обеспечивает прозрачное добавление новых компонентов и переопределение стандартных сервисов, а ServiceLoader предельно упрощает сборку клиента, состоящего из нескольких модулей, в т.ч. находящихся в разных jar’ах.
  • Отсутствие привязки к механизму хранения данных. Стандартное хранилище предполагает наличие файловой системы, но, благодаря использованию интерфейса java.nio.file.Path, поддерживает в т.ч. и in-memory файловые системы, такие как Jimfs [4].
  • Полная поддержка механизма расширения стандартного протокола собственными типами сообщений [5], что может оказаться полезным в случае разработки нестандартного клиента для специфических нужд.
  • Относительно невысокий CPU и memory footprint и весьма шустрая производительность даже при наличии большого количества обрабатываемых торрентов и сетевых соединений, не в последнюю очередь благодаря использованию NIO и однопоточного ядра для отправки и приема сообщений (в то время как альтернативные реализации для простоты используют отдельный поток на каждое соединение и блокирующий ввод-вывод).

В число обязательных для серьезного BitTorrent-клиента опций, которые поддерживаются в Bt, входят:


  • Широкие возможности по тюнингу и конфигурации [6]
  • Интеграция с Mainline DHT [7]
    Поддержка HTTP и UDP трекеров, в т.ч. мультитрекеров и приватных трекеров
  • Поиск локальных пиров через multicast [8]
  • Обмен информацией о пирах с другими участниками раздачи [9]
  • Обфускация траффика с использованием сеансовых ключей и асимметричного шифрования [10]
  • Параллельная закачка/раздача нескольких торрентов
  • Выборочное скачивание отдельных файлов
  • И наконец, работа с magnet-ссылками. Для скачивания торрента достаточно указать его уникальный идентификатор в виде ссылки:
    magnet:?xt=urn:btih:af0d9aa01a9ae123a73802cfa58ccaf355eb19f1

Создание простейшего command-line клиента


Ради минимизации телодвижений и во избежание ненужных ошибок я рекомендую не пытаться воспроизводить код проекта по тексту статьи, а сразу скачать готовый проект с github [11].


Конфигурация проекта


Для удобства использования исполняемый файл клиента будет представлять собой fat jar: классы и ресурсы приложения и его зависимостей будут собраны в единый архив. Создадим новый Maven проект, в pom.xml которого объявим класс, содержащий метод main(), название исполняемого файла, внешние зависимости и пару плагинов.


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.atomashpolskiy</groupId>
    <artifactId>bt-cli-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Bt CLI Launcher</name>
    <description>Command line BitTorrent client</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <compiler.source>1.8</compiler.source>
        <compiler.target>1.8</compiler.target>

        <main.class>bt.cli.CliClient</main.class>

        <bt-version>1.7</bt-version>
        <jopts-version>5.0.2</jopts-version>
        <slf4j-version>1.7.21</slf4j-version>
        <log4j-version>2.4.1</log4j-version>
    </properties>

    <build>
        <finalName>bt-launcher</finalName>
        ...
    </build>

    <dependencies>
        <dependency>
            <groupId>com.github.atomashpolskiy</groupId>
            <artifactId>bt-core</artifactId>
            <version>${bt-version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.atomashpolskiy</groupId>
            <artifactId>bt-http-tracker-client</artifactId>
            <version>${bt-version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.atomashpolskiy</groupId>
            <artifactId>bt-dht</artifactId>
            <version>${bt-version}</version>
        </dependency>
        <dependency>
            <groupId>net.sf.jopt-simple</groupId>
            <artifactId>jopt-simple</artifactId>
            <version>${jopts-version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j-version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j-version}</version>
        </dependency>
    </dependencies>

</project>

В качестве зависимостей мы указали три стандартных модуля Bt:


  • bt-core: “ядро” библиотеки; содержит базовую функциональность, которая не использует внешних зависимостей
  • bt-http-tracker-client: модуль интеграции с HTTP трекерами
  • bt-dht: модуль интеграции с Mainline DHT

Также нам потребуется Log4J (в т.ч. бридж к SLF4J) и любимая многими библиотека JOpt Simple [12], делающая работу с аргументами командной строки простой и приятной.


Сразу же добавим конфигурацию конфигурацию логирования log4j2.xml, но здесь ее текст приводить не будем. Скажем лишь, что приложение будет осуществлять логирование в два файла: bt.log и bt-dht.log. Во второй файл будут попадать события и сообщения, связанные с работой DHT, которые в большинстве случаев не представляют для пользователя большого интереса.


Исходный код


Окей, с настройкой проекта покончено, приступаем к написанию кода. В следующем разделе мы посвятим некоторое время написанию необходимой “обвязки” для обработки аргументов программы и настройки JRE. Самые нетерпеливые могут пропустить данный раздел и перейти сразу к написанию кода торрент-клиента.


Опции, режимы работы и параметры JRE


Даже такое небольшое приложение как торрент-клиент может иметь большое количество конфигурационных параметров, зависящих от целей использования и текущего окружения. Определим список опций, которые хотелось бы предоставить пользователю нашего приложения:


  • вывод справки (help)
  • указание .torrent файла или magnet-ссылки для скачивания
  • указание директории для сохранения файлов
  • активация режима принудительной обфускации траффика (может пригодиться, если ISP пользователя режет/шейпит BitTorrent-трафик и при этом не использует продвинутые средства статистического анализа траффика, которые могут определить его природу даже при использовании обфускации)
  • активация режима последовательной загрузки (например, для проигрывания медиа-файлов параллельно с загрузкой)
  • отключение интерактивного выбора файлов для загрузки (чтобы скачивались все файлы, которые содержатся в торренте; это может потребоваться, если список файлов большой)
  • активация режима сидирования после завершения загрузки (по умолчанию клиент будет завершать работу, как только все файлы были скачаны)
  • указание используемого IP адреса и портов (отдельно для BitTorrent и DHT соединений)
  • запрос более подробного логирования для отладки в случае проблем

Список опций и парсинг аргументов спрячем в отдельном классе Options, а здесь приведем лишь изначальную версию главного метода программы bt.cli.CliClient.main().


package bt.cli;

??import joptsimple.OptionException;??

public class CliClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(CliClient.class);
    ?
    public static void main(String[] args) {?
        Options options;?
        try {?
            options = Options.parse(args);
        ?} catch (OptionException e) {?
            Options.printHelp(System.out);
            return;?
        }
    ?}?
}

Теперь наше приложение умеет выводить симпатичную справку!


Option (* = required)  Description                                             
---------------------  -----------                                             
-?, -h, --help                                                                 
-S, --sequential       Download sequentially                                   
-a, --all              Download all files (file selection will be disabled)    
* -d, --dir <File>     Target download location                                
--dhtport <Integer>    Listen on specific port for DHT messages                
-e, --encrypted        Enforce encryption for all connections                  
-f, --file <File>      Torrent metainfo file                                   
-i, --inetaddr         Use specific network address (possible values include IP
                         address literal or hostname)                          
-m, --magnet           Magnet URI                                              
-p, --port <Integer>   Listen on specific port for incoming connections        
-s, --seed             Continue to seed when download is complete              
--trace                Enable trace logging                                    
-v, --verbose          Enable more verbose logging

С обработкой опций почти что все, осталось только настроить Log4J и установить параметры JRE для корректной работы обфускации. Добавим в main() вызов нескольких служебных методов.


configureLogging(options.getLogLevel());?
configureSecurity();?
registerLog4jShutdownHook();

Методы, связанные с настройкой Log4J, не представляют сейчас для нас большого интереса, посмотреть их можно здесь и здесь. Метода configureSecurity() коснемся чуть подробнее.


Дело в том, что протокол обфускации использует шифрование с использованием сеансовых ключей, минимальный рекомендуемый размер которых составляет 160 бит. Согласно американским законам, которые регулируют дистрибуцию ПО (а значит, неизбежно касаются и Oracle JDK), предельный допустимый размер ключей для шифрования по умолчанию не может превышать 128 бит. Использовать ключи большего размера не запрещено, но пользователь должен сам выполнить необходимые настройки и “разблокировать” такую возможность. Конфигурация Bt позволяет установить размер ключей от 128 до 4096 бит, но в данном случае мы хотели бы оставить оптимальное значение, установленное по умолчанию, и настроить-таки JRE. Вплоть до Oracle JRE версии 8u152 для этого необходимо было скачать jar файл с сайта Oracle [13] и подменить одноименный файл в установленном дистрибутиве. Начиная с версии 8u152, того же эффекта можно добиться, просто установив переменную окружения crypto.policy=unlimited [14]. Именно это и делает метод configureSecurity().


private static void configureSecurity() {
    // Starting with JDK 8u152 this is a way 
    //   to programmatically allow unlimited encryption
    // See http://www.oracle.com/technetwork/java/javase/8u152-relnotes-3850503.html
    String key = "crypto.policy";
    String value = "unlimited";
    try {
        Security.setProperty(key, value);
    } catch (Exception e) {
        LOGGER.error(String.format(
              "Failed to set security property '%s' to '%s'", key, value), e);
    }
}

Таким образом, мы выбираем компромиссный вариант:


  • если у пользователя “свежая” JRE, то все будет работать “из коробки”;
  • если пользователь не запросил принудительную обфускацию всего траффика, то Bt автоматически отключит возможность устанавливать соединения с пирами, использующими обфускацию; при этом plaintext соединения будут работать в штатном режиме;
  • если JRE старая и не настроенная, а пользователь запрашивает обфускацию, то Bt прекратит работу с ошибкой, попросив пользователя выполнить настройку JRE.

Фух! Недаром Java славится своим многословием и запутанностью. Приступаем к финальной части…


Интеграция с Bt


Код торрент-клиента будет состоять из конструктора, нескольких вспомогательных методов и метода для запуска. Для начала рассмотрим конструктор.


private final Options options;
private final SessionStatePrinter printer;
private final BtClient client;

public CliClient(Options options) {
    this.options = options;
    this.printer = new SessionStatePrinter();

    Config config = buildConfig(options);

    BtRuntime runtime = BtRuntime.builder(config)
            .module(buildDHTModule(options))
            .autoLoadModules()
            .build();

    Storage storage = new FileSystemStorage(options.getTargetDirectory().toPath());
    PieceSelector selector = options.downloadSequentially() ?
        SequentialSelector.sequential() : RarestFirstSelector.randomizedRarest();

    BtClientBuilder clientBuilder = Bt.client(runtime)
            .storage(storage)
            .selector(selector);

    if (!options.shouldDownloadAllFiles()) {
        CliFileSelector fileSelector = new CliFileSelector();
        clientBuilder.fileSelector(fileSelector);
        runtime.service(IRuntimeLifecycleBinder.class)
                   .onShutdown(fileSelector::shutdown);
    }

    clientBuilder.afterTorrentFetched(printer::onTorrentFetched);
    clientBuilder.afterFilesChosen(printer::onFilesChosen);

    if (options.getMetainfoFile() != null) {
        clientBuilder = clientBuilder.torrent(toUrl(options.getMetainfoFile()));
    } else if (options.getMagnetUri() != null) {
        clientBuilder = clientBuilder.magnet(options.getMagnetUri());
    } else {
        throw new IllegalStateException("Torrent file or magnet URI is required");
    }

    this.client = clientBuilder.build();
}

Пройдемся по коду, делая необходимые пояснения.


Config config = buildConfig(options);

Метод buildConfig() создает конфигурацию рантайма Bt. Рантайм — это контейнер для клиентов, каждый из которых выполняет обработку своего торрента. Основные функции рантайма:


  • Сборка модулей в единый IoC контейнер, содержащий общие для всех клиентов сервисы и расширения.
  • Предоставление разделяемых ресурсов, таких как пул соединений, центр событий, компоненты, выполняющие взаимодействие с трекерами и поиск пиров, сервер DHT, и т.д.
  • Управление жизненным циклом приложения: запуск и останов сервисов, вызов пользовательских колбэков.

Отдельный клиент представляет собой небольшую, легковесную обертку над несколькими специфичными для конкретного торрента объектами (контекстом). Его задача заключается в последовательном выполнении стадий обработки конкретного типа торрента (.torrent файл или magnet-ссылка) и предоставлении пользователю API для запуска и останова обработки.


Соответственно, настройка Bt выполняется на двух уровнях:


  • Конфигурация рантайма, которая содержит общие для всех клиентов параметры: IP адрес, порты, интервалы запуска внутренних задач, таймауты интеграции с внешними агентами (трекерами, пирами), различные лимиты и ограничения (на количество соединений, размер блока данных, передаваемого по сети, глубину очереди I/O операций, и т.д. и т.п.
  • Конфигурация отдельного клиента, которая включает в себя специфичные для конкретного торрента параметры: режим выбора и загрузки блоков (последовательная загрузка или рандомизированный выбор наиболее редких блоков), источник метаданных (.torrent файл или magnet-ссылка), директория для сохранения, колбэки, которые следует вызвать после завершения определенного этапа загрузки, и т.д.

Рассмотрим код создания конфигурации рантайма.


private static Config buildConfig(Options options) {
    Optional<InetAddress> acceptorAddressOverride = 
                      getAcceptorAddressOverride(options);
    Optional<Integer> portOverride = tryGetPort(options.getPort());

    return new Config() {
        @Override
        public InetAddress getAcceptorAddress() {
            return acceptorAddressOverride.orElseGet(super::getAcceptorAddress);
        }

        @Override
        public int getAcceptorPort() {
            return portOverride.orElseGet(super::getAcceptorPort);
        }

        @Override
        public int getNumOfHashingThreads() {
            return Runtime.getRuntime().availableProcessors();
        }

        @Override
        public EncryptionPolicy getEncryptionPolicy() {
            return options.enforceEncryption()? 
                EncryptionPolicy.REQUIRE_ENCRYPTED 
                     : EncryptionPolicy.PREFER_PLAINTEXT;
        }
    };
}

private static Optional<Integer> tryGetPort(Integer port) {
    if (port == null) {
        return Optional.empty();
    } else if (port < 1024 || port > 65535) {
        throw new IllegalArgumentException("Invalid port: " + port + 
                              "; expected 1024..65535");
    }
    return Optional.of(port);
}

private static Optional<InetAddress> getAcceptorAddressOverride(Options options) {
    String inetAddress = options.getInetAddress();
    if (inetAddress == null) {
        return Optional.empty();
    }
    try {
        return Optional.of(InetAddress.getByName(inetAddress));
    } catch (UnknownHostException e) {
        throw new IllegalArgumentException(
             "Failed to parse the acceptor's internet address", e);
    }
}

Здесь мы на основании указанных пользователем параметров создаем новый экземпляр класса bt.runtime.Config, в котором переопределяем ряд методов, с тем чтобы они возвращали указанное пользователем значение, если оно есть, или значение по умолчанию в противном случае.


Стоит обратить внимание на два параметра.


Первый — это numOfHashingThreads, или число потоков, которые будут выполнять первоначальную верификацию уже скачанных данных ("хэширование" на общепринятом жаргоне; оно необходимо при перезапуске клиента). По умолчанию Bt использует всего один поток, но процедура верификации прекрасно поддается параллелизации, поэтому имеет смысл использовать несколько потоков. Оптимальное число потоков находится в интервале [количество ядер; количество ядер * 2], т.к. отдельные потоки могут простаивать в ожидании завершения очередной I/O операции чтения.


Второй параметр — это политика применения обфускации траффика. Политики используются в протоколе установления соединения с пирами, и их всего четыре [15]:


1) Не применять обфускацию и не устанавливать соединения с пирами, которые требуют применения обфускации.
2) По умолчанию предлагать пиру использовать plaintext, но соглашаться на обфускацию, если этого требует пир.
3) По умолчанию предлагать пиру использовать обфускацию, но соглашаться на plaintext, если этого требует пир.
4) Всегда применять обфускацию и не устанавливать соединения с пирами, которые требуют использовать plaintext.


В нашем приложении мы выбираем между двумя наиболее распространенными политиками: принудительная обфускация или обфускация по требованию пира. Первая политика подходит для параноидальных пользователей (и недобросовестных ISP), а вторая позволяет устанавливать соединения с максимальным количеством пиров.


BtRuntime runtime = BtRuntime.builder(config)
                .module(buildDHTModule(options))
                .autoLoadModules()
                .build();

Сборка рантайма обычно сводится к двум вещам:


  • Определению списка необходимых модулей (в т.ч. отключению стандартных расширений, таких как обмен информацией о пирах и поиск локальных пиров).
  • Включению поиска и автозагрузки модулей, присутствующих в classpath приложения (в нашем случае это модули, объявленные в pom.xml).

Помимо основного модуля в нашем приложении присутствуют два модуля расширений: модуль интеграции с HTTP трекерами и модуль интеграции с Mainline DHT. Первый модуль будет найден и загружен автоматически благодаря вызову autoLoadModules(), а для второго модуля мы хотим указать нестандартную конфигурацию и поэтому переопределяем его вручную.


private static Module buildDHTModule(Options options) {
    Optional<Integer> dhtPortOverride = tryGetPort(options.getDhtPort());

    return new DHTModule(new DHTConfig() {
        @Override
        public int getListeningPort() {
            return dhtPortOverride.orElseGet(super::getListeningPort);
        }

        @Override
        public boolean shouldUseRouterBootstrap() {
            return true;
        }
    });
}

Мы переопределяем два параметра:


  • Порт, на котором DHT сервер будет слушать входящие сообщения.
  • Разрешение на использование “публичной” DHT инфраструктуры (общедоступных bootstrap-нод таких как router.bittorrent.com); в случае использования BitTorrent для целей, отличных от скачивания и раздачи файлов в Интернете, этот параметр следует оставить установленным в false и указать список собственных DHT-нод (в этой роли может выступать любой инстанс Bt).

Storage storage = new FileSystemStorage(options.getTargetDirectory().toPath());
PieceSelector selector = options.downloadSequentially() ?
        SequentialSelector.sequential() : RarestFirstSelector.randomizedRarest();

BtClientBuilder clientBuilder = Bt.client(runtime)
                .storage(storage)
                .selector(selector);

Следующий шаг — это указание директории для сохранения загруженных файлов. Как уже упоминалось выше, конструктор класса bt.data.file.FileSystemStorage принимает параметр типа java.nio.file.Path, что позволяет использовать его в сочетании с in-memory файловыми системами, такими как JimFS. В самой библиотеке такая возможность используется в интеграционных тестах (с тем, чтобы сэкономить время выполнения на файловом I/O), но, гипотетически, могут быть и более экзотические варианты использования [16], например:


  • Последовательная загрузка текстовых файлов непосредственно в консоль в духе утилиты more.
  • Медиа-проигрыватель, который воспроизводит файлы “на лету” и не использует устройство хранения данных.

if (!options.shouldDownloadAllFiles()) {
        CliFileSelector fileSelector = new CliFileSelector();
        clientBuilder.fileSelector(fileSelector);
       runtime.service(IRuntimeLifecycleBinder.class)
              .onShutdown(fileSelector::shutdown);
}

Продолжаем собирать клиент. Мы предоставили пользователю возможность пропустить этап выбора файлов на тот случай, если их слишком много, или если пользователь хочет загрузить всю раздачу. Если эта опция не указана, то мы предоставляем Bt собственную реализацию селектора файлов, ориентированную на работу в условиях терминального интерфейса.


public class CliFileSelector extends TorrentFileSelector {
    private static final String PROMPT_MESSAGE_FORMAT = 
           "Download '%s'? (hit <Enter> or type 'y' to confirm or type 'n' to skip)";
    private static final String ILLEGAL_KEYPRESS_WARNING = 
           "*** Invalid key pressed. Please, use only <Enter>, 'y' or 'n' ***";

    private AtomicReference<Thread> currentThread;
    private AtomicBoolean shutdown;

    public CliFileSelector() {
        this.currentThread = new AtomicReference<>(null);
        this.shutdown = new AtomicBoolean(false);
    }

    @Override
    protected SelectionResult select(TorrentFile file) {
        while (!shutdown.get()) {
            System.out.println(getPromptMessage(file));

            try {
                switch (readNextCommand(new Scanner(System.in))) {
                    case "":
                    case "y":
                    case "Y": {
                        return SelectionResult.select().build();
                    }
                    case "n":
                    case "N": {
                        System.out.println("Skipping...");
                        return SelectionResult.skip();
                    }
                    default: {
                        System.out.println(ILLEGAL_KEYPRESS_WARNING);
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        throw new IllegalStateException("Shutdown");
    }

    private static String getPromptMessage(TorrentFile file) {
        return String.format(PROMPT_MESSAGE_FORMAT, 
                           String.join("/", file.getPathElements()));
    }

    private String readNextCommand(Scanner scanner) throws IOException {
        currentThread.set(Thread.currentThread());
        try {
            return scanner.nextLine().trim();
        } finally {
            currentThread.set(null);
        }
    }

    public void shutdown() {
        this.shutdown.set(true);
        Thread currentThread = this.currentThread.get();
        if (currentThread != null) {
            currentThread.interrupt();
        }
    }
}

От нас требуется реализовать класс с одним методом, который для каждого отдельного файла определяет, как следует поступить: скачать или пропустить. В данном случае мы в интерактивном режиме запрашиваем у пользователя нужное действие. Если пользователь нажимает Enter или вводит “y”, то файл будет скачан, а если пользователь вводит “n”, то файл будет пропущен.


clientBuilder.afterTorrentFetched(printer::onTorrentFetched);
clientBuilder.afterFilesChosen(printer::onFilesChosen);

Как уже упоминалось выше, для каждого клиента можно определить набор колбэков, которые будут вызваны по завершении очередной фазы обработки торрента:


  • После того как загружены метаданные (из файла или magnet-ссылки), чтобы вывести информацию о торренте на экран (не забываем, что изначально у программы есть только путь к файлу или вообще 20-байтный идентификатор в виде URI).
  • После того как выбраны файлы для загрузки.

В нашем случае мы оповещаем компонент отрисовки UI об изменении статуса торрента, чтобы он мог вывести соответствующие информационные сообщения для пользователя.


Отдельного метода для указания колбэка, который следует выполнить после завершения загрузки, нет, зато есть удобный метод stopWhenDownloaded(), чтобы автоматически завершить работу клиента. В нашем приложении мы его не используем, т.к. возможно, нам потребуется сидировать раздачу.


if (options.getMetainfoFile() != null) {
        clientBuilder = clientBuilder.torrent(toUrl(options.getMetainfoFile()));
} else if (options.getMagnetUri() != null) {
        clientBuilder = clientBuilder.magnet(options.getMagnetUri());
} else {
        throw new IllegalStateException("Torrent file or magnet URI is required");
}

this.client = clientBuilder.build();

Завершаем создание клиента указанием источника метаданных, в нашем случае это либо .torrent файл, либо magnet-ссылка. В прочих случаях можно передать абстрактный java.util.function.Supplier<Torrent>, в котором будет реализовано получение метаданных (например, из базы данных).


Дело осталось за малым: запустить клиент на выполнение! Для этого добавим пару строк в main() и реализуем метод для запуска, который также станет последним в нашем приложении.


public static void main(String[] args) throws IOException {
    Options options;
    try {
        options = Options.parse(args);
    } catch (OptionException e) {
        Options.printHelp(System.out);
        return;
    }

    configureLogging(options.getLogLevel());
    configureSecurity();
    registerLog4jShutdownHook();

    CliClient client = new CliClient(options);
    client.start();
}

// прочий код опущен...

private void start() {
    printer.start();
    client.startAsync(state -> {
        boolean complete = (state.getPiecesRemaining() == 0);
        if (complete) {
            if (options.shouldSeedAfterDownloaded()) {
                printer.onDownloadComplete();
            } else {
                printer.stop();
                client.stop();
            }
        }
        printer.updateState(state);
    }, 1000).join();
}

В методе start() мы видим новую конструкцию, а именно метод клиента startAsync(). Он определен в двух версиях, одна из которых не имеет параметров, а другая — та, которую мы используем, — принимает на вход функцию-слушателя, которая будет вызываться клиентом с определенной периодичностью (здесь — раз в 1 секунду).


Задачи слушателя очень простые:


  • Передавать актуальную информацию о статусе обработки торрента в компонент отрисовки UI (в т.ч. об объеме переданных данных, подключенных пирах и т.д.)
  • Отслеживать момент завершения загрузки.

В нашем приложении мы не можем воспользоваться автоматическим завершением работы после загрузки, т.к. пользователь может выбирать, оставаться ли ему на раздаче после скачивания всех файлов, поэтому мы используем более сложный подход с функцией-слушателем.


Итак, на этом работа над кодом приложения завершена. За бортом остался только класс SessionStatePrinter, который управляет отрисовкой UI. Он довольно простой и носит исключительно утилитарный характер, поэтому мы его рассматривать не будем, а перейдем к тестированию разработанного приложения.


Тестируем то, что получилось


Статья и так получилась довольно объемная, так что, вместо того чтобы и дальше продолжать утомлять вас текстом и многочисленными скриншотами, я прикрепляю анимированный ролик.


image


В конце в System.out логируется InterruptedException, это ограничение бесплатной версии это небольшая шероховатость, которая никак не влияет на работу приложения.


Магнет-ссылка на раздачу, используемую в ролике:


magnet:?xt=urn:btih:985BAD472E60E763F5C77B13CBE41AE2892604B6


Все файлы, присутствующие в раздаче, находятся в public domain. Это собрание сочинений Марка Твена в 12 томах в переводе на русский язык Государственного издательства художественной литературы 1959-1961 гг.


Заключение


Итак, на этом заканчивается наше первое знакомство с библиотекой Bt. Мы рассмотрели основные возможности и базовый API и собрали свой небольшой BitTorrent клиент, пусть и не такой навороченный как его старшие собратья uTorrent, Transmission, Deluge и пр., но обладающий достаточным набором функций для того, чтобы пользоваться им на регулярной основе для скачивания файлов, просмотра фильмов и т.д.


Нельзя сказать, что проект находится в начале пути: в скором времени ему исполняется 2 года с момента написания первых строк кода. В то же время еще многое предстоит реализовать, дополнить и исправить. Сам протокол BitTorrent тоже не стоит на месте, недавно вышла спецификация второй версии [17], поддержку которой очень хотелось бы включить и в Bt.


Меня очень радует тот факт, что несмотря на свой возраст BitTorrent по прежнему составляет значительную часть Интернет траффика. Это прекрасно спроектированный протокол синхронизации данных, который, к несчастью, в представлении многих синонимичен с пиратством и воровством. Я надеюсь, что качественная реализация протокола на Java может подстегнуть интерес к использованию BitTorrent в благих целях, в т.ч. внутри организаций, на мобильных устройствах на базе Android и для создания новых продуктов и экосистем [18].


Спасибо, что дочитали до конца! В комментариях с удовольствием отвечу на все ваши вопросы.


Ссылки


[1] https://github.com/atomashpolskiy/bt
[2] https://github.com/mpetazzoni/ttorrent
[3] https://github.com/bitletorg/bitlet
[4] https://github.com/google/jimfs
[5] http://atomashpolskiy.github.io/bt/extension-protocol/
[6] http://atomashpolskiy.github.io/bt/javadoc/latest/bt/runtime/Config.html
[7] https://en.wikipedia.org/wiki/Mainline_DHT
[8] https://en.wikipedia.org/wiki/Local_Peer_Discovery
[9] https://en.wikipedia.org/wiki/Peer_exchange
[10] https://en.wikipedia.org/wiki/BitTorrent_protocol_encryption
[11] https://github.com/atomashpolskiy/bt-cli-demo
[12] https://pholser.github.io/jopt-simple/
[13] http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
[14] http://www.oracle.com/technetwork/java/javase/8u152-relnotes-3850503.html
[15] http://atomashpolskiy.github.io/bt/encryption/
[16] https://github.com/atomashpolskiy/bt/issues/9
[17] http://bittorrent.org/beps/bep_0052.html
[18] https://www.makeuseof.com/tag/8-legal-uses-for-bittorrent-youd-be-surprised/

Комментарии (5)


  1. Moxa
    27.02.2018 14:19

    выглядит очень интересно, а последовательную загрузку с учетом приоритетов файлов оно умеет?


    1. sadpotato Автор
      27.02.2018 18:16

      Спасибо! Пока что умеет только просто последовательную в том порядке, в котором файлы размещены в торренте


  1. AnterKan
    27.02.2018 18:55

    Недавно как раз была задача, в которой нужно было использовать торрент. Ваша либа не подошла по одной причине: java 8. Пришлось использовать конкурента #2 :)


    1. sadpotato Автор
      27.02.2018 20:13

      Если не секрет, какого рода задача? По работе или в личном проекте?


      1. AnterKan
        28.02.2018 11:08

        По работе. Кстати наш форк так же уже использует NIO и модульный:)
        github.com/JetBrains/ttorrent-lib/tree/master