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

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


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

Тебе не надоело сливать файлы с файлообменников типа rapidshare.com? Лично меня уже достали их ограничения. К счастью, если хорошая альтернатива – Bittorent трекеры. Сегодня мы попробуем поковырять этот протокол и написать свой продвинутый клиент.

Теория bittorent

Как обычно, перед тем как ринуться в бой, тщательно продумаем стратегию и разберемся с теорией. «Bittorent» – протокол для сетей типа p2p и предназначен он для передачи больших файлов по сети. Первая версия протокола появилась в 2001 году. К настоящему времени Bittorent стал очень популярным, более того – он является стандартом быстрого распространения файлов. Популярным его сделали ряд особенностей:

  • Отсутствие очередей. Закачка файлов начинается сразу и без каких-либо ограничений, присущих таким сетям как edonkey.
  • Не требуется постоянное функционирование сервера – трекера. По сути, клиенту достаточно всего один раз подключиться к серверу, чтобы получить информацию о пирах (клиентах, которые занимаются непосредственно раздачей файлов). После чего можно спокойно скачивать файл.
  • Закачка любого файла производится по частям. Тем самым, существенно увеличивается скорость закачки, ведь постоянное присутствие сида (обладателя всех частей файла) необязательно. В случае отсутствия сида будет происходить обмен частей между пирами.
  • Скорость закачки ограничена только шириной канала раздающего. Соответственно, чем больше клиентов, которые раздают файл, тем быстрее ты сможешь его скачать. Это далеко не все плюсы протокола BitTorrent, но их должно хватить, чтобы забыть про ослов и прочие rapid.
  • Общаемся по понятиям

    Чтобы понять, о чем речь в статье, необходимо разобраться с терминами. Знать их должен любой пользователь BitTorrent, а программист, решивший закодить клиент, – и подавно. Уверен, что ты и так их знаешь, но некоторая систематизация не помешает. Начнем с основ. Трекер (tracker) – сервер, на котором хранятся IP-адреса участников раздачи, рейтинг участников и хэши файлов. Главная задача трекера – предоставить возможность клиентам найти друг друга.

    Пир (peer) – клиент, участвующий в раздаче. Как правило, пиру присуще два состояния – закачка и отдача уже скачанных частей файла.

    Сид (seed) – пир, который уже скачал весь файл полностью и располагает всеми его сегментами. Чтобы стать сидом, не обязательно скачивать какой-либо файл, можно просто начать раздачу своего добра.

    Личер – он же пиявка (leech) – пир, у которого еще нет всех частей файла, но он продолжает закачку. В большинстве случаев, термин используется в негативном смысле. Так называют клиентов, которые скачивают больше, чем отдают. Толпа/Рой (swarm) – все пиры, участвующие в раздаче.

    Рейтинг (share ratio) – соотношение отданного и скачанного. Рейтинг необходим, в первую очередь, для пополнения контента. Работает по следующему принципу: скачивая файл, ты уменьшаешь свой рейтинг, а отдавая файл – наоборот, увеличиваешь. Клиент с маленьким рейтингом рискует быть забаненным и у него меньше возможностей, чем у клиента с более высоким (например, отсутствует одновременная закачка нескольких торрентов).

    Анонс (announce) – отправка информации на сервер. В качестве отправителя выступает клиент, а в качестве информации – соотношение скачанного и отданного. Получив эти данные, трекер передает клиенту IP-адреса других клиентов.

    Анонс URL (Announce URL) – адрес трекера. Именно по этому адресу и происходит отправка информации.

    Торрент/торрент-файл (torrent) – файл метаданных, в котором содержится информация о принимаемых/раздаваемых файлах, количестве сегментов и их хэшах. Подробнее о структуре этого файла мы поговорим позже.

    Как все это работает?

    С основными понятиями разобрались, а значит, пора переходить к более детальному рассмотрению Bittorent. Перед тем, как приступить к закачке/раздаче файлов, необходимо скачать соответствующий торрент-файл. Как правило, торренты добываются из всевозможных форумов, вроде torrents.ru или через специальные поисковые системы вроде piratebay.org. Скачав торрент-файл, ты должен скормить его торрент-клиенту. Далее все просто: клиент соединяется с трекером (анонс url хранится в торрент-файле), сообщает ему свой IP и хэш необходимого файла, в ответ сервер отправляет адреса пиров/сидов участвующих в раздаче этого файла. После этого необходимость в трекере на некоторое время исчезает. Ты качаешь и обмениваешься с другими пирами.

    Обмен с другими пирами выглядит так: ты посылаешь запрос на закачку сегмента нужного файла определенному пиру – если он не против и может поделиться этим кусочком, то начинается процесс закачки. Скачав сегмент, ты оповещаешь остальных пиров о наличии в твоем распоряжении нового кусочка (чтобы другие пиры знали, у кого его качать). Затем все повторяется. Причем повторяется с шага, на котором тебе необходимо соединиться с сервером и получить информацию о других пирах. Закончив закачку всего файла, ты получаешь статус сида. На рисунке 2 приведена схема, демонстрирующая процесс работы по протоколу BitTorrent.

    Структура торрент-файла

    В файле метаданных (торрент-файле), как я уже и говорил, находится вся информация по файлу (или файлам), участвующему в раздаче. Без него по протоколу Bittorent скачать ничего не удастся. В общем виде структуру файла метаданных можно разделить на три составляющие. Внутренности torrent-файла – это bencoding-данные. Формат файла позволяет хранить следующие типы данных: байт-строки, числа, списки и директивы.

    На первый взгляд, на рисунке показана «каша» непонятных данных. Сразу возникает чувство, что все сложно и запутанно. На самом деле, сложного ничего нет. Давай попробуем рассмотреть примеры записи bencoding-данных.

    Начнем с правил записи строк. В общем виде формат записи строковых данных выглядит так:

    СТРОКИ
    <длина строки>:<строка>. Пример: 5:xakep
    Числа
    <ключ i><число><ключ e>. Пример: i31337e
    Списки
    <ключ l><bencoding данные><ключ e>. Пример: l5:xakep5:lamere
    Директивы
    <ключ d><строка bencoding><элемент bencoding><ключ e>. Пример: d5:coder6:spidere (Coder => spider)

    В спецификации структуры файла метаданных есть несколько предопределенных директив:

  • info – директива для описания свойств файлов. В зависимости от типа торрент-файла (обычный – один файл или смешанный – несколько файлов) эта директива применяется по-разному. В директиву входят:
  • piece length – длина сегмента файла; pieces – хэш сумма сегмента, полученная по алгоритму SHA1. Разницу применения директивы для обычного и смешанного режимов смотри в таблице 2;
  • announce – анонс URL;
  • announce list – список, содержащий несколько announce URL;
  • create date – дата создания torrent файла в формате Unix-time;
  • comment – комментарий от создателя торрент-файла;
  • created by – название и версия программы, в которой был создан torrent-файл.
  • Практика

    Мы уже рассмотрели достаточно теории. Самое время что-нибудь накодить. К сожалению, рассмотреть написание всего Bittorent-клиента в рамках одной статьи невозможно, поэтому сегодня мы напишем первую часть – редактор torrent-файлов. Итак, к делу. Запускай Delphi и создавай новый проект. Дизайн можешь подогнать под мой вариант.

    По всей форме у меня растянут компонент TPageControl с двумя созданными закладками. На первой («Содержимое torrent») расположен компонент TListView. В этом компоненте мы будем хранить название и размеры файлов, которые впоследствии будем добавлять в торрент. Ради удобства отображения я установил у TListview свойство ViewStyle в vsReport и создал три колонки: файл, размер, путь. На второй закладке я разместил восемь компонентов TEdit, один TMemo и одну копию TDateTimePicker.

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

  • OpenTorrentBtn – кнопка для открытия torrent-файла;
  • SaveTorrentBtn – пимпа для создания нового торента;
  • NewBtn – служит для очистки все элементов формы;
  • AddFileBtn – кнопка для добавления нового файла в torrent;
  • DelFileBtn – кнопка для удаления файла из торрента.
  • На этом с дизайном формы пора заканчивать и переходить к самому захватывающему и увлекательному процессу – кодингу. Сегодня я покажу, как можно читать и создавать торрент-файлы. Переходи в редактор кода и сразу объяви новую структуру:

    type
     TPieces = record
      _hash : string;
      _hashBin: string;
     end;

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

    В разделе «private» нашей формы объяви процедуру CreateTorrent (fs:TFileStream; multifile:Boolean). Этим методом мы будем создавать новый торрент-файл. В качестве параметров в процедуру будут передаваться fs – переменная типа TFIleStream (файловый поток для создания торрент-файла) и булевское значение (multifile), определяющее тип будущего торрент-файла (обычный или смешанный). Нажимай <ctrl+shift+с> и Delphi создаст заготовку процедуры. Перепиши в нее код из соответствующей врезки.

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

    Procedure WriteBuff(buff:string) записывает в файловый поток первый символ из переданной в качестве параметра строки-переменной buff. Если ты внимательно читал теорию, то уже должен был догадаться, что использовать эту процедуру мы будем для записи «ключей» bencoding-данных. Процедуры WriteStr() и WriteInt() имеют аналогичное предназначение и будут использоваться для записи строк (WriteStr()) и чисел (WriteInt()).

    Разобравшись с локальными процедурами, можно двигаться дальше. Впереди будет много интересного. Сейчас на секундочку отвлекись от текста и посмотри на таблицу 1, в которой я определил уровни структуры torrent-файла. Первым уровнем идет «Заголовок», а значит, самым первым шагом в нашей процедуре будет формирование заголовка будущего торрент-файла.

    Формирование заголовка я начинаю с записи ключа – d, а затем по очереди записываю имена элементов (так называемые директивы) и их значения, которые мы будем вводить в компонентах TEdit, расположенных на второй закладке. Запись элементов однообразна и я думаю, все должно быть понятным. Хотя нет, процесс записи времени создания стоит рассмотреть подробней. Как я уже говорил в теоретической части, время создания торрент-файла должно храниться в формате Unix-time. К сожалению, в Delphi среди стандартных функций нет той, которая могла бы конвертировать время в Unix-time и обратно. Следовательно, подобную функцию придется писать самому. А может и не придется, ведь во всемирной паутинке легко найти примеры кода, реализующего конвертирование. Вариантов много, но мне больше всех нравится этот:

    function TForm1.WinTimeToUnixTime (winTime: TDateTime): Integer;
    var
     FileTime: TFileTime;
     SystemTime: TSystemTime;
     I: Integer;
    begin
     DateTimeToSystemTime(WinTime, SystemTime);
     SystemTimeToFileTime(SystemTime, FileTime);
     
     I := Integer(FileTime.dwHighDateTime) shl 32
        + FileTime.dwLowDateTime;
      
      Result := (I - 116444736000000000) div Int64(10000000);
    end;

    Код функции выполняющей обратную трансформацию времени смотри в исходнике, который дожидается тебя на нашем DVD. После записи в файл основных директив начинается процесс описания файлов (запись директивы info). Тут мы встаем перед выбором: если создаем торрент с типом «смешанный» (multiFile), то нам необходимо запустить цикл и пробежаться по всему списку выбранных файлов и записать в torrent размеры/пути для каждого файла. В качестве путей (директива path) указывается не тот путь, по которому хранится файл на диске, а тот, который определяет местоположения файла относительно торрента. Например, формат файла метаданных (торрент) позволяет добавлять как файлы, так и директории.

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

    Он у меня равен 65536 байтам или 64 килобайтам. Если ты будешь писать полноценную программу для создания торрент-файлов, то должен предоставлять пользователю самостоятельное право выбора размера сегмента, так как этот размер задается не от балды (как у меня в примере), а относительно общего размера файлов, для которых мы создаем торрент. Для разбиения файла по сегментам и получения хэшей я создал еще одну процедуру – GetPieces(). Ее содержимое ты можешь увидеть в листинге №2. После выполнения данной процедуры массив pieces заполнится элементами типа TPieces, содержащими посчитанные хэш суммы сегментов файлов.

    Процедура GetPieces() принимает всего один параметр – размер сегмента файла. После получения информации о размере сегмента в процедуре запускается цикл, в ходе которого перебираются все файлы из TListView и для каждого из них вычисляется SHA1 хэш. Полученные данные записываются в динамический массив _pieces типа TPieces (структура, которую мы определили в самом начале). Для вычисления хэш-суммы я воспользовался объектом TSha1 из модуля MessageDigest от Dave Shapiro. Работать с алгоритмами SHA1, Md5 (и другими) с помощью этого модуля одно удовольствие. Все, что требуется для получения хеша – воспользоваться методом Transform(), после чего в свойствах hashValue и hashValueBytes появится рассчитанная хэш-сумма. Больше в процедуре ничего интересного нет, поэтому перейдем сразу к завершающему шагу – наполним кнопку SaveTorrentBtn жизнью. Создай для нее обработчик события OnClick и напиши в нем:

    var
     _NewFile:TFileStream;
    begin
     If (SaveDialog1.Execute) Then
     begin
      _NewFile := TFileStream.Create
     (SaveDialog1.FileName+'.torrent', fmCreate);
     if (ListView1.Items.Count = 1) Then
      CreateTorrent(_NewFile, false)
     else
      CreateTorrent(_NewFile, true);
     end;

    В этом коде я инициализирую переменную типа файловый поток. Вызывая метод «Create», я передаю два параметра:

  • имя файла (файл с таким именем мы будем создавать);
  • режим доступа к файлу. Поскольку нам нужно создать новый файл, то указываем fmCreate.
  • Тестирование

    На сегодня скучный урок программирования можно считать оконченным, а значит нужно протестировать наше творение. Скомпилируй и запусти наш пример. Попробуй заполнить все TEdit, добавить файлы в ListView и сохранить собранный проект в виде торрента. Если у тебя все прошло без ошибок, то не спеши радоваться, так как основное тестирование только начинается. Скачай какой-нибудь torrent клиент (например, uTorrent) и попробуй открыть им получившийся у тебя файл. Если все тип-топ, то uTorrent пропарсит подсунутый ему файлик и предложит начать закачку. Но если uTorrent ругнется и сообщит ошибку, то значит, ты где-то накосячил и придется провести немало времени в играх с отладчиком. Код чтения торрент-файла я приводить не стал – статья не резиновая. Зато на диске, ты найдешь полный работоспособный исходник. В чтении файла нет ничего сложно. Раз уж ты смог разобраться с созданием торрент-файла, то с чтением проблем возникнуть не должно.

    Создание torrent-файла

    procedure TForm1.CreateTorrent(fs:TFileStream; multifile:boolean);
     procedure WriteBuff(buff:string);
     begin
      fs.WriteBuffer(buff[1], length(buff));
     end;
     procedure WriteStr (s:string);
     begin
      WriteBuff(IntToStr(length(s))+':'+s);
     end;
     procedure WriteInt (int:int64);
     begin
      WriteBuff('i');
      WriteBuff(IntToStr(int));
      WriteBuff('e');
     end;
    VAR
     i:integer;
     _pieceLength:Integer;
    BEGIN
     WriteBuff('d');
     WriteStr('announce');
     WriteStr(AnnounceUrlEdit.Text);
     If (CommentsMemo.Text <> '') Then
     begin
      WriteStr('comment');
      WriteStr(CommentsMemo.Text);
     end;
     If (ProgNameEdit.Text <> '') Then
     begin
      WriteStr('created by');
      WriteStr(ProgNameEdit.Text);
     end;
     WriteStr('creation date');
     WriteInt(WinTimeToUnixTime(DateCreate.DateTime));
     If (EncodingEdit.Text <> '') Then
     begin
      WriteStr('encoding');
      WriteStr(EncodingEdit.Text);
     end;
     WriteStr('info');
     WriteBuff('d');
     If (MultiFile) Then
     begin
      WriteStr('files');
      WriteBuff('l');
      for i:=0 to ListView1.Items.Count-1 Do
       with listView1.Items.Item[i] do
       begin
        WriteBuff('d');
        WriteStr('length');
        WriteInt(StrToInt(SubItems.Strings[0]));
        WriteStr('path');
        WriteBuff('l');
        WriteStr(ExtractFileName(SubItems.Strings[1]));
        WriteBuff('e');
        WriteBuff('e');
       end;
      WriteBuff('e');
     end
     else
      begin
       WriteStr('length');
       WriteInt(StrToInt(ListView1.Items.Item[0].SubItems.Strings[0]));
      end;
     WriteStr('name');
     WriteStr(NameEdit.Text);
     _pieceLength := 65536;
     WriteStr('piece length');
     WriteInt(_pieceLength);
     WriteStr('pieces');
     GetPieces(_pieceLength);
     WriteBuff(IntToStr((high(pieces)+1)*20));
     WriteBuff(':');
     for i:=0 to High(pieces) Do
      WriteBuff(pieces[i]._hashBin);
     WriteBuff('e');
     WriteBuff('e');
     Fs.Free;
     ShowMessage('Torrent файл создан!');
    END;

    Содержимое GetPieces().

    var
    _sha1 : TSHA1;
    _data : Integer;
    _buff : pchar;
    _pieceCount:Integer;
    _file : TFileStream;
    _hashBinStr : string;
    _p : Pointer;
    _PieceSize:integer;
    i:integer;
    begin
    _pieceSize := pieceLength;
    _sha1 := TSHA1.Create;
    _pieceCount := 0;
    GetMem(_Buff, pieceLength);
    pieces := nil;
    for i:=0 to listView1.Items.Count-1 do
    begin
     _file := TFileStream.Create
     (ListView1.Items.Item[i].SubItems[1], fmOpenRead);
     _file.Seek(0, soFromBeginning);
     
     repeat
      _data := _file.Read(_buff^, _pieceSize);
      _sha1.Transform(_buff^, _data);
      If (_data = _pieceSize) or ((_file.Position = _file.Size)
       and (i = ListView1.Items.Count-1)) Then
      begin
       _sha1.Complete;
       _p := _sha1.HashValueBytes;
      SetLength(_hashBinStr, 20);
      
      move(_p^,_hashBinStr[1],20);
      inc(_pieceCount);
      SetLength(pieces, _pieceCount);
      pieces[_pieceCount-1]._hash := lowercase(_sha1.HashValue);
      pieces[_pieceCount-1]._hashBin := _hashBinStr;
      _Sha1.Clear;
     _pieceSize := pieceLength;
     end;
    Until (_file.Position = _file.Size);
    Dec(_pieceSize, _data);
    file.Free;
    end;
    end;

    Заключение

    Уже не первый раз убеждаешься в том, что все нервные крики в сторону Delphi – это просто бред и комплексы фанатов С++ (данная фраза проверена этическим комитетом; выдана справка о том, что провокационной она не является, будучи написанной автором в состоянии аффекта – Прим. ред). На Delphi можно написать практически любую программу, будь то компактная хакерская тулза или продвинутая программа для работы с БД. Мне остается только попрощаться с тобой и пожелать удачи в кодинге. Все свои вопросы ты можешь задать мне по мылу – буду рад пообщаться. До встречи!

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

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

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