Персональный блог Игоря Антонова aka "spider_net"

Программируем torrent-клиент на C#


Рубрика: c# -> Программирование -> Журнал Хакер -> Статьи
Метки: | | |
Просмотров: 6718
Программируем torrent-клиент на C#

«Шесть лет прошло со времен первой войны людей и орков…» Действительно, прошло уже несколько месяцев с момента выхода статьи, в которой мы на практике разобрали процесс создания и парсинга torrent-файлов. К большому сожалению, до самого вкусного момента (взаимодействия с трекером) мы добрались только сегодня – из-за проблем с отладкой готового примера. Лишь после нескольких сеансов электростимуляции толстым зондом со стороны редактора рубрики я смог это дело осилить и облечь в суровые строки журнальной статьи.

C# вместо Delphi

Для первой части статьи я писал пример на моем любимом Delphi, но сегодня мне предстоит ему изменить и воспользоваться великим и могучим C#. Многие Delphi-ненавистники возрадуются и громко закричат: «Неужели на Delphi нельзя создать полноценный клиент?». Вовсе нет, на Delphi можно написать практически любое приложение и торрент-клиент – не исключение, но есть одно но. Как ты понимаешь, протокол BitTorrent – это не хухры-мухры и просто так реализовать его в приложении не удастся. В настоящее время для дельфина не существует ни одной нормальной библиотеки/модуля для упрощения взаимодействия с этим протоколом. Все те библиотеки, которые мне попадались на глаза, морально устарели и требовали переписывания до 60% кода. Заниматься переписыванием и изобретением очередного велосипеда – очень долго и нудно, а Dr.Klouniz все повышал вольтаж на моих электродах, двигая гигантским реостатом и раскатисто хохоча. Поэтому я забил на эту идею и занялся поисками альтернативных библиотек – и на этот раз я искал не для Delphi, а для C#. К счастью, тут поиски были недолгими. Буквально на второй странице результатов, Гугл выдал мне ссылку на продвинутую библиотеку для работы с протоколом BitTorrent. Недолго думая, я взял курс по найденному линку и оказался на сайте проекта – MonoTorrent for C#.

Разрешите представиться: MonoTorrent

