Прошло 8 лет после написания статьи Игорем Антоновым (Spider_NET) про создание торрент-клиента на C#, но в сети так и не появилось самого простого примера, как это можно сделать на Delphi.

Чтобы развеять сомнения по поводу неэффективности языка Delphi в таком «непростом» деле, как написание полноценного битторрент-клиента, я и решил написать эту статью.

Сразу скажу, что наш торрент-клиент на Delphi будет с открытым исходным кодом и будет поддерживать практически все современные битторрент-технологии, в том числе DHT, magnet-ссылки, последовательная закачка и т.д.

Поиск в интернете уже готовых исходников клиента на Delphi привел к результатам, но эти результаты оказались далеко неидеальными. Первым результатом оказался давно заброшенный Torrent Torque (2007г), причём альфа-версия. TorrentTorque мне не удалось нормально скомпилировать и испытать.
Следующим результатом поиска, оказался малоизвестный в рунете Ares Galaxy, который оказался вполне работоспособным и даже популярным в некоторых странах торрент-клиентом. Помучавшись с компиляцией, мне всё же удалось испытать желанный код, но у него оказались недостатки, которые как выяснилось, разработчиками не исправляются уже давно. Кроме того, Ares Galaxy написан на Delphi 7, а это значит, что для компиляции в более новых версиях RAD Studio необходимо переписывать огромное количество кода. Но меня это не остановило и я нашёл другой выход для решения данной задачи.

Начав разбираться в исходниках Ares Galaxy, выяснил, что многие операции выполняются в одном потоке, что в результате кратковременно останавливает процесс закачки всех торрентов в списке. Потому я решил исправить недостатки и вынес процедуры, которые замедляют выполнение кода, в отдельные потоки.

Получив положительный результат, решил разместить исходники на sourceforge.net. Код выполнил в виде dll-библиотеки BTService, с применением системы плагинов, о которой в своё время подробно рассказал Александр Алексеев в своей серии статей «Разработка системы плагинов». Так что с применением такой системы плагинов возможно создание битторрент-клиента в любом компиляторе RAD Studio и не только на Delphi, но и на других языках программирования. Библиотека BTService и её исходники доступны по ссылке: http://btservice.sourceforge.net/

Итак, приступим к написанию простого клиента на основе библиотеки BTService.

Интерфейс клиента выполним в классическом стиле, с минимальным набором компонентов, но чтобы была видна основная функциональность библиотеки.



Пять кнопок на панели инструментов: добавление magnet-ссылки, добавление торрента, создание торрента, запуск торрента и остановка торрента.

Список торрентов будем отображать в стандартном TListView. Списки файлов, подключенных пиров и трекеров также разместим на TListView, которые соответственно будут отображаться при открытии вкладок TPageControl. Ну а внизу главной формы StatusBar, на котором будет отображаться magnet-ссылка выделенного в списке торрента, четыре состояния торрента и общие скорости закачки и отдачи для всех торрентов в списке.

Теперь по порядку разберёмся с событиями создания, запуска и остановки торрентов.

Все подробности связанные с созданием торрента и спецификацией битторрент-протокола описывать не будем, т.к. до нас это сделал Игорь в своей статье «Кодим BitTorrent-клиент. Часть первая». Весь код, выполняющий создание торрента, доступен в библиотеке BTService. Кому интересна его реализация, смотрите исходники библиотеки. Ну а я лишь укажу код, взаимодействующий с библиотекой.

Для начала создадим форму «Создать новый торрент». На которой разместим три кнопки: «Добавить файл», «Добавить папку» и «Создать». Добавим TPageControl. На первой вкладке разместим основные параметры. Параметр «Размер части» выполним в TCombobox, «Начать закачку» и «Частный торрент» выполним в TCheckBox. На других вкладках TPageControl разместим поля TMemo, добавляющие в торрент-файл адреса трекеров, веб-сидов и комментарии. Процесс создания торрента будем отображать на двух TProgressBar. На первом будет отображаться выполнение хэширования отдельного файла, а на другом общий процесс выполнения хэширования всех файлов торрента.



Для кнопки «Добавить файл» код будет следующий:

procedure TfCreateTorrent.btnAddFileClick(Sender: TObject);
begin
  FormStyle := fsNormal;
  if OpenDialog1.Execute then
  begin
    ComboBox1.Text := PChar(OpenDialog1.Filename); // выбор файла
    btnCreate.Enabled := true;
  end;
  FormStyle := fsStayOnTop;
end;

Для кнопки «Добавить папку» код будет следующий:

procedure TfCreateTorrent.btnAddFolderClick(Sender: TObject);
var
  chosenDirectory: string;
begin
  FormStyle := fsNormal;
  if SelectDirectory('Выберите каталог: ', '', chosenDirectory) then
  begin
    ComboBox1.Text := chosenDirectory; // выбор папки
    btnCreate.Enabled := true;
  end;
  FormStyle := fsStayOnTop;
end;

То есть в поле «Выбор источника»(ComboBox1.Text) добавляется путь файла или папки, в зависимости от того, что мы хотим добавить в торрент, один файл или несколько файлов в папке.

Далее на кнопку «Создать» пишем код:

Код создания торрент-файла
procedure TfCreateTorrent.btnCreateClick(Sender: TObject);
var
  BTCreateTorrent: IBTCreateTorrent;
  X: Integer;
  FindBTPlugin: Boolean;
  Id: string;
begin
  if ButtonSave then
  begin
    if ComboBox1.Text[length(ComboBox1.Text)] = '\' then
      ComboBox1.Text := copy(ComboBox1.Text, 1, length(ComboBox1.Text) - 1);

    if FileExists(ComboBox1.Text) then
    begin
      SaveDialog1.Filter := 'Torrent Files (*.torrent)|*.torrent';
      SaveDialog1.Filename := ComboBox1.Text + '.torrent';
      SaveDialog1.DefaultExt := 'Torrent files (*.torrent)';
      FormStyle := fsNormal;
      if SaveDialog1.Execute then
      begin
        FormStyle := fsStayOnTop;
        TorrentFileName := SaveDialog1.Filename; // выбор пути для сохраниния торрент-файла
        ButtonSave := False;
        btnCreate.Caption := 'Остановить';

        EnterCriticalSection(TorrentSection);
        try
          for X := 0 to Plugins.Count - 1 do
          begin
            if (Supports(Plugins[X], IBTCreateTorrent, BTCreateTorrent)) then // поиск интерфейса IBTCreateTorrent, отвечающего за создание торрент-файла
            begin
              FindBTPlugin := true;
              break;
            end;
          end;
        finally
          LeaveCriticalSection(TorrentSection);
        end;

        if FindBTPlugin then
        begin         
            Id := IntToStr(CreateTorrentID)          

          EnterCriticalSection(TorrentSection);
          try
            try
              BTCreateTorrent.SingleFileTorrent((Id), (ComboBox1.Text),
                (SaveDialog1.Filename), (mmoComment.Lines.Text),
                (GetAnnounceURL), (mmWebSeeds.Lines.Text), CheckBox2.checked,
                ComboBox2.ItemIndex, False, False, '', '', '', '');  //запуск процедуры создания торрент-файла из плагина BTService для одиночного файла
            except
            end;
          finally
            LeaveCriticalSection(TorrentSection);
          end;

          repeat
            application.ProcessMessages;

            EnterCriticalSection(TorrentSection);
            try         
              try
                GetInGeted(BTCreateTorrent.GetInfoTorrentCreating(Id)); // получение информации о создании торрент файла  
              except
              end;            
            finally
              LeaveCriticalSection(TorrentSection);
            end;

            if (Stop) and (not(GetedStatus = 'stoped')) then
            begin
              EnterCriticalSection(TorrentSection);
              try      
                try
                  BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла     
                except
                end;           
              finally
                LeaveCriticalSection(TorrentSection);
              end;
            end;

            WaitingCreation;

            if (Stop) and (not(GetedStatus = 'stoped')) then
            begin
              EnterCriticalSection(TorrentSection);
              try          
                try  
                  BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла         
                except
                end;
              finally
                LeaveCriticalSection(TorrentSection);
              end;
            end;

            sleep(10);
          until (GetedStatus = 'completed') or (GetedStatus = 'stoped');
        end;
        
        try
          ReleaseCreateTorrentThread(BTCreateTorrent, Id); // уничтожение потока создания торрент-файла
        except
        end;

        if (GetedStatus = 'completed') then
        begin
          sGauge2.Position := sGauge2.Max;
          sGauge1.Position := sGauge1.Max;
          StatusBar1.Panels[0].Text :=
            'Создание торрент-файла успешно завершено!';

          if CheckBox1.checked then
            StartTorrent;
        end;
        if (GetedStatus = 'stoped') then
        begin
          StatusBar1.Panels[0].Text := 'Остановлено.';
        end;

        Stop := False;
        btnCreate.Enabled := true;
        ButtonSave := true;
        btnCreate.Caption := 'Создать';
      end;
    end
    else if DirectoryExists(ComboBox1.Text) then
    begin
      SaveDialog1.Filter := 'Torrent Files (*.torrent)|*.torrent';
      SaveDialog1.Filename := ComboBox1.Text + '.torrent';
      SaveDialog1.DefaultExt := 'Torrent files (*.torrent)';
      FormStyle := fsNormal;
      if SaveDialog1.Execute then
      begin
        FormStyle := fsStayOnTop;
        TorrentFileName := SaveDialog1.Filename; // выбор пути для сохраниния торрент-файла
        ButtonSave := False;
        btnCreate.Caption := 'Остановить';

        EnterCriticalSection(TorrentSection);
        try
          for X := 0 to Plugins.Count - 1 do
          begin
            if (Supports(Plugins[X], IBTCreateTorrent, BTCreateTorrent)) then // поиск интерфейса IBTCreateTorrent, отвечающего за создание торрент-файла
            begin
              FindBTPlugin := true;
              break;
            end;
          end;
        finally
          LeaveCriticalSection(TorrentSection);
        end;

        if FindBTPlugin then
        begin          
            Id := IntToStr(CreateTorrentID)          

          EnterCriticalSection(TorrentSection);
          try    
            try
              BTCreateTorrent.CreateFolderTorrent((Id), (ComboBox1.Text),
                (SaveDialog1.Filename), (mmoComment.Lines.Text),
                (GetAnnounceURL), (mmWebSeeds.Lines.Text), CheckBox2.checked,
                ComboBox2.ItemIndex, False, False, '', '', '', ''); //запуск процедуры создания торрент-файла из плагина BTService для каталога с файлами        
            except
            end;
          finally
            LeaveCriticalSection(TorrentSection);
          end;

          repeat
            application.ProcessMessages;

            EnterCriticalSection(TorrentSection);
            try              
                GetInGeted(BTCreateTorrent.GetInfoTorrentCreating(Id));  // получение информации о создании торрент файла              
            finally
              LeaveCriticalSection(TorrentSection);
            end;

            if (Stop) and (not(GetedStatus = 'stoped')) then
            begin
              EnterCriticalSection(TorrentSection);
              try        
                try        
                  BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла      
                except
                end;
              finally
                LeaveCriticalSection(TorrentSection);
              end;
            end;

            WaitingCreation;

            if (Stop) and (not(GetedStatus = 'stoped')) then
            begin
              EnterCriticalSection(TorrentSection);
              try    
                try
                  BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла           
                except
                end;
              finally
                LeaveCriticalSection(TorrentSection);
              end;
            end;

            sleep(10);
          until (GetedStatus = 'completed') or (GetedStatus = 'stoped');

          EnterCriticalSection(TorrentSection);
          try
            try
              ReleaseCreateTorrentThread(BTCreateTorrent, Id); // уничтожение потока создания торрент-файла
            except
            end;
          finally
                LeaveCriticalSection(TorrentSection);
          end;

          if (GetedStatus = 'completed') then
          begin
            sGauge2.Position := sGauge2.Max;
            sGauge1.Position := sGauge1.Max;
            StatusBar1.Panels[0].Text :=
              'Создание торрент-файла успешно завершено!';
            if CheckBox1.checked then
              StartTorrent;
          end;
          if (GetedStatus = 'stoped') then
          begin
            StatusBar1.Panels[0].Text := 'Остановлено.';
          end;

          Stop := False;
          btnCreate.Enabled := true;
          ButtonSave := true;
          btnCreate.Caption := 'Создать';
        end;
      end;
    end
    else
    begin
      Stop := False;
      btnCreate.Enabled := true;
      ButtonSave := true;
      btnCreate.Caption := 'Создать';
    end;
  end
  else
  begin
    Stop := true;
    btnCreate.Enabled := False;
    ButtonSave := true;
    StatusBar1.Panels[0].Text := 'Приостановка процесса...';
    btnCreate.Caption := 'Останавливается...';
  end;
