Программируем torrent-клиент на Delphi
Рубрика: Статьи -> Журнал Хакер -> Программирование -> Delphi
Метки: bittorent | delphi | md5 | sha | torrent | программирование
Просмотров: 11892
Тебе не надоело сливать файлы с файлообменников типа rapidshare.com? Лично меня уже достали их ограничения. К счастью, если хорошая альтернатива – Bittorent трекеры. Сегодня мы попробуем поковырять этот протокол и написать свой продвинутый клиент.
Теория bittorent
Как обычно, перед тем как ринуться в бой, тщательно продумаем стратегию и разберемся с теорией. «Bittorent» – протокол для сетей типа p2p и предназначен он для передачи больших файлов по сети. Первая версия протокола появилась в 2001 году. К настоящему времени Bittorent стал очень популярным, более того – он является стандартом быстрого распространения файлов. Популярным его сделали ряд особенностей:
Общаемся по понятиям
Чтобы понять, о чем речь в статье, необходимо разобраться с терминами. Знать их должен любой пользователь 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)
В спецификации структуры файла метаданных есть несколько предопределенных директив:
Практика
Мы уже рассмотрели достаточно теории. Самое время что-нибудь накодить. К сожалению, рассмотреть написание всего Bittorent-клиента в рамках одной статьи невозможно, поэтому сегодня мы напишем первую часть – редактор torrent-файлов. Итак, к делу. Запускай Delphi и создавай новый проект. Дизайн можешь подогнать под мой вариант.
По всей форме у меня растянут компонент TPageControl с двумя созданными закладками. На первой («Содержимое torrent») расположен компонент TListView. В этом компоненте мы будем хранить название и размеры файлов, которые впоследствии будем добавлять в торрент. Ради удобства отображения я установил у TListview свойство ViewStyle в vsReport и создал три колонки: файл, размер, путь. На второй закладке я разместил восемь компонентов TEdit, один TMemo и одну копию TDateTimePicker.
В этих компонентах мы будем выводить различную информацию, выдернутую из torrent-файла. Для комфортного отображения даты создания торрент-файла я воспользовался компонентом TDateTimePicker. С ним мы избавимся от лишних преобразований полученной даты. Центр управления нашей программой будет находиться на панели инструментов. На ней я создал пять кнопок:
На этом с дизайном формы пора заканчивать и переходить к самому захватывающему и увлекательному процессу – кодингу. Сегодня я покажу, как можно читать и создавать торрент-файлы. Переходи в редактор кода и сразу объяви новую структуру:
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», я передаю два параметра:
Тестирование
На сегодня скучный урок программирования можно считать оконченным, а значит нужно протестировать наше творение. Скомпилируй и запусти наш пример. Попробуй заполнить все 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