Библиотека Monotorrent – одна из самых профессиональных и функциональных среди имеющихся альтернатив. Для программиста она предоставляет шикарный API, позволяющий использовать протокол BitTorrent с чрезвычайной легкостью, не задумываясь о лишних проблемах. Автор этого замечательного творения – Alan McGovern. Изначально библиотека входила в проект Summer of Code 2006. Но после того как она засветилась и стала набирать фанатов, Alan решил заняться доработкой MonoTorrent и сделать отдельный проект. Вот так и появился на свет MonoTorrent, который сегодня используют все C#'ники. Петь дифирамбы MT можно очень долго, поэтому давай отвлечемся от этого занимательного, но бесполезного дела и взглянем на четыре ключевые особенности, ради которых стоит юзать именно эту либу:

  • Упрощенная процедура для создания/чтения torrent-файлов. Все то, что мы научились делать в первой части статьи, в MonoTorrent делается в пару строчек кода. На скорость работы такая универсальность и упрощенность не повлияли – она находится на высоте и все действия (парсинг файлов, общение с сервером, переиндексация не закачанного файла) происходят очень быстро.
  • Функции на любой случай! Возможностей MN с лихвой хватит как для разработки клиентских приложений (например, Torrent-клиентов), так и серверных (например, Tracker-серверов). Все необходимые классы уже реализованы.
  • Простота использования. Код MonoTorrent написан очень качественно. Поэтому, если ты нормально ориентируешься в ООП, то без труда сможешь разобраться с внутренностями этой библиотеки и по необходимости дописать пару возможностей самостоятельно.
  • Кросс-платформенность. Разработчик MT нехило постарался и сделал свое детище полностью кросс-платформенным. Поэтому грани между платформами размываются, и ты можешь разрабатывать хоть под Unix-like системы (смотри документацию по проекту Mono, хоть под Windows Mobile.
  • Установка MonoTorrent

    Все, хватит занудной теории, давай переходить к долгожданной практике. Перед тем как использовать эту мощную либу, тебе нужно ее скачать и проинсталлить. Самую последнюю версию библиотеки ты всегда можешь найти на http://www.monotorrent.com. Распакуй найденный/скачанный архив и попробуй произвести перекомпиляцию всех файлов. Минута ожидания и... на тебя обрушивается водопад error'ов, в которых сообщается о невозможности обнаружения каких-то модулей. Не отчаивайся, сейчас мы это исправим. В качестве лекарства тебе придется добыть один очень популярный у программистов C# фреймворк и подключить его к своей Visual Studio. Беги на http://nunit.org и качай наиболее свежую версию дистрибутива фреймворка (чтобы далеко не бегать, достаточно просто заглянуть к нам на DVD). Установка фреймворка стандартная. Все, что от тебя требуется – просто закрыть Visual Studio и запустить скачанный инсталлятор. После завершения установки твоя среда разработки уже будет знать о местоположении библиотеки, а значит, тебе нужно вновь попробовать компильнуть сорцы MonoTorrent. На этот раз компиляция пройдет успешно.

    Делаем проект вместе с MonoTorrent

    Создавай в своей студии новый проект типа Console Application. Да-да, ты не ослышался, сегодня мы будем делать именно консольный торрент-клиент. Создал? Теперь потрудись и подключи к своему проекту новый «Reference», расположенный в файле MonoTorrent.dll. Сам файлик MonoTorrent.dll ты можешь найти в папке <директория с файлами monotorrent>/bin/debug. Если ты пришел к нам из Delphi и до этого никогда не юзал C# и Visual Studio, то знай же, что для подключения новой References (ссылки) необходимо:

  • Перейти в Solution Explorer (View -> Solution Explorer);
  • Раскрыть группу Solution;
  • Щелкнуть правой кнопкой и выбрать пункт Add Reference;
  • В появившемся окне перейти на вкладку browse и выбрать файл MonoTorrent.dll;
  • После выполнения этой нехитрой процедуры тебе станут доступны все возможности MT. Теперь можно отвлечься от всяких организационных вопросов и приступить непосредственно к кодингу. Первое, с чего должен начинаться любой проект – с определения списка необходимых пространств имен. К имеющемуся списку добавь:

    MonoTorrent.BEncoding; //здесь сосредоточена вся работа с BenCoding.
    MonoTorrent.Common; //основные методы.
    MonoTorrent.Client.Tracker; //методы для работы с трекером.
    MonoTorrent.Client; //клиентские функции.

    Итак, пространства имен подключены, пора переходить к основной части и заняться приготовлением фарша для автоматически созданного класса Main. Перейди в самое начало описания класса и объяви несколько полей:

    //Путь к папке, из которой мы работаем
    static string _programPath;
    //Папка, в которую будем качать
    static string _downloadPath;
    //Имя и путь к файлу, который будет содержать служебную информацию, необходимую для возобновления закачки
    static string _fastResumeFile;
    //Путь к торрент-файлу.
    static string _torrentPath;
    //Движок, реализующий функции закачки
    static ClientEngine _engine; 
    //вспомогательный класс
    static Top10Listener _listener;
    //Менеджер для хранения законченных настроек для очередного torrent-файла
    static TorrentManager _manager;

    Пока ты переписываешь, я буду комментировать происходящее в листинге. Наше приложение будет консольным, а значит, нужно организовать привычный для таких приложений интерфейс взаимодействия с пользователем. Ты уже наверняка догадался, что речь идет о параметрах, которые мы так любим передавать подобным тулзам. Из параметров наша программа должна принимать, как минимум, два: путь к торрент-файлу, который необходимо закачать, и папку на жестком диске, куда нужно все это сохранить. Поскольку мы точно знаем, что параметров будет два, то при запуске программы нам нужно убедиться, что так и есть. Именно это я и делаю в самой первой строчке. Если «длина» args меньше 2, то сообщим пользователю, что не хватает параметров, и преспокойно прервем работу. Успешно получив параметры, я записываю их в соответствующие переменные. Помимо этого мне приходится определять путь к текущей директории (папке, из которой работает софтина).

    В ней мы станем сохранять файл temp.data, который будет содержать необходимые сведения для возобновления закачки. Кому захочется иметь торрент-клиент, который не имеет возможности докачивать? Закончив возню с переменными, нужно позаботиться о настройке обработчиков событий. Это необходимо сделать, чтобы в определенный момент наша программа, например, могла нормально приостановить свою работу и не наделать ошибок. В первую очередь позаботимся о корректном завершении нашего приложения и установим делагат для события CancelKeyPress. Оно возникает, если мы попытались прервать работу приложения и нажали в консоли. Поскольку приложение у нас достаточно непростое, нужно позаботиться о корректном завершении всех запущенных потоков. Весь код для правильного прерывания работы определим в функции exit() (ее код ты можешь посмотреть в моих исходниках). Эту же функцию нужно вызывать во время срабатывания:

  • AppDomain.CurrentDomain.ProcessExit (завершение процесса);
  • AppDomain.CurrentDomain.UnhandledException (необработанного исключения);
  • Установив обработчики событий, я вызываю процедуру doDownload(), в которой реализована процедура приема файла. Ее код приведен во второй врезке. Поэтому тебе ничего не нужно делать, кроме как начать его переписывать.

    if (args.Length < 2)
    {
    Console.WriteLine("Please run this program with
    parameters:");
    Console.WriteLine("<torrent path> <Download
    folder>");
    Console.ReadKey();
    return;
    }
    _programPath = Environment.CurrentDirectory;
    _torrentPath = args[0]; 
    _downloadPath = args[1];
    _fastResumeFile = _programPath + "\temp.data";
    _listener = new Top10Listener(10);
    Console.CancelKeyPress += 
    delegate { exit(); };
    AppDomain.CurrentDomain.ProcessExit +=
    delegate { exit(); };
    AppDomain.CurrentDomain.UnhandledException +=
    delegate(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine(e.ExceptionObject); exit(); };
    Thread.GetDomain().UnhandledException += 
    delegate(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine(e.ExceptionObject); exit(); };
    doDownload();

    Вторая врезка получилась достаточно объемной, но поверь мне, она была бы еще больше (раз в 20), если бы мы писали все вручную и не прибегали бы к помощи MonoTorrent (Пользуясь случаем, наподдам автору еще пару сотен джоулей в качестве компенсации за задержку статьи, – Прим. ред.). Итак, листинг начинается с определения порта для входящих подключений. Для своего примера я выбрал порт с номером 31337. Ради удобства использования и универсальности, номер порта можно передавать через параметры. Определив порт, нужно создать экземпляр класса Torrent. С ним мы будем выполнять загрузку торрент-файла и получать все необходимые сведения. Вытащить из него можно много чего – далее перечислены соответствующие свойства:

  • CreatedBy - автор создания торрент-файла;
  • CreationDate - дата создания файла;
  • Comment – комментарий;
  • AnouncedUrls - список анонсов;
  • Size - размер файла(ов) для закачки;
  • Piecelength - размер одной части;
  • Pieces.Count - количество частей.
  • Если ты читал первую часть статьи, то уже понял, что эта вся та информация, над получением которой мы корпели в Delphi. Ну что поделать – здесь нам помогает библиотека, а в Delphi приходилось работать руками и головой. Проинициализировав объект для работы с торрент-файлом, нужно начать подготовку «движка», который будет содержать настройки очередной закачки. Но перед тем как перейти к движку, подготовим для него набор опций. Для этого я создаю новый экземпляр класса EngineSettings() и заполняю его основные свойства: SavePath (путь для сохранения файлов) и ListenPort (порт для входящих подключений). Определившись с опциями, я начинаю инициализацию самого движка (ClientEngine) и передаю ссылку на подготовленные EngineSettings. Оп-па, я немного забежал вперед и совершенно несправедливо обделил вниманием создание TorrentsSettings. В них ты можешь задать основные настройки, которые будут влиять на закачку в плане скорости. Например, в моем случае при инициализации переменной типа TorrentSettings я передаю следующие параметры:

  • Слоты для отдачи;
  • Количество одновременных соединений;
  • Ограничение скорости на закачку (0 – без ограничений);
  • Ограничение скорости на отдачу (0 – без ограничений).
  • На этом с настройками все. Двигаемся дальше. Нам нужно создать или прочитать «индексный» файл. Создаем – если он не существует и у нас новая закачка. А читают его так: BEncodedValue.Decode(File.ReadAllBytes(_fastResumeFile). Затем я загружаю torrent-файл. Загрузка выполняется методом load(). В качестве одного-единственного параметра он принимает путь к файлу. Если во время загрузки возникли ошибки, то сообщим об этом пользователю и прервем выполнение программы, ну а если все тип-топ, то выведем информацию о загруженном торрент-файле. Вот теперь мы подошли к самому интересному – к закачке.

    Перед тем, как зарегистрировать torrent-файл для «движка», нам необходимо определиться, будем ли мы продолжать докачку или же начнем лить абсолютно новый файл. Для новой закачки мы просто создадим новый торрент-менеджер (new TorrentManager(_torrent,_downloadPath, _torrentDef);), а вот если закачка уже была запущена, то нужно передать наш _fastResume. После этого нам ничего не остается сделать, как выполнить метод Register проинициализированного «движка».

    В качестве параметров этому методу передадим ссылку на созданный торрент-менеджер. После регистрации на менеджер будут действовать все параметры, которые мы ранее установили. Итак, уже почти все готово для начала закачки, за исключением одного нюанса – обработчиков событий. Их нужно объявить (вспоминаем про делегаты!), чтобы получить возможность следить за состоянием процесса закачки. Дабы не париться с расписыванием кода реакций на события, я просто взял стандартный шаблон (из дистрибутива MT) и немного подкорректировал. Его код ты найдешь в моем исходнике. Сложного в нем ничего нет, и если ты более-менее знаешь C#, то проблем не возникнет.

    Создав соответствующие обработчики, можно стартовать закачку – с помощью метода Start() объекта типа TorrentManager. Закачка началась, и теперь все, что нам остается делать – выводить пользователю соответствующие информационные сообщения. В листинге, опубликованном в статье, есть соответствующий комментарий. Именно в этом месте и нужно написать код для вывода сообщений. Пример такого кода ты опять-таки найдешь в исходнике.

    int _port;
    _port = 31337;
    Torrent _torrent;
    EngineSettings _engineSettings =
    new EngineSettings();
    TorrentSettings _torrentDef =
    new TorrentSettings(5, 100, 0, 0);
    _engineSettings.SavePath = _downloadPath;
    _engineSettings.ListenPort = _port;
    _engine = new ClientEngine(_engineSettings);
    BEncodedDictionary _fastResume;
    try
    {
    fastResume =
    BEncodedValue.Decode<BEncodedDictionary>
    (File.ReadAllBytes(_fastResumeFile));
    }
    catch
    {
    _fastResume = new BEncodedDictionary();
    }
    try
    {
    _torrent = Torrent.Load(_torrentPath);
    }
    catch
    {
    Console.Write("Decoding error");
    _engine.Dispose();
    exit();
    }
    Console.WriteLine("Created by: {0}",
    _torrent.CreatedBy);
    Console.WriteLine("Creation date: {0}",
    _torrent.CreationDate);
    Console.WriteLine("Comment: {0}",
    _torrent.Comment);
    Console.WriteLine("Publish URL: {0}",
    _torrent.PublisherUrl);
    Console.WriteLine("Size: {0}",
    _torrent.Size);
    Console.WriteLine("Piece length: {0}",
    _torrent.PieceLength);
    Console.WriteLine("Piece count: {0}",
    _torrent.Pieces.Count);
    Console.WriteLine("Press any key
    for continue...");
    Console.ReadKey();
    if (_fastResume.ContainsKey(_torrent.InfoHash))
    _manager = new TorrentManager(
    _torrent, _downloadPath, _torrentDef,
    new FastResume((BEncodedDictionary)
    _fastResume[_torrent.InfoHash]));
    else
    _manager = new TorrentManager(_torrent,
    _downloadPath, _torrentDef);
    _engine.Register(_manager);
    _manager.TorrentStateChanged +=
    delegate(object o, TorrentStateChangedEventArgs e)
    {
    lock (_listener)
    _listener.WriteLine("Last status: " +
    e.OldState.ToString() + " Current status: " +
    e.NewState.ToString());
    };
    foreach (TrackerTier ttier in 
    _manager.TrackerManager.TrackerTiers)
    {
    foreach (MonoTorrent.Client.Tracker.Tracker
    tr in ttier.Trackers)
    {
    tr.AnnounceComplete +=
    delegate(object sender,
    AnnounceResponseEventArgs e)
    {
    _listener.WriteLine(string.Format("{0}: {1}",
    e.Successful, e.Tracker.ToString()));
    };
    }
    }
    _manager.Start();
    int i = 0;
    bool _running = true;
    StringBuilder _stringBuilder =
    new StringBuilder(1024);
    while (_running)
    {
    if ((i++) % 10 == 0)
    {
    if (_manager.State == TorrentState.Stopped) {
    _running = false;
    exit();
    //Здесь можно выводить всевозможную полезную информацию (скорость закачки, количество активных соединений и т.д.).
    }
    }
    System.Threading.Thread.Sleep(500);

    Потестим?

    Настал час триумфа, – мы должны убедиться, что наши действия не пропали втуне, и наш торрент-клиент действительно сможет выполнить свою основную задачу. Попробуй скомпилировать и запустить проект. Не забудь при запуске передать соответствующие параметры! Чтобы убедиться в работоспособности нашего детища, я подготовил самый обычный торрент-файл (скачал с tfile.ru) и запустил клиент со следующими параметрами: xtorrent.exe C:\test.torrent C:\ .

    Через пару секунд я увидел в своей консоли информацию о торрент-файлике (рисунок «Вся информация о закачке») и предложение начать загрузку. Согласился – и спустя еще мгновение мой торрент-клиент успешно соединился с трекером и приступил к закачке. Через минут пять в корне моего диска C: появился соответствующий файл, а Torrent-клиент завершил свою работу.

    Статья опубликована в журнале "Хакер" (http://xakep.ru). Октябрь 2008 г.

    Ссылка на журнал: http://goo.gl/3S5WLF

    Оставьте комментарий!
    comments powered by HyperComments