![]() |
Einbinden eines Thread Downloads
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:
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.
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; Die Unit funktioniert im Testprojekt auch und synchronisiert den Ergebnisstream in den Hauptthread über DownloadComplete zurück:
Code:
Mein Problem nun:
procedure TForm1.DownloadComplete(const stream: TStream; Success: Boolean);
begin if Success then begin memWerte.lines.LoadFromStream(stream) end else Label1.Text := 'Download failed'; end; 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; |
AW: Einbinden eines Thread Downloads
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) |
AW: Einbinden eines Thread Downloads
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:lol: Hier der korrekte Aufruf:
Code:
Meine ursprüngliche Frage ist damit aber nicht beantwortet.
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; Hier die (lauffähige) Unit:
Code:
Gibt es dazu Anmerkungen?
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. |
AW: Einbinden eines Thread Downloads
Du gibst den Stream nur frei, wenn dem Ereignis ein Handler zugewiesen wurde?
|
AW: Einbinden eines Thread Downloads
Warum den THTTPClient doppelt erstellen?
Im Excecute ist es OK, das im Create war eh ungenutzt.
Delphi-Quellcode:
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:
Für die Funktion zwar nicht relevant, aber
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;
Delphi-Quellcode:
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:
Und weil jetzt vom der Threadableitung im Grunde nichts mehr übrig ist, lass ich des nachfolgend einfach mal weg.
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;
Delphi-Quellcode:
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.
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); 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); |
AW: Einbinden eines Thread Downloads
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:
Vor allem aber verstehe ich nicht, wie/wo deine Prozedur ThreadedDownloadFile einzubinden ist.
TDownloadCompleteEvent = reference to procedure(Stream: TStream; Success: Boolean);
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? |
AW: Einbinden eines Thread Downloads
Zitat:
|
AW: Einbinden eines Thread Downloads
Es gibt Fremd-Bibliotheken, die sowas drin haben.
Das Ganze kann man ja noch mit vielen Dingen erweitern und somit nicht allen Recht machen ... den Einen isses zuviel und Anderen fehlt immer irgendwas. * z.B. ein Pooling, damit man 200 Dateien downloaden kann, aber nicht zu viele Threads gleichzeitig laufen. * schönere Fehlernehandlung * Proxy * uvm. Je nach Anforderung muß man sich dann was Passendes suchen. Es gibt verschiedene Arten von Prozeduren/Methoden und genauso gibt es verschiedene Arten von MethodenZeigern, welche Referenzen auf diese Methoden aufnehmen können.
Delphi-Quellcode:
type
TEvent1 = reference to procedure(const URL: String; Stream: TStream; Error: Exception); // Proc1, Proc2, Proc3, Proc4 und Inline5 TEvent2 = procedure(const URL: String; Stream: TStream; Error: Exception) of object; // Proc2, Proc3 TEvent3 = procedure(const URL: String; Stream: TStream; Error: Exception); // Proc1 (glaub auch Proc4) // Prozedur procedure Proc1(const URL: String; Stream: TStream; Error: Exception); type TDemo = class // Methode procedure Proc2(const URL: String; Stream: TStream; Error: Exception); // KlassenMethode class procedure Proc3(const URL: String; Stream: TStream; Error: Exception); // statische KlassenMethode class procedure Proc4(const URL: String; Stream: TStream; Error: Exception); static; end; // anonyme Methode, also "Inline5" im Code begin eineVariable := procedure(const URL: String; Stream: TStream; Error: Exception) begin end; AlsParameter(procedure(const URL: String; Stream: TStream; Error: Exception) begin end); end; // eingebettete Prozedur procedure {TDemo.}Test; procedure Proc6(const URL: String; Stream: TStream; Error: Exception); begin end; begin Proc6(…); end; Freigeben muß man den Thread ja nicht selbst, denn durch das FreeOnTerminate gibt er sich selbst frei. Weil man extern nicht weiß wann, darf extern grundsätzlich niemals "ungeschützt" auf eine Variable zugegriffen werden, welche auf diesen Thread zeigt. Extern garkeine Variable, mit dem Objektzeiger, und schon kommt auch niemand auf dumme Ideen. Ohne FreeOnTerminate könnte man extern z.B. auf Thread.Terminated prüfen und anschließend selbst den den Thread freigeben, bzw. vorher z.B. die runtergeladenen Daten von ihm abholen. Man kann selbst TThread ableiten und seinen Code ins Execute einfügen, sowie eventuell auch ins Create. Im Prnzip gibt es aber bereits eine fertige Ableitung des TThread, die eine Prozedur bekommt, welche sie dann im Thread ausführt. ![]()
Delphi-Quellcode:
Also anstatt einer eigenen TThread-Klasse, hatte ich einfach nur dieses Fertige benutzt.
type
TAnonymousThread = class(TThread) private FProc: TProc; protected procedure Execute; override; public constructor Create(const AProc: TProc); end; class function TThread.CreateAnonymousThread(const ThreadProc: TProc): TThread; begin Result := TAnonymousThread.Create(ThreadProc); end; constructor TAnonymousThread.Create(const AProc: TProc); begin inherited Create(True); FreeOnTerminate := True; FProc := AProc; end; procedure TAnonymousThread.Execute; begin FProc(); end;
Ohne den auskommentierten Code, für eine alternative Behandlung, ist es hier recht kurz gehalten (passt alles komplett auf den Bildschirm), und durch die anonymen (eingebetteten) Methoden liegt hier der Code funktional übersichtlich in seiner Ablaufreihenfolge. Aber grundsätzlich ist es auch kein Problem Vieles in externe Methoden auszulagern, aber dann am Besten schön in einer Klasse gekapselt. Bei dem eingebetteten Synchronize kann man es auch ausnutzen, dass sich Variablen "automatisch" vom Übergeordneten Code dort reingeben lässt. (Delphi verschiebt diese Variablen "heimlich" in ein Interface, mit Referenzzähung ... bis der aufrufende Code und der Eingebetette alle beendet wurden) |
AW: Einbinden eines Thread Downloads
Zitat:
Himitsu ist sehr klug und verdient sicher seine Brötchen mit Software. Ich bin Hobbyprogrammierer, verstehe leider nur Bahnhof und habe kein Problem damit auch Code einzusetzen, den ich nicht bis ins Letzte durchdrungen habe. :cry: |
AW: Einbinden eines Thread Downloads
Zitat:
Wenn du den Code in eine separate Unit packen möchtest, musst du die Funktion natürlich unter interface deklarieren, damit sie aus anderen Units sichtbar ist:
Delphi-Quellcode:
Dann schreibst du die Unit SyncThreadedDownload dort, wo du sie nutzen möchtest, unter uses. Dann kannst du einfach das Beispiel von himitsu 1:1 verwenden:
unit SyncThreadedDownload;
interface 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); implementation procedure ThreadedDownloadFile(const URL: String; OnDownloadComplete: TDownloadCompleteEvent); begin ... end; end.
Delphi-Quellcode:
Statt Machwas(Stream) musst du natürlich den heruntergeladenen Stream mit den Daten verwenden, um die Datei z.B. zu verarbeiten oder zu speichern. Und natürlich musst du auch die korrekte Adresse der Datei eintragen.
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); |
Alle Zeitangaben in WEZ +1. Es ist jetzt 16:29 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-2025 by Thomas Breitkreuz