end;


Как видно из кода, первым делом происходит выбор каталога для сохранения торрента. А далее происходит поиск интерфейса IBTCreateTorrent, отвечающего за вызов процедуры SingleFileTorrent из плагина BTService. Данная процедура запускает процесс создания торрент-файла с содержанием одного файла, а для папки с файлами запускается процедура CreateFolderTorrent. После этого запускатся цикл repeat, в котором происходит периодическое обращение к функции GetInfoTorrentCreating, которая возвращает результат действий из плагина в процессе создания торрента и информацию о проценте выполненного хеширования. Если результат возвращается GetedStatus = 'completed', то процесс создания торрента завершился удачно и можно выходить из цикла.

Для добавления торрента в список создадим форму «Добавить торрент». Разместим на неё две кнопки: «Закачать» и «Добавить в список». Первая будет добавлять торрент в список и сразу начинать процесс скачивания, а вторая будет просто добавлять торрент в список для ожидания последующих действий над ним. Для отображения информации о торренте добавим на форму TEdit («Файл торрента:»), TComboBox («Сохранить в:»), TLabel(«Имя торрента:»,«Описание:», «Дата:») и список TListView, который будет показывать содержимое файлов и папок торрента.



Код добавления и закачки торрента
procedure TfAddTorrent.btnDownloadClick(Sender: TObject);
begin
  if AddTask(true, false) then
    close;
end;

function TfAddTorrent.AddTask(Now: Boolean; ShowPrev: Boolean): Boolean;
var
  find: Boolean;
  TorrentDataSL: TStringList;
  X: Integer;
  DataTask: TTask;
  BTPluginAddTrackers: IBTServicePluginAddTrackers;
begin
  Result := false;
  if Trim(HashValue) = '' then
  begin
    MessageBox(Handle,
      PChar('Нет доступа к торрент файлу или ошибка чтения торрент-файла'),
      PChar(Options.Name), MB_OK or MB_ICONWARNING or MB_TOPMOST);
    Exit;
  end;

  find := false;
  with TasksList.LockList do
    try
      for X := 0 to Count - 1 do
      begin
        DataTask := Items[X];
        if DataTask.Status <> tsDeleted then
          if DataTask.HashValue = HashValue then
          begin
            find := true;
            break;
          end;
      end;
    finally
      TasksList.UnLockList;
    end;
  if find then
  begin
    if MessageBox(Application.Handle,
      PChar('Вы пытаетесь добавить торрент, который уже есть в списке. Хотите загрузить из него список трекеров?'),
      PChar(Options.Name), MB_OKCANCEL or MB_ICONWARNING) = ID_OK then
    begin
      for X := 0 to Plugins.Count - 1 do
      begin
        if (Supports(Plugins[X], IBTServicePluginAddTrackers,
          BTPluginAddTrackers)) then
        begin
          try
            BTPluginAddTrackers.AddTrackers(HashValue, trackers); // Добавление трекеров из торрент-файла
          except
          end;
          break;
        end;
      end;
    end;
    Exit;
  end;

  TorrentDataSL := TStringList.Create;
  try
    TorrentDataSL.Insert(0, BoolToStr(true));

    if Now then
      TorrentDataSL.Insert(1, BoolToStr(true))
    else
      TorrentDataSL.Insert(1, BoolToStr(false));

    TorrentDataSL.Insert(2, Edit1.Text);
    TorrentDataSL.Insert(3, ExcludeTrailingBackSlash(cbDirectory.Text));
    TorrentDataSL.Insert(4, IntToStr(0));
    TorrentDataSL.Insert(5, Edit2.Text);

    AddTorrent(TorrentDataSL.Text, HashValue, Now, ShowPrev);
  finally
    TorrentDataSL.Free;
  end;

  try
    ForceDirectories(ExcludeTrailingBackSlash(cbDirectory.Text));
  except
  end;

  SaveTasksList;
  Result := true;
end;

function TfAddTorrent.AddTorrent(TorrData: string; HashValue: string;
  Now: Boolean; ShowPrev: Boolean): Boolean;
var
  AddDataTask: TTask;
  AddedData: TStringList;
  CreaName, CreatedName: string;
  Plugin2: IAddDownload;
  IndexPlugin2: Integer;
  Silent: Boolean;
  Down: Boolean;
begin
  Result := false;
  AddedData := TStringList.Create;

  try
    AddedData.Text := TorrData;
    try
      Silent := StrToBool(AddedData[0]);
      Down := StrToBool(AddedData[1]);
    except
      Down := true;
      Silent := true;
    end;
    if Silent then
    begin
      AddDataTask := TTask.Create;
      AddDataTask.TorrentFileName := AddedData[2]; // путь к добавляемому торрент-файлу
      AddDataTask.HashValue := HashValue; // info hash
      AddDataTask.LinkToFile := 'magnet:?xt=urn:btih:' +
        AnsiLowerCase(AddDataTask.HashValue); // magnet-ссылка
      AddDataTask.Directory := ExcludeTrailingBackSlash(AddedData[3]); // директория для сохранения содержимого закачиваемого торрента
      AddDataTask.ID := Options.LastID + 1; // идентификатор в списке торрентов
      Options.LastID := AddDataTask.ID;
      CreaName := AddedData[5];
      CreaName := trimleft(CreaName);
      CreaName := trimright(CreaName);
      CreatedName := CreaName;
      AddDataTask.FileName := CreatedName; // имя файла или каталога закачиваемого торрента
      AddDataTask.Description := ''; 
      if CheckBox1.Checked then
        AddDataTask.ProgressiveDownload := true // функция последовательной закачки включена
      else
        AddDataTask.ProgressiveDownload := false; // функция последовательной закачки отключена

      if Down then
        AddDataTask.Status := tsQueue // добавляем закачку в очередь
      else
        AddDataTask.Status := tsReady; // торрент готов к закачке

      AddDataTask.TotalSize := SizeTorrent; // размер содержимого файлов закачиваемого торрента
      AddDataTask.LoadSize := 0;
      AddDataTask.TimeBegin := 0;
      AddDataTask.TimeEnd := 0;
      AddDataTask.TimeTotal := 0;
      AddDataTask.MPBar := TAMultiProgressBar.Create(nil); // создание прогрессбара

      Plugin2 := nil;
      DeterminePlugin2('bittorrent', IServicePlugin, Plugin2, IndexPlugin2);
      if Plugin2 <> nil then
        if Plugins[IndexPlugin2] <> nil then
          if (Plugins[IndexPlugin2].TaskIndexIcon > 0) then
            AddDataTask.TaskServPlugIndexIcon := Plugins[IndexPlugin2]
              .TaskIndexIcon
          else
          begin
            if pos('magnet:?', AnsiLowerCase(AddDataTask.LinkToFile)) = 1 then
              AddDataTask.TaskServPlugIndexIcon := 34; 
          end;

      TasksList.Add(AddDataTask);
      Result := true;

      PostMessage(Options.MainFormHandle, WM_MYMSG, 0, 12345);

      if Now then
        LoadTorrentThreads.Add(TLoadTorrent.Create(false, AddDataTask, true)); // создание потока выполняющего запуск торрента
    end;

  finally
    AddedData.Free;
  end;
end;


В процедуре добавления торрента происходит проверка на наличие info hash в списке торрентов. Если info hash найден, то вместо добавления торрента в список, будет предложено добавить адреса трекеров из торрента BTPluginAddTrackers.AddTrackers(HashValue, trackers), иначе добавление торрента в список будет продолжено. После добавления торрента в список TasksList.Add(AddDataTask), будет создан поток TLoadTorrent (модуль uTorrentThreads), который выполнит запуск торрента BTPlugin.StartTorrent(DataTorrent) и в котором также запустится цикл repeat, проверяющий состояние и получающий информацию о торренте каждую секунду GetedData := BTPlugin.GetInfoTorrent(DataTask.HashValue).

За отображение полученной информации отвечает событие TListView OnData:

Код отображения полученной информации
procedure TfMainForm.lvTasksData(Sender: TObject; Item: TListItem);
var
  i: Integer;
  Task: TTask;
  Procent, Procent2: string;
  EndPoint: Integer;
