AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Thema durchsuchen
Ansicht
Themen-Optionen

Einbinden eines Thread Downloads

Ein Thema von Stewag · begonnen am 16. Jul 2024 · letzter Beitrag vom 22. Jul 2024
Antwort Antwort
Stewag

Registriert seit: 12. Jun 2008
180 Beiträge
 
Delphi 12 Athens
 
#1

Einbinden eines Thread Downloads

  Alt 16. Jul 2024, 22:51
In einem mobilen Projekt für Android und iOS verwende ich bisher zum Download von Dateien in einen Stream System.Net.HttpClient und verarbeite dann das Ergebnis:

Code:
 mstream := TMemoryStream.Create;
    try
      DownloadFile('https://www.example.de/datei.txt', mstream);
      memWerte.LoadFromStream(mstream, TEncoding.ANSI);
    except
      on E: Exception do
        showmessage(E.Message)
    end;
mstream.Free;
Da Android die damit verbundene Wartezeit für den User nicht schätzt, habe ich eine Unit erstellt, die die Datei in einem synchronen Thread lädt. Das ist wohl das empfohlene Verfahren.
Die Unit funktioniert im Testprojekt auch und synchronisiert den Ergebnisstream in den Hauptthread über DownloadComplete zurück:

Code:
procedure TForm1.DownloadComplete(const stream: TStream; Success: Boolean);
begin
  if Success then
  begin
     memWerte.lines.LoadFromStream(stream)
  end
  else
    Label1.Text := 'Download failed';
end;
Mein Problem nun:
Wie bekomme ich das Ergebnis, das DownloadComplete liefert, am besten unter einen Hut mit dem Hauptstream?
Muss ich dazu einen Timer einrichten, der zyklisch prüft, ob memWerte.lines schon <> '' ist oder gibt es einen eleganteren Weg?

Hier der Aufruf des Thread Downloads:
Code:
procedure TForm1.Button1Click(Sender: TObject);
var
  Downloader: TFileDownloader;
  mstream: TMemoryStream;
begin
   try
     mstream := TMemoryStream.Create;
     Downloader := TFileDownloader.Create('https://www.example/datei.txt', mstream);
     Downloader.OnDownloadComplete := DownloadComplete;
     Downloader.Start;
   finally
      mstream.Free;
   end
end;
Steffen

