📜 ⬆️ ⬇️

We program the BitTorrent client. Pure Delphi

It has been 8 years since Igor Antonov (Spider_NET) has written an article about creating a torrent client in C # , but the simplest example of how to do this in Delphi has not yet appeared on the network.

In order to dispel doubts about the ineffectiveness of the Delphi language in such a "difficult" matter as writing a full-fledged bittorrent client, I decided to write this article.

At once I will say that our Delphi torrent client will be open source and will support almost all modern bittorrent technologies, including DHT, magnet links, sequential downloads, etc.

An online search for Delphi client source codes yielded results, but these results were far from ideal. The first result was the long-abandoned Torrent Torque (2007), with the alpha version. TorrentTorque failed to compile and test normally.
The next search result turned out to be Ares Galaxy , little-known in Runet, which turned out to be quite workable and even popular in some countries a torrent client. Having suffered from compilation, I still managed to try out the desired code, but it turned out to be flawed, which, as it turned out, the developers hadn’t been fixed for a long time. In addition, Ares Galaxy is written in Delphi 7, which means that to compile in newer versions of RAD Studio you need to rewrite a huge amount of code. But this did not stop me and I found another way out to solve this problem.
')
Having started to understand the source code of Ares Galaxy, I found out that many operations are performed in one thread, which, as a result, briefly stops the download process of all torrents in the list. Therefore, I decided to correct the flaws and put procedures that slow down the execution of the code into separate threads.

Having received a positive result, I decided to place the source code on sourceforge.net. The code was executed in the form of the BTService dll-library, with the use of a plug-in system, which Alexander Alekseev told in detail in his series “Development of a plug-in system” . So, using such a plugin system, it is possible to create a bittorrent client in any RAD Studio compiler and not only in Delphi, but also in other programming languages. The BTService library and its sources are available at the link: http://btservice.sourceforge.net/

So, let's start writing a simple client based on the BTService library.

We will execute the client interface in the classical style, with a minimal set of components, but so that the main functionality of the library is visible.



Five buttons on the toolbar: add a magnet link, add a torrent, create a torrent, start a torrent, and stop a torrent.

The list of torrents will be displayed in a standard TListView. The lists of files, connected peers and trackers will also be placed on the TListView, which will be displayed accordingly when you open the TPageControl tabs. Well, at the bottom of the main form StatusBar, which will display a magnet link selected in the torrent list, four torrent states and total download and upload speeds for all torrents in the list.

Now let's sort through the events of creating, starting and stopping torrents.

We will not describe all the details related to the creation of the torrent and the specification of the bittorrent protocol. Igor did this before us in his article “Code BitTorrent Client. Part One . All the code that creates the torrent is available in the BTService library. Who is interested in its implementation, see the source of the library. Well, I just indicate the code that interacts with the library.

To begin, create a form "Create a new torrent." On which we place three buttons: “Add file”, “Add folder” and “Create”. Add TPageControl. Place the main parameters on the first tab. The parameter “Part Size” will be executed in TCombobox, “Start download” and “Private torrent” will be executed in TCheckBox. On the other tabs of the TPageControl we will place the TMemo fields, which add the addresses of the trackers, web sites and comments to the torrent file. The process of creating a torrent will be displayed on two TProgressBar. The first will show the execution of a hash of a single file, and the other will show the overall process of hashing all torrent files.



For the button “Add file” the code will be as follows:

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; 

For the “Add folder” button, the code will be as follows:

 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; 

That is, in the “Source selection” field (ComboBox1.Text), the path of the file or folder is added, depending on whether we want to add to the torrent, one file or several files in the folder.

Next, on the button “Create” we write the code:

Code to create a torrent file
 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; 


As can be seen from the code, the first thing is the selection of the directory to save the torrent. And then there is a search for the IBTCreateTorrent interface, which is responsible for calling the SingleFileTorrent procedure from the BTService plugin. This procedure starts the process of creating a torrent file with the contents of a single file, and the CreateFolderTorrent procedure is launched for the file folder. After that, a repeat loop is launched, in which a periodic call to the GetInfoTorrentCreating function occurs, which returns the result of actions from the plug-in in the process of creating a torrent and information about the percentage of hashing performed. If the result is returned GetedStatus = 'completed', then the process of creating a torrent completed successfully and you can exit the cycle.

To add a torrent to the list, create a form “Add torrent”. Let's place two buttons on it: “Download” and “Add to list”. The first will add the torrent to the list and immediately begin the download process, and the second will simply add the torrent to the list to wait for subsequent actions on it. To display information about the torrent, add TEdit ("Torrent file:"), TComboBox ("Save to:"), TLabel ("Torrent name:", "Description:", "Date:") and the TListView list, which will be show the contents of torrent files and folders.



Code add and download torrent
 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; 


The procedure for adding a torrent is checked for the presence of info hash in the list of torrents. If info hash is found, then instead of adding a torrent to the list, you will be prompted to add tracker addresses from the torrent BTPluginAddTrackers.AddTrackers (HashValue, trackers), otherwise the addition of the torrent to the list will continue. After adding a torrent to the TasksList.Add (AddDataTask) list, a TLoadTorrent stream will be created (uTorrentThreads module), which will launch the BTPlugin.StartTorrent (DataTorrent) torrent and which will also run a repeat loop that checks the status and receives torrent information every second GetedData: = BTPlugin.GetInfoTorrent (DataTask.HashValue).

The TListView OnData event is responsible for displaying the information received:

Display code received information
 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; 


Here we come to the time of testing. Although the client code is complete, I decided to supplement it with a progress bar, which I placed in our client’s status bar, instead of displaying a magnet link, which is already displayed in the list of torrents. We need this in order to see how the download takes place, sequentially or not.
After compilation, launch our client, add the torrent to the list by clicking on the "Add torrent" button. We will add one torrent without setting the “Sequential download” label, and on the other we will install this label and wait for the download to start. As a result, during the download we should see the following picture:





In other words, when a torrent is selected in the list, in which we have set the “Sequential upload” label, the download occurs sequentially, while for the other torrent, the injection of parts of the torrent occurs selectively, without a sequence.

As a result, we received a valid torrent client, fully implemented in the Delphi language and non-inferior to the functionality of modern clients. BTService bittorrent library sources and DelphiTorrent client sources (examples directory) are available via SVN: svn.code.sf.net/p/btservice/svn

We have created a torrent client that can only be used in Windows. Therefore, we should expect a continuation in which I will talk about creating a client for the Android OS and IOS, as there are all prerequisites for this.

Source: https://habr.com/ru/post/260339/


All Articles