begin
  with TasksList.LockList do
    try
      for i := 0 to Count - 1 do
      begin
        if i = Item.Index then
        begin
          Task := Items[Item.Index];
          // Иконка состояния
          case Task.Status of
            tsReady: Item.ImageIndex := 0; // Ожидание закачки 
            tsQueue: Item.ImageIndex := 1; // В очереди
            tsError: Item.ImageIndex := 2; // Ошибка завершена
            tsErroring: Item.ImageIndex := 2; // Ошибка не завершена
            tsLoading: Item.ImageIndex := 3;  // Закачка
            tsStoping: Item.ImageIndex := 4; // Остановка
            tsStoped: Item.ImageIndex := 5; // Пауза
            tsLoad: Item.ImageIndex := 6; // Закачано
            tsProcessing: Item.ImageIndex := 8; // Поиск
            tsSeeding: Item.ImageIndex := 9; // Раздача
            tsBittorrentMagnetDiscovery: Item.ImageIndex := 10; // Magnet-поиск
            tsDelete: Item.ImageIndex := 11; // Удаляется
            tsDeleted: Item.ImageIndex := 11; // Удален
          end;
          Item.SubItems.Add(Task.FileName); // Имя Файла
          Item.SubItems.Add(Task.LinkToFile); // Ссылка
          Item.SubItemImages[1] := 12;
          // Состояние
          case Task.Status of
            tsReady: Item.SubItems.Add('Ожидание');
            tsQueue: Item.SubItems.Add('В очереди');
            tsError: Item.SubItems.Add('Ошибка');
            tsErroring: Item.SubItems.Add('Ошибка');
            tsLoading:
              begin
                if Task.TotalSize > 0 then
                begin
                  Procent := FloatToStr((Task.LoadSize / Task.TotalSize) * 100);
                  begin
                    EndPoint := pos(',', Procent);
                    if EndPoint <> 0 then
                    begin
                      Procent2 := copy(Procent, 1, EndPoint + 1);
                      Item.SubItems.Add(Procent2 + '% ' + 'Закачка');
                    end
                    else
                    begin
                      try
                        Procent2 := FloatToStrF(StrToInt(Procent),
                          ffFixed, 6, 1);
                      except
                      end;
                      Item.SubItems.Add(Procent2 + '% ' + 'Закачка');
                    end;
                  end;
                end
                else
                  Item.SubItems.Add('');
              end;
            tsStoping: Item.SubItems.Add('Останавливается');
            tsStoped: if (Task.TotalSize > 0) then
                Item.SubItems.Add(FloatToStrF((Task.LoadSize / Task.TotalSize) *
                  100, ffFixed, 6, 1) + '% ' + 'Пауза')
              else
                Item.SubItems.Add('0% ' + 'Пауза');
            tsLoad: Item.SubItems.Add('Завершено');
            tsProcessing: Item.SubItems.Add('Поиск');
            tsSeeding: Item.SubItems.Add('Раздача');
            tsBittorrentMagnetDiscovery: Item.SubItems.Add('Magnet-поиск');
            tsDelete: Item.SubItems.Add('Удалено');
            tsDeleted: Item.SubItems.Add('Удалено');
            tsFileError: Item.SubItems.Add('Ошибка торрента');
            tsAllocating: Item.SubItems.Add('Распределение');
            tsFinishedAllocating: Item.SubItems.Add('Распределение завершено');
            tsRebuilding: Item.SubItems.Add('Восстановление');
            tsJustCompleted: Item.SubItems.Add('Завершается');
            tsCancelled: Item.SubItems.Add('Отменено');
            tsUploading: Item.SubItems.Add('Раздача');
            tsStartProcess: Item.SubItems.Add('Запуск');
          end;
          if Task.Speed > 0 then
          begin
            Item.SubItems.Add(GetTimeStr((Task.TotalSize - Task.LoadSize)
              div Task.Speed)); // Осталось
          end
          else
            Item.SubItems.Add('');
          if Task.TotalSize > 0 then
            Item.SubItems.Add(BytesToText(Task.TotalSize)) // Размер
          else
            Item.SubItems.Add('');
          Item.SubItems.Add(BytesToText(Task.LoadSize)); // Закачано
          if Task.Speed > 0 then
            Item.SubItems.Add(BytesToText(Task.Speed) + '/s') // Скорость
          else
            Item.SubItems.Add('');
          if Task.NumConnectedSeeders > 0 then
            Item.SubItems.Add(IntToStr(Task.NumConnectedSeeders)) // Сиды
          else
            Item.SubItems.Add('');
          if Task.NumConnectedLeechers > 0 then
            Item.SubItems.Add(IntToStr(Task.NumConnectedLeechers)) // Пиры
          else
            Item.SubItems.Add('');
          if Task.UploadSpeed > 0 then
            Item.SubItems.Add(BytesToText(Task.UploadSpeed) + '/s') // Скорость отдачи
          else
            Item.SubItems.Add('');
          if Task.UploadSize > 0 then
            Item.SubItems.Add(BytesToText(Task.UploadSize)) // Отдано
          else
            Item.SubItems.Add('');
          break;
        end;
      end;
    finally
      TasksList.UnLockList;
    end;
end;


Вот мы и подошли к моменту тестирования. Хотя код клиента и является завершённым, я решил всё же дополнить его прогрессбаром, который разместил в статусбаре нашего клиента, вместо отображения magnet-ссылки, которая и так отображается в списке торрентов. Это нам нужно для того, чтобы видеть как происходит закачка, последовательно или нет.
После компиляции запускаем наш клиент, добавляем торрент в список нажав на кнопку «Добавить торрент». Один торрент добавим без установки метки «Последовательная закачка», а на другом установим эту метку и дождёмся начала закачки. В результате во время закачки мы должны увидеть следующую картину:





Т.е при выделении в списке торрента, в котором мы установили метку «Последовательная закачка», закачка происходит последовательно, а у другого торрента закачка частей торрента происходит выборочно, без последовательности.

В итоге мы получили действующий торрент-клиент, полностью выполненный на языке Delphi и неуступающий функциональности современных клиентов. Исходники битторрент-библиотеки BTService и исходники клиента DelphiTorrent (каталог examples) доступны по SVN: svn.code.sf.net/p/btservice/svn