Geändert von Stewag (16. Jul 2024 um 23:27 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von himitsu
himitsu
Online

Registriert seit: 11. Okt 2003
Ort: Elbflorenz
44.326 Beiträge
 
Delphi 12 Athens
 
#2

AW: Einbinden eines Thread Downloads

  Alt 16. Jul 2024, 23:35
Ich würde mal sagen, dass es eine saublöde Idee ist, mstream freizugeben, bevor der Thread fertig ist, sogar als er noch nichtmal gestartet wurde.


Ich würde den Stream im Execute des Threads erstellen und dort auch freigeben, nach dem OnDownloadComplete (egal ob mit oder ohne Synchronize)
Ein Therapeut entspricht 1024 Gigapeut.

Geändert von himitsu (16. Jul 2024 um 23:48 Uhr)
  Mit Zitat antworten Zitat
Stewag

Registriert seit: 12. Jun 2008
180 Beiträge
 
Delphi 12 Athens
 
#3

AW: Einbinden eines Thread Downloads

  Alt 17. Jul 2024, 10:32
Himitsu hat natürlich Recht!
Das .Free habe ich erst spontan, nachträglich, im Forum nachgesetzt, weil mir der Befehl zu fehlen schien und ich befürchtete, als Erstes eins deswegen auf den Deckel zu bekommen.

Nun ist es umgekehrt

Hier der korrekte Aufruf:

Code:
procedure TForm1.Button1Click(Sender: TObject);
var
  Downloader: TFileDownloader;
  mstream: TMemoryStream;
begin
  mstream := TMemoryStream.Create;
  Downloader := TFileDownloader.Create('https://www.example/datei.txt', mstream);
  Downloader.OnDownloadComplete := DownloadComplete;
  Downloader.Start;
end;
Meine ursprüngliche Frage ist damit aber nicht beantwortet.

Hier die (lauffähige) Unit:
Code:
unit SynchronousHttpClientStreamDownloader;

interface

uses
  System.Classes, System.SysUtils, System.Net.HttpClient, System.Net.URLClient;

type
  TDownloadCompleteEvent = procedure(const stream: TStream; Success: Boolean) of object;

  TFileDownloader = class(TThread)
  private
    FURL: string;
    FStream: TStream;
    FHttpClient: THTTPClient;
    FOnDownloadComplete: TDownloadCompleteEvent;
    FSuccess: Boolean;
    procedure DownloadFile;
    procedure DoDownloadComplete;
  protected
    procedure Execute; override;
  public
    constructor Create(const URL: String; aStream: TStream);
    destructor Destroy; override;
    property OnDownloadComplete: TDownloadCompleteEvent read FOnDownloadComplete write FOnDownloadComplete;
  end;

implementation

{ TFileDownloader }

constructor TFileDownloader.Create(const URL: String; aStream: TStream);
begin
  inherited Create(True);
  FreeOnTerminate := True;
  FURL := URL;
  FStream := aStream;
  FHttpClient := THTTPClient.Create;
end;

destructor TFileDownloader.Destroy;
begin
  FHttpClient.Free;
  inherited;
end;

procedure TFileDownloader.DownloadFile;
var
  vHTTP: THTTPClient;
begin
  try
    FStream := TMemorystream.Create; // FStream muss an dieser Stelle erzeugt werden, im Execute klappt es nicht. Free muss aber im Execute stehen
    Assert(FStream <> nil);

    vHTTP := THTTPClient.Create;
    vHTTP.CustomHeaders['Pragma'] := 'no-cache';

    try
      vHTTP.Get(FURL, FStream);
      FSuccess := True;
    finally
      vHTTP.Free;
    end;
  except
    on E: Exception do
    begin
      FSuccess := False;
    end;
  end;
end;

procedure TFileDownloader.DoDownloadComplete;
begin
  if Assigned(FOnDownloadComplete) then
  begin
    FOnDownloadComplete(FStream, FSuccess);
    FStream.Free
  end
end;

procedure TFileDownloader.Execute;
begin
  DownloadFile;
  Synchronize(DoDownloadComplete);
end;

end.
Gibt es dazu Anmerkungen?
Steffen

Geändert von Stewag (17. Jul 2024 um 10:35 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von DeddyH
DeddyH

Registriert seit: 17. Sep 2006
Ort: Barchfeld
27.656 Beiträge
 
Delphi 12 Athens
 
#4

AW: Einbinden eines Thread Downloads

  Alt 17. Jul 2024, 10:39
Du gibst den Stream nur frei, wenn dem Ereignis ein Handler zugewiesen wurde?
Detlef
"Ich habe Angst vor dem Tag, an dem die Technologie unsere menschlichen Interaktionen übertrumpft. Die Welt wird eine Generation von Idioten bekommen." (Albert Einstein)
Dieser Tag ist längst gekommen
  Mit Zitat antworten Zitat
Benutzerbild von himitsu
himitsu
Online

Registriert seit: 11. Okt 2003
Ort: Elbflorenz
44.326 Beiträge
 
Delphi 12 Athens
 
#5

AW: Einbinden eines Thread Downloads

  Alt 17. Jul 2024, 11:16
Warum den THTTPClient doppelt erstellen?
Im Excecute ist es OK, das im Create war eh ungenutzt.

Assert(FStream <> nil);
Kann niemals NIL sein, denn wenn "doch", dann wirft das Create bereits eine Exception und es kommt dort nie vorbei.

FOnDownloadComplete erst beim Ausführen zu prüfen ist auch etwas unpraktisch.
Alles war ja sinnlos, wenn diese Methode fehlt, da der Download nur als Stream und von diesem Event verwendet wird.
Ohne das, wurde es vollkommen nutzlos runtergeladen.
Also hier am Besten gleich zu Beginn prüfen und z.B. eine Exception werfen, bereits im Create.

Delphi-Quellcode:
constructor TFileDownloader.Create(const URL: String; OnDownloadComplete: TDownloadCompleteEvent);
begin
  if not Assigned(OnDownloadComplete) then
    raise Exception.Create('peng');
  inherited Create(False); // und jetzt außen kein Start mehr, da es am Ende des Create von selst startet
  FreeOnTerminate := True;
  FURL := URL;
  FOnDownloadComplete := OnDownloadComplete;
  // und der Rest im Execute/DownloadFile
end;
Für die Funktion zwar nicht relevant, aber
TDownloadCompleteEvent = reference to procedure(Stream: TStream; Success: Boolean);
hat den Vorteil, dass man hier nicht nur Methoden und KlassenMethoden übergegen kann,
sondern auch einfache Prozeduren oder anonyme Methoden.

Den Stream intern erstellen, da wo er auch freigegeben wird, nicht extern.
Nja, da FHttpClient, FSuccess und eigentlich auch FStream im Grunde ausschließlich während der Laufzeit des Threads nötig sind und bezüglich einem threadsaven Zugriffs nicht von außen abgreifbar sein sollten, sind sie als lokale Variable im Execute eh besser aufgehoben.
Das Event als Property, auf welches man auch nach dem Start extern zugreifen kann, fliegt auch raus.
Sowas lässt sich wundeschön als Startparameter übergeben, also als Parameter ans Create und das Start aka Suspended=False ebenfalls.
Die restlichen Variablen/Felder müssen nicht unbedingt global sein.

Durch das FreeOnTerminate sind die Thread-Variable und seine Property von Extern sowieso nicht mehr zu verwenden, da sie per se als ungültig zu betrachten sind, denn von extern weißt du nicht, ob der Thread schon fertig ist und freigegeben wurde.

Selbst wenn der Thread blieb, dann dort als ClassProcedure rein,
aber jetzt einfach mal blöd als einface Prozedur.

Delphi-Quellcode:
  TFileDownloader = class(TThread)
  private
    FURL: string;
    FStream: TStream;
    FOnDownloadComplete: TDownloadCompleteEvent;
    procedure DownloadFile;
    procedure DoDownloadComplete;
  protected
    procedure Execute; override;
  public
    class procedure Download(const URL: String; OnDownloadComplete: TDownloadCompleteEvent); static;
  end;
Und weil jetzt vom der Threadableitung im Grunde nichts mehr übrig ist, lass ich des nachfolgend einfach mal weg.
Delphi-Quellcode:
uses
  System.RTLConsts, System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient;

type
  TDownloadCompleteEvent = reference to procedure(Stream: TStream; Success: Boolean);

procedure ThreadedDownloadFile(const URL: String; OnDownloadComplete: TDownloadCompleteEvent);
begin
  if (URL = '') or not Assigned(OnDownloadComplete) then
    raise EArgumentException.CreateRes(@SArgumentNil);
  TThread.CreateAnonymousThread(
    procedure
    var
      HTTP: THTTPClient;
      Stream: TMemorystream;
      Success: Boolean;
    begin
      TThread.NameThreadForDebugging('ThreadedDownloadFile');
      HTTP := THTTPClient.Create;
      Stream := TMemoryStream.Create;
      try
        HTTP.CustomHeaders['Pragma'] := 'no-cache';
        try
          HTTP.Get(URL, Stream);
          Success := True;
        except
          Success := False;
        end;
        TThread.Synchronize(nil,
          procedure
          begin
            OnDownloadComplete(Stream, Success);
          end);
      finally
        Stream.Free;
        HTTP.Free;
      end;
    end).Start;
end;



ThreadedDownloadFile('http://sonst.wo/dat.ei', YourDownloadComplete);

// oder
ThreadedDownloadFile('http://sonst.wo/dat.ei',
  procedure(const URL: String; Stream: TStream; Success: Boolean)
  begin
    if Success then
      Machwas(Stream);
  end);
Gut, dass man im OnDownloadComplete zwar mitbekommt, ob es nicht ging, aber nicht warum ... statt eines doofen Success könnte man z.B. die Exception oder ihre Message übergeben.

Für mehrere Downloads das selbe OnDownloadComplete, dann auch die URL mit als Parameter dort rein, damit man dort weiß was es ist.
Delphi-Quellcode:
uses
  System.RTLConsts, System.SysUtils, System.Classes, System.Net.HttpClient, System.Net.URLClient;

type
  TDownloadCompleteEvent = reference to procedure(const URL: String; Stream: TStream; Error: Exception);

procedure ThreadedDownloadFile(const URL: String; OnDownloadComplete: TDownloadCompleteEvent);
begin
  if (URL = '') or not Assigned(OnDownloadComplete) then
    raise EArgumentException.CreateRes(@SArgumentNil);
  TThread.CreateAnonymousThread(
    procedure
    var
      HTTP: THTTPClient;
      Stream: TMemorystream;
      Success: Boolean;
      DError: Exception;
    begin
      TThread.NameThreadForDebugging('ThreadedDownloadFile');
      DError := nil;
      HTTP := THTTPClient.Create;
      Stream := TMemoryStream.Create;
      try
        HTTP.CustomHeaders['Pragma'] := 'no-cache';
        try
          HTTP.Get(URL, Stream);
          {TThread.Synchronize(nil,
            procedure
            begin
              OnDownloadComplete(URL, Stream, nil);
            end);}

        except
          {on E: Exception do begin
            TempErr := E;  // durch einen Bug muß es kopiert werden, auch wenn der Compiler sich bei OnDownloadComplete(URL, nil, E); nicht beschwert ... E ist im Sync leider NIL
            TThread.Synchronize(nil,
              procedure
              begin
                //OnDownloadComplete(URL, nil, E);  // siehe Bugreport im alten QualityPotal
                OnDownloadComplete(URL, nil, TempErr);
              end);
          end;}

          FreeAndNil(Stream); // für das if-Assigned im OnDownloadComplete ... oder einfach lassen, auch wenn es eh nichts sinnvolles enthält, und nur auf Assigned(Error) prüfen
          DError := AcquireExceptionObject as Exception;
        end;
        TThread.Synchronize(nil,
          procedure
          begin
            OnDownloadComplete(URL, Stream, DError);
          end);
      finally
        DError.Free;
        Stream.Free;
        HTTP.Free;
      end;
    end).Start;
end;



ThreadedDownloadFile('http://sonst.wo/dat.ei', YourDownloadComplete);

// oder
ThreadedDownloadFile('http://sonst.wo/dat.ei',
  procedure(const URL: String; Stream: TStream; Error: Exception)
  begin
    if Assigned(Stream) then begin // if not Assigned(Error) then
      Machwas(Stream);
      ShowMessage('Download complete: ' + URL);
    end;
  end);
Ein Therapeut entspricht 1024 Gigapeut.

Geändert von himitsu (17. Jul 2024 um 11:43 Uhr)
  Mit Zitat antworten Zitat
Stewag

Registriert seit: 12. Jun 2008
180 Beiträge
 
Delphi 12 Athens
 
#6

AW: Einbinden eines Thread Downloads

  Alt 19. Jul 2024, 18:17
Danke für deine Hinweise und Code, Himitsu!

Da ich mit threads aber kaum Erfahrung habe, bekomme ich damit leider keine funktionierende SyncThreadedDownload Unit hin.

Verstehe ich das richtig, dass weder Constructor noch Destructor dafür benötigt werden?

Dies läuft bei mir auch nicht:
Code:
TDownloadCompleteEvent = reference to procedure(Stream: TStream; Success: Boolean);
Vor allem aber verstehe ich nicht, wie/wo deine Prozedur ThreadedDownloadFile einzubinden ist.

Eigentlich sollte doch eine Unit, die eine Datei in einem synchronen Thread lädt, ein übliches Mittel in jedem Werkzeugkasten sein. Deshalb wundert es mich, dass ich auch nach intensivem googeln nichts dergleichen finde.

Könnte hier vielleicht jemand netterweise seine entsprechende Unit (Firemonkey!) zur Verfügung stellen?

Da niemand etwas anderes erwähnt hat, ist es wohl erforderlich, dass nach dem Aufruf des Download Threads ein Timer zyklisch prüft, ob OnDownladComplete schon ein Ergebnis bereit gestellt hat?
Steffen
  Mit Zitat antworten Zitat
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 13:51 Uhr.
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz