2012-11-30 15 views
19

Voglio implementare un semplice downloader HTTP usando TIdHttp (Indy10). Ho trovato due tipi di esempi di codice da internet. Sfortunatamente nessuno di loro mi soddisfa al 100%. Ecco il codice e voglio qualche consiglio.Scarica il file progressivamente usando TIdHttp


Variante 1

var 
    Buffer: TFileStream; 
    HttpClient: TIdHttp; 
begin 
    Buffer := TFileStream.Create('somefile.exe', fmCreate or fmShareDenyWrite); 
    try 
    HttpClient := TIdHttp.Create(nil); 
    try 
     HttpClient.Get('http://somewhere.com/somefile.exe', Buffer); // wait until it is done 
    finally 
     HttpClient.Free; 
    end; 
    finally 
    Buffer.Free; 
    end; 
end; 

Il codice è compatto e molto facile da capire. Il problema è che alloca lo spazio su disco all'avvio del download. Un altro problema è che non possiamo mostrare direttamente l'avanzamento del download nella GUI, a meno che il codice non venga eseguito in un thread in background (in alternativa possiamo associare l'evento HttpClient.OnWork).


Variante 2:

const 
    RECV_BUFFER_SIZE = 32768; 
var 
    HttpClient: TIdHttp; 
    FileSize: Int64; 
    Buffer: TMemoryStream; 
begin 
    HttpClient := TIdHttp.Create(nil); 
    try 
    HttpClient.Head('http://somewhere.com/somefile.exe'); 
    FileSize := HttpClient.Response.ContentLength; 

    Buffer := TMemoryStream.Create; 
    try 
     while Buffer.Size < FileSize do 
     begin 
     HttpClient.Request.ContentRangeStart := Buffer.Size; 
     if Buffer.Size + RECV_BUFFER_SIZE < FileSize then 
      HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1 
     else 
      HttpClient.Request.ContentRangeEnd := FileSize; 

     HttpClient.Get(HttpClient.URL.URI, Buffer); // wait until it is done 
     Buffer.SaveToFile('somefile.exe'); 
     end; 
    finally 
     Buffer.Free; 
    end; 
    finally 
    HttpClient.Free; 
    end; 
end; 

In primo luogo abbiamo interrogare la dimensione del file dal server e poi scaricare il contenuto dei file in pezzi. I contenuti dei file recuperati verranno salvati sul disco quando vengono ricevuti completamente. Il potenziale problema è che dobbiamo inviare più richieste GET al server. Non sono sicuro che alcuni server (come megaupload) potrebbero limitare il numero di richieste entro un determinato periodo di tempo.


Le mie aspettative

  1. Il downloader devono inviare un solo GET-richiesta al server.
  2. Lo spazio su disco non deve essere allocato all'avvio del download.

Eventuali suggerimenti sono apprezzati.

+4

Se si desidera un caching 'TFileStream', guarda Il contributo di David qui: [File bufferizzati (per un accesso più veloce al disco)] (http://stackoverflow.com/a/5639712/576719). –

risposta

26

La variante n. 1 è la più semplice ed è la modalità di utilizzo di Indy.

Per quanto riguarda il problema di allocazione del disco, è possibile derivare una nuova classe da TFileStream e sovrascrivere il metodo SetSize() per non eseguire alcuna operazione. TIdHTTP tenterà comunque di pre-allocare il file quando appropriato, ma in realtà non allocherà alcun spazio su disco. Scrivendo su TFileStream il file crescerà come necessario.

Per quanto riguarda lo stato di segnalazione, TIdHTTP ha OnWork... eventi a tale scopo. Il parametro AWorkCountMax di OnWorkBegin sarà la dimensione effettiva del file se nota (la risposta non è suddivisa in blocchi) o 0 se non è nota. Il parametro AWorkCount dell'evento OnWork sarà il numero cumulativo di byte che sono stati trasferiti fino a quel momento.Se la dimensione del file è nota, puoi visualizzare la percentuale totale dividendo semplicemente lo AWorkCount con lo AWorkCountMax e moltiplicando per 100, altrimenti visualizza solo il valore AWorkCount da solo. Se si desidera visualizzare la velocità del trasferimento, è possibile calcolarla dalla differenza dei valori AWorkCount e degli intervalli di tempo tra più eventi OnWork.

Prova questa:

type 
    TNoPresizeFileStream = class(TFileStream) 
    procedure 
    procedure SetSize(const NewSize: Int64); override; 
    end; 

procedure TNoPresizeFileStream.SetSize(const NewSize: Int64); 
begin 
end; 

.

type 
    TSomeClass = class(TSomething) 
    ... 
    TotalBytes: In64; 
    LastWorkCount: Int64; 
    LastTicks: LongWord; 
    procedure Download; 
    procedure HttpWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64); 
    procedure HttpWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64); 
    procedure HttpWorkEnd(ASender: TObject; AWorkMode: TWorkMode); 
    ... 
    end; 

procedure TSomeClass.Download; 
var 
    Buffer: TNoPresizeFileStream; 
    HttpClient: TIdHttp; 
begin 
    Buffer := TNoPresizeFileStream.Create('somefile.exe', fmCreate or fmShareDenyWrite); 
    try 
    HttpClient := TIdHttp.Create(nil); 
    try 
     HttpClient.OnWorkBegin := HttpWorkBegin; 
     HttpClient.OnWork := HttpWork; 
     HttpClient.OnWorkEnd := HttpWorkEnd; 

     HttpClient.Get('http://somewhere.com/somefile.exe', Buffer); // wait until it is done 
    finally 
     HttpClient.Free; 
    end; 
    finally 
    Buffer.Free; 
    end; 
end; 

procedure TSomeClass.HttpWorkBegin(ASender: TObject; AWorkMode: TWorkMode; AWorkCountMax: Int64); 
begin 
    if AWorkMode <> wmRead then Exit; 

    // initialize the status UI as needed... 
    // 
    // If TIdHTTP is running in the main thread, update your UI 
    // components directly as needed and then call the Form's 
    // Update() method to perform a repaint, or Application.ProcessMessages() 
    // to process other UI operations, like button presses (for 
    // cancelling the download, for instance). 
    // 
    // If TIdHTTP is running in a worker thread, use the TIdNotify 
    // or TIdSync class to update the UI components as needed, and 
    // let the OS dispatch repaints and other messages normally... 

    TotalBytes := AWorkCountMax; 
    LastWorkCount := 0; 
    LastTicks := Ticks; 
end; 

procedure TSomeClass.HttpWork(ASender: TObject; AWorkMode: TWorkMode; AWorkCount: Int64); 
var 
    PercentDone: Integer; 
    ElapsedMS: LongWord; 
    BytesTransferred: Int64; 
    BytesPerSec: Int64; 
begin 
    if AWorkMode <> wmRead then Exit; 

    ElapsedMS := GetTickDiff(LastTicks, Ticks); 
    if ElapsedMS = 0 then ElapsedMS := 1; // avoid EDivByZero error 

    if TotalBytes > 0 then 
    PercentDone := (Double(AWorkCount)/TotalBytes) * 100.0; 
    else 
    PercentDone := 0.0; 

    BytesTransferred := AWorkCount - LastWorkCount; 

    // using just BytesTransferred and ElapsedMS, you can calculate 
    // all kinds of speed stats - b/kb/mb/gm per sec/min/hr/day ... 
    BytesPerSec := (Double(BytesTransferred) * 1000)/ElapsedMS; 

    // update the status UI as needed... 

    LastWorkCount := AWorkCount; 
    LastTicks := Ticks; 
end; 

procedure TSomeClass.HttpWorkEnd(ASender: TObject; AWorkMode: TWorkMode); 
begin 
    if AWorkMode <> wmRead then Exit; 

    // finalize the status UI as needed... 
end; 
+0

Solo qualcosa non correlato ma ho visto nel tuo codice: usi le zecche per calcolare l'intervallo di tempo. Non è un grosso problema in questo esempio. Ma suggerisco di utilizzare TDateTime per rappresentare StartTime e StopTime e utilizzare TTimeSpan.Subtract (StopTime, StartTime) per calcolare l'intervallo di tempo. Perché MSDN dice che i segni di spunta verranno ripristinati se il sistema viene eseguito continuamente per 49,7 giorni. Se l'applicazione viene eseguita su un server, la durata potrebbe essere calcolata in modo errato. – stanleyxu2005

+6

Ho scelto intenzionalmente di non utilizzare 'TDateTime' perché non volevo che il codice fosse influenzato dalle possibili modifiche all'orologio (ora legale, manipolazione dell'utente, ecc.). Inoltre, il codice scandisce gli intervalli che si verificano tra gli eventi, che non si avvicinano mai al limite di 49,7 giorni di "LongWord". 'GetTickDiff()' rappresenta il wrap-around che si verifica ogni volta che 'GetTickCount()' torna a zero, quindi non è un problema. –

+3

@RemyLebeau Come ho detto ieri ho provato di nuovo Indy, questa volta versione 10 e mi sento come se la v10 sia davvero molto meglio di 9 e funzioni bene. Ho già ripreso, loggato tramite HTTP POST e ho usato le idee del tuo codice qui sopra per fare alcune statistiche quindi sì- Indy è grande, grazie amico! – Tom

4

Ecco un esempio che mostra come utilizzare i componenti OnWork per mostrare una barra di avanzamento:

Download a File from internet programatically with an Progress event using Delphi and Indy

Non si deve preoccupare della allocazione del disco. Lo spazio su disco allocato in realtà non è stato scritto, quindi non danneggerà i tuoi dischi. Sii felice che sia assegnato in modo che non sia possibile che un altro processo rivendichi lo spazio su disco e ti lasci libero dallo spazio!

2

Non dimenticare di aggiungere questo per la variante 2

: Else HttpClient.Request.ContentRangeEnd := FileSize; 

Sostituire

if Buffer.Size + RECV_BUFFER_SIZE < FileSize then 
    HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1; 

Con

if Buffer.Size + RECV_BUFFER_SIZE < FileSize then 
    HttpClient.Request.ContentRangeEnd := Buffer.Size + RECV_BUFFER_SIZE - 1; 
    Else HttpClient.Request.ContentRangeEnd := FileSize; 
+0

Grazie, hai ragione. Il codice è aggiornato – stanleyxu2005