Мы создали торрент-клиент, которым пользоваться возможно только в ОС Windows. Потому следует ожидать продолжение, в котором я расскажу о создании клиента для ОС Android и IOS, т.к все предпосылки для этого имеются.

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


  1. MetaDone
    15.06.2015 16:56
    +21

    if Task.Status = tsReady then // Ожидание закачки
                Item.ImageIndex := 0;         
              if Task.Status = tsQueue then // В очереди
                Item.ImageIndex := 1;         
              if Task.Status = tsError then // Ошибка завершена
                Item.ImageIndex := 2;         
              if Task.Status = tsErroring then // Ошибка не завершена
                Item.ImageIndex := 2;        
              if Task.Status = tsLoading then  // Закачка
                Item.ImageIndex := 3;         
              if Task.Status = tsStoping then // Остановка
                Item.ImageIndex := 4;         
              if Task.Status = tsStoped then // Пауза
                Item.ImageIndex := 5;         
              if Task.Status = tsLoad then // Закачено
                Item.ImageIndex := 6;         
              if Task.Status = tsGetUrl then // Получение ссылки
                Item.ImageIndex := 7;         
              if Task.Status = tsProcessing then // Поиск
                Item.ImageIndex := 8;         
              if Task.Status = tsSeeding then // Раздача
                Item.ImageIndex := 9;         
              if Task.Status = tsBittorrentMagnetDiscovery then // Обработка магнет
                Item.ImageIndex := 10;         
              if (Task.Status = tsDelete) or (Task.Status = tsDeleted) then // Удален
                Item.ImageIndex := 11;
    
    

    я не знаком с делфи, но там конструкции switch разве нет?


    1. valexey
      15.06.2015 17:04
      +3

      Тем более что там явно же enum нужно отобразить в число, а еще точнее, в ID картинки. Поскольку мы полностю контроллируем и именование/нумерацию наших картинок, и enum, думаю легко можно было бы весь этот код превратить в одну строчку преобразования из одного в другое.


    1. avtorfile Автор
      15.06.2015 17:09
      -24

      Вместо switch имеется Case. Суть статьи не в написании идеального кода на Delphi, а показать возможности Delphi в создании программ на подобии торрент-клиента.


      1. Seekeer
        15.06.2015 17:13
        +10

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


        1. avtorfile Автор
          15.06.2015 17:18
          -22

          Может всё таки по теме обсуждение вести?


      1. valexey
        15.06.2015 17:19
        +3

        Ну, если уж даже на хаскеле это возможно ( github.com/jlouis/combinatorrent ), то это возможно и на делфи и на js :-)


        1. avtorfile Автор
          15.06.2015 17:26
          -6

          Ранее были сомнения на этот счёт. Ещё одно подтверждение возможности не будет лишним )


          1. valexey
            15.06.2015 17:27

            У кого?


            1. avtorfile Автор
              15.06.2015 17:48

              Эта статья является продолжением серии статей, написанной Игорем Антоновым ещё в 2007-2008 г. Он сам попросил написать новую статью. Если почитаете эту серию, то поймёте, что после прочтения может сложиться впечатление, что на delphi писать клиент сложнее чем на других яп. Многие дельфисты, да и не только дельфисты до сих пор наталкиваются на старые статьи и как вы думаете, у них не будет возникать сомнений по поводу Delphi?


              1. kahi4
                15.06.2015 18:07

                У вашей логики две проблемы: первая — сложнее != невозможно. Даже «сложнее» вызывает сомнения (не уж то делфи настолько не приспособлен к работе с сетью и файловой системой?).
                Второе — вы ничего данным примером не показали. Кто знает, как внутри эта библиотека устроена кроме вас? Может там часть написана на другом языке, а на делфи только обертка?
                upd. Что-то сначала не увидел исходников, только dll. Вторая проблема снимается.

                Какое-то ненаучное доказательство несуществующей проблемы.


                1. avtorfile Автор
                  15.06.2015 18:13

                  У вашей логики две проблемы: первая — сложнее != невозможно. Даже «сложнее» вызывает сомнения (не уж то делфи настолько не приспособлен к работе с сетью и файловой системой?).
                  Второе — вы ничего данным примером не показали. Кто знает, как внутри эта библиотека устроена кроме вас? Может там часть написана на другом языке, а на делфи только обертка?

                  Какое-то ненаучное доказательство несуществующей проблемы.


                  Благодарю за критику. Согласен, что не всё гладко, но не согласен, что нет доказательства. Исходный код, как и бинарники приложены.


              1. valexey
                15.06.2015 18:39
                +7

                Ок. Я нашел эти статьи (статьи выходили в журнале Хакер — уже дурной признак):
                iantonov.me/page/programmiruem-torrent-klient-na-delphi
                iantonov.me/page/programmiruem-torrent-klient-na-sdelphis-c

                Процитирую отрывок из первой

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

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

                Из этого сделать вывод, что «на делфи невозможно написать торрент-клиент» может сделать только «программист», руководствующийся принципом «это невозможно сделать, если для этого не написано готового компонента/библиотеки».

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

                PS. И да, обзор архитектуры и особенностей библиотеки для торрента, писанной на Delphi был бы намного интересней, чем статья про лепления гуйни к готовой уже библиотеки. Уж что лепка гуйни на делфи возможна, думаю, знают даже делфисты.


                1. avtorfile Автор
                  15.06.2015 19:01
                  -1

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

                  Начинающие программисты (не только дельфисты) вполне могут.

                  PS. И да, обзор архитектуры и особенностей библиотеки для торрента, писанной на Delphi был бы намного интересней, чем статья про лепления гуйни к готовой уже библиотеки. Уж что лепка гуйни на делфи возможна, думаю, знают даже делфисты.

                  В следующей статье вопрос архитектуры постараюсь раскрыть.


                  1. valexey
                    15.06.2015 19:10

                    Начинающие программисты будут скорее читать хабр, или rsdn сегодняшний, нежели статьи в Хакере которые выходили 7 лет назад.


                    1. avtorfile Автор
                      15.06.2015 19:13

                      Статьи не только в хакере, а также на VR и на сайте Игоря.


                  1. valexey
                    15.06.2015 21:33
                    +8

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

                    Таким же кодом как в статье (ComboBox1, вот этот вот унылый if и прчее) вы прививаете начинающему делфипрограммеру дурные манеры. Вы ему как бы говорите — смотри, парень, так прокатывает даже в статье! Так что к тебе то спросу и вовсе не будет! Так писать — норма!

                    В итоге люди которые не знакомы с делфи (либо уже предвзято к делфи относятся) смотрят на статью, и делают вывод — что даже в статье по делфи быдлокод, значит средний делфикодер накодит вообще страх и ужас => делфикодеры недопрограммисты.

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

                    Таким образом, статья в нынешнем виде является антирекламой делфи.


        1. avtorfile Автор
          15.06.2015 17:30
          +1

          Кроме того, у библиотеки BTService имеются возможности написания торрент-клиента и на других ЯП. Если кому интересно, то читайте про систему плагинов Александра Алексеева, (ссылка на блок Александра имеется в статье).


      1. kahi4
        15.06.2015 17:28
        +13

        Эм. Нет, суть статьи «смотрите, я нашел библиотеку для делфи, с помощью которой можно скачивать торренты, зацените».

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

        Вообще по стилю кода — все ужасно. Просто ужасно. Последний раз я описывал бизнес-логику в хандлере кнопки на первом курсе, наверное.


        1. avtorfile Автор
          15.06.2015 17:32
          -10

          Библиотека писалась мною. Это не реклама, бизнесменам — проходите мимо… или самостоятельно пилите…


          1. kahi4
            15.06.2015 17:42
            +6

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

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

            Кроме того, у библиотеки BTService имеются возможности написания торрент-клиента и на других ЯП.

            Есть такое ощущение, что это свойство dll, не зависить от языка, на котором его подключают.

            Начав разбираться в исходниках Ares Galaxy, выяснил, что многие операции выполняются в одном потоке, что в результате кратковременно останавливает процесс закачки всех торрентов в списке. Потому я решил исправить недостатки и вынес процедуры, которые замедляют выполнение кода, в отдельные потоки.

            Вы уж определитесь, писалась она вами или же только правилась? Я ни в коем случае не сомневаюсь в ваших способностях, может в делфи заведено не пользоваться switch-case, но как-то сложно поверить в высокое качество библиотеки после увиденного в ваших примерах, уж простите.


            1. avtorfile Автор
              15.06.2015 18:24

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

              Учту при написании следующей статьи.
              Есть такое ощущение, что это свойство dll, не зависить от языка, на котором его подключают.

              Не так. Если использовать не тот тип данных при обмене м/у ядром и библиотекой, то получится моноязычная библиотека.
              Вы уж определитесь, писалась она вами или же только правилась? Я ни в коем случае не сомневаюсь в ваших способностях, может в делфи заведено не пользоваться switch-case, но как-то сложно поверить в высокое качество библиотеки после увиденного в ваших примерах, уж простите.

              Мною писался плагин на основе исходников Ares Galaxy, который пришлось корректировать в силу его недостатков. А точнее зависание основного потока, который останавливает работу всех торрентов списка.


      1. vlx
        15.06.2015 18:37
        +4

        Целые игры Age of wonders были написаны на Делфи, с чего вдруг торрент клиент стал непосильной задачей?


      1. AtomKrieg
        16.06.2015 04:21
        -1

        А вам нравится Оберон?


    1. avtorfile Автор
      16.06.2015 08:38
      -1

      Считаю критику обоснованной. Код подправил


  1. Infanty
    15.06.2015 17:21
    +3

    Спасибо за вашу библиотеку.


    1. avtorfile Автор
      15.06.2015 17:37
      -2

      Не за что.


    1. valexey
      15.06.2015 19:08
      +2

      Только нужно учесть, что эта либа распространяется под GPL v2 или выше так как оригинальный код Ares Galaxy был под GPL 2. Следовательно и, например, клиент описанный в данной статье, также будет под GPL, равно как и любая другая программа которая будет использовать эту либу.

      PS. Кстати, автору неплохо бы указать лицензию на sf.net явным образом.


      1. avtorfile Автор
        15.06.2015 19:24
        +1

        PS. Кстати, автору неплохо бы указать лицензию на sf.net явным образом.

        Спасибо, что заметили. Указал.


  1. Ivan_83
    15.06.2015 17:53
    +5

    Хорошая проба пера.

    Но:
    1. Что дельфи умеет сеть было известно ещё году в 2003, когда спамеры заюзали инди либу для своих спам ботов. Кажется кип на дельфях был писан.

    2. сорсфорс — помойка, уже столько писали про это в последнее время. А отдельные личности ещё лет 5 назад сказали что это гандюшник.

    3. Отталкиватся стоило не от «а вот здесь у нас узкое место, добавим ещё поток» а «сейчас запилим правильный скелет а потом обвесим мясом из этого донора».
    Для примера, что будет с вашим клиентом если закинуть в него 5000 торрент файлов?

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


    1. avtorfile Автор
      15.06.2015 18:02
      -2

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

      Можно конечно сказать о существовании торрент-клиентов, которые имеют более отлаженный проверенный годами код и зачем изобретать велосипед? Но история показывает, что и проверенные клиенты невсегда чисты на руку и добаляют в свой код различные майнеры, которые могут замедлять работу процессора без особой на то надобности. Потому, на сколько же хорошо иметь приложения, которые можно самостоятельно компилировать, определять и исправлять недостатки, чем приложения с закрытым кодом, который в какой-то момент может оказаться зловредным. Потому такая библиотека не будет лишней…


    1. valexey
      15.06.2015 18:02
      +1

      Что дельфи умеет сеть было известно ещё году в 2003, когда спамеры заюзали инди либу для своих спам ботов. Кажется кип на дельфях был писан.

      Это вообще было известно сразу после появления делфи — там же есть полный (или почти полный) биндинг к WinAPI. В том числе и к сети. А если вдруг биндинга нужного нет, то всегда можно сделать — биндинги к сишному добру там делаются элементарно (как и в куче других ЯП).

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


  1. gene4000
    15.06.2015 18:23
    +4

    Вот лучше бы библиотеку BTService описали: Как протокол PEX работает, как DHT запустить.


  1. js605451
    15.06.2015 18:54
    +10

    Ну почему в каждой статье про Delphi я вижу что-то типа «ComboBox1»? (вопрос риторический)


  1. Imp5
    15.06.2015 21:18
    +6

                  try
                    try
                      BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла
                    except
                    end;
                  finally
                    LeaveCriticalSection(TorrentSection);
                  end;
    

    Это что за конструкция?
    Автор понимает, как работает try / finally / except?


    1. vlx
      15.06.2015 21:37
      +9

      Автор не понимает даже банальных конструкций языка, отмахиваясь благой целью показать «возможности делфи». Вас десятиэтажные ифы, в которых даже else нет никак не заставили задуматься, а только обработка исключений? :) Хрен с ним, с case'ом, но где else хотя бы?


    1. xSomeonEx
      16.06.2015 17:00

      А тут все просто. Подразумевается, что если AV вывалится в BTCreateTorrent.StopCreateTorrentThread(Id), то дальнейший участок кода(в данном случае секция finally..end и все, что после finally..end) пройдет без проблем. Если ж выдать просто:

      try
        BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла
      finally
        LeaveCriticalSection(TorrentSection);
      end;
      

      то при AV в BTCreateTorrent.StopCreateTorrentThread(Id), оно выполнит секцию finally..end и вывалится из метода, ЕМНИП


      1. xSomeonEx
        16.06.2015 17:15

        Хотя с другой стороны, можно было нашлепать просто:

        try
          BTCreateTorrent.StopCreateTorrentThread(Id); // остановка создания торрент-файла
        except
        end;
        
        LeaveCriticalSection(TorrentSection);
        


        Но exception как-то обрабатывать-то нужно по-человечески.


  1. AveryanovSergey
    16.06.2015 00:27
    +10

    Код невыразимо прекрасен: и полное отсутствие ООП с развешиванием кучи кода на обработчиках событий, и семантика имен со всякими ComboBox1, и чуть ли не десяток уровней вложенности if. С 2003 года, когда я писал на Delphi последний раз, ничего не поменялось: большинство кода на нем — это формошлепство вчерашних школьников.

    Статья хорошо подходит, чтобы отбить любое желание писать на, в общем-то, неплохом инструменте.


    1. vlx
      16.06.2015 01:57

      Это «корован» последовательных ифов, а не вложенных


      1. toxicdream
        16.06.2015 08:13

        От этого не легче. Обидно все же за язык.


      1. fshp
        16.06.2015 12:50

        Это ещё хуже, т.к. все условия проверятся.


        1. vlx
          16.06.2015 12:59

          а мужики-то и не знали =)


      1. svd71
        16.06.2015 15:50

        Вы ошиблись по Фрейду или по грамматике?


        1. vlx
          16.06.2015 15:52

          Я не ошибался.


          1. svd71
            16.06.2015 23:49
            -1

            Последовательность одинаковых вещей обычно назавается «кАрАван».

            А вот кОрОван наверное как то связан с коровами. Но я не знаю как.


            1. vlx
              17.06.2015 00:05

              lurkmore.to/%D0%9A%D0%BE%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D1%8B


              1. svd71
                17.06.2015 09:25
                +1

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


                1. withkittens
                  17.06.2015 18:42
                  +1

                  Так ведь слово даже в кавычки взяли.
                  Это должно было навести на мысль, что «корован»-то не обычный ;)


  1. stepanp
    16.06.2015 13:04
    -5

    Delphi…
    Какой сейчас год?


  1. dyadyaSerezha
    16.06.2015 14:38

    Замечательно, просто замечательно. Но! Еще остались отдельные несознательные граждане, которые не верят, что на Дельфи можно написать: браузер с поддержкой последних стандартов, поисковик, АвтоКАД, Фотошоп, Скайп и т.д. и т.п. Надеемся, что вы не остановитесь на достигнутом, продолжите свое доказательство, и поэтому с нетерпением ждем следующих статей и реализаций. ;)


    1. Tujh
      16.06.2015 16:49
      +1

      Вроде как в Скайпе UI как раз написан на Делфи.


      1. Akvel
        17.06.2015 11:47

        Слабо верится, что Микрософт использует делфи


        1. avtorfile Автор
          17.06.2015 11:58

          Skype для платформы Windows написан на Delphi, в википедии это указано: ru.wikipedia.org/wiki/Skype


        1. Tujh
          17.06.2015 12:10

          Сколько времени развивается проект Skype, и сколько времени он принадлежит Microsoft-у? Думаете они сразу возьмут и всё перепишут, ну скажем, на C#? :)


        1. vlx
          17.06.2015 14:41

          уже не использует давно


          1. vlx
            17.06.2015 15:09
            +1

            где-то я такое вычитал. Хотя по SpyXX названия классов по-прежнему делфинские — TChatBanner, TDivider и т д.