BrainSpace.it » Blog Archive » Java ed il protocollo HTTP (Terza Parte)

Java ed il protocollo HTTP (Terza Parte)

 

Nella seconda parte della serie “Java ed il protocollo HTTP” abbiamo imparato come scaricare un file da un server HTTP remoto leggendo un flusso sequenziale di dati; in questa terza parte sfrutteremo il capo “Range” dell’header HTTP per scaricare il file remoto dividendolo in più segmenti che saranno scaricati in modo parallelo. La figura seguente può aiutare a capire il procedimento che, normalmente, è definito multi-threading download:

schema_mutlithread_dl.png

 

E questo è il risultato finale che otterremo:

L'applicazione in esecuzione - Fare click per vederla a dimensioni reali

SimpleMultithreadDownloader in esecuzione (Fai click per vedere l’immagine a dimensione reale)

Ok, ora che abbiamo un idea di massima di cosa vogliamo ottenere cerchiamo di dare un occhiata a come è organizzato il progetto; i sorgenti ed i file di progetto per l’IDE NetBeans sono disponibili sotto licenza GPL sezione “Riferimenti”.

Struttura

Il progetto chiamato (con scarsissima fantasia) SimpleMultithreadDownloader è costituito da 7 classi:

  • Main: crea semplicemente una nuova istanza della finestra principale dell’applicazione dopo aver impostato il Look&Feel dell’applicazione in base a quello predefinito dell’ambiente di esecuzione.
  • MainFrame: estende la classe JFrame per implementare la finestra principale dell’applicazione.
  • TranferProgressPanel: estende la classe JPanel per implementare un pannello informativo sullo stato del trasferimento. E’ impiegato per monitorare lo stato sia lo stato globale del download e sia per quello di ciascun segmento.
  • AddDownloadDialog: estende la classe JDialog ed implementa una semplice finestra per l’inserimento e la validazione del URL del file remoto e del percorso del file di destinazione.
  • Utils: Definisce alcuni indici e rende disponibili alcune funzioni utili alle altre classi.
  • MainTranferThread: Implementa il thread principale che si occupa di inizializzare i segmenti, avviare e monitorare il download riassemblare i segmenti scaricati in un unico file sul percorso di destinazione al termine del trasferimento.
  • SegmentTranferThread: singolo thread che si occupa del download di un segmento.

Le classi sono in relazione fra loro secondo lo schema UML seguente (solo Class Diagram):

Fai cliick sulla miniatura per vedere l'immagine a dimensione reale
Fai click sulla miniatura per vedere l’immagine a dimensione reale

Funzionamento

Come visto dalla struttura il progetto è composto essenzialmente di alcune classi che costituiscono la GUI dell’applicazione, mentre le classi che materialmente eseguono il trasferimento sono MainTranferThread e SegmentTranferThread. Volutamente tralascerò di illustrare le operazioni compiute dalle classi che costituiscono l’interfaccia, piuttosto comprensibili a chi abbia un minimo di esperienza con Swing (grazie anche al codice dettagliatamente commentato ed alla documentazione dell’API generata con Javadoc) per concentrarmi sulle classi che eseguono il download multi-thread.

Una volta che l’utente ha inserito l’indirizzo URL del file da scaricare ed il percorso di destinazione (notare che se gli appunti contengono un indirizzo URL valido il percorso sorgente e destinazione saranno generati automaticamente a partire da tale indirizzo) la finestra principale si occupa di creare una nuova istanza della classe MainTranferThread e la inizializza con i gli indirizzi di sorgente e destinazione inseriti. Il metodo init(String sourceURL, String destinationPath), fornito dalla classe MainTranferThread, si occupa di leggere le informazioni relative al file roto ed a calcolare il numero e la dimensione di segmenti da utilizzare per il download. La dimensione minima di un segmento è fissata a 100KB ed il numero massimo di segmenti utilizzabili è 10:

// Tenta di stabilire una connessione con l'URL sorgente per leggere
// le informazioni relative al file da scaricare.
URLConnection connection = new URL(sourceURL).openConnection();

// Legge e morizza la dimensione totale del file da scaricare...
this.downloadSize = connection.getContentLength();
// ..ed il suo MIME-Type
this.contentType = connection.getContentType();

// Inizia il ciclo per il calcolo del numero di segmenti e la loro dimensione. Il ciclo
// parte da un numero di segmenti pari ad uno solamente ed aggiunge un segmento ogni
// 100 KB di dati da trasferire fino ad arrivare ad un massimo di 10 segmenti.
for (segmentsCount = 1; segmentsCount < = 10; segmentsCount++) {
// Calcola la dimensione media di un segmento
segmentsAvgSize = downloadSize / segmentsCount;

// Se la dimensione media degli "segmentsCount" segmenti è inferirore o uguale
// a 100 KB...
if (segmentsAvgSize <= 102400) {
// ... il numero di segmenti calcolati è sufficiente per eseguire il download ed
// il ciclo può terminare
break;
}
// ..altrimenti aggiunge un ulteriore segmento (se il numero di segmenti è ancora inferirore a 10)
}

Dato che il calcolo della dimensione media di un segmento restituisce un numero intero è possibile che la dimensione dell’ultimo segmento non corrisponda a quella media e quindi deve essere ricalcolata:

// Ricalcola la dimensione dell'ultimo segmento in quanto la dimensione media non
// considera i numeri decimali, quindi fermo restando la dimensione media del segmento
// e' necessario ricalcolare la dimensione dell'ultimo in modo che sia:
//
// dimensione ultimo segmento = dimensione totale - somma della dimensione di tutti i segmenti tranne l'ultimo
int lastSegmentSize = downloadSize - (segmentsAvgSize * (segmentsCount - 1));

Infine vengono create tante istanze della classe SegmentTranferThread quanti sono i segmenti da utilizzare e ciascuna istanza è inizializzata, oltre che con i percorsi di sorgente e destinazione, anche con l’offset del primo byte del segmento all’interno del file originale e con la dimensione del segmento stesso:

// Offset iniziale del primo segmento
int segmentOffset = 0;

// Inizializza l'array dei segmenti di trasferinto al numero di segmenti calcolato.
segmentsThreads = new SegmentTranferThread[segmentsCount];

// Inizia il ciclo di inizializzazione dei singoli segmenti.
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++) {
// Crea il nuovo segmento
segmentsThreads[segmentIndex] = new SegmentTranferThread();

boolean result;

// Inizializza il segmento con l'RUL sorgente, il path di destinazione l'offset del primo byte da leggere
// . Controlla se il segmento è l'ultimo, in questo caso passa
// al metodo init la lunghezza dell'ultimo segmento anzichè quella media.
if (segmentIndex == (segmentsCount - 1)) {
result = segmentsThreads[segmentIndex].init(sourceURL, destinationPath, segmentOffset, lastSegmentSize);
} else {
result = segmentsThreads[segmentIndex].init(sourceURL, destinationPath, segmentOffset, segmentsAvgSize);
}

// Controlla se il segmento è stato inizializzato correttamente...
if (result == false) {
// se si è verificato un errore imposta lo stato del trasferimento ad "Errore" e termina
// l'inizializzazione restituendo false.
tranferStatus = Utils.STATUS_ERROR;
return false;
}

// Sposta l'offset del segmento successivo dolo l'ultimo byte
// che deve essere letto dal segmento attuale.
segmentOffset += segmentsAvgSize;
}

Quando un segmento viene inizializzato tramite l’invocazione del metodo init(String sourceURL, String destinationPath, int segmentOffset, int segmentsSize) della classe SegmentTranferThread, viene creato un nuovo file tporaneo per la morizzazione dei dati scaricati dal segmento e tale file è marchiato per essere rimosso automaticamente al termine dell’esecuzione dell’applicazione:

// Crea un file tporaneo con il nome basato sul nome del file da prelevare ed estensione .part
// Il file sarà creato nella cartella tporanea di sista (in linux: /tmp)
File tpFile = File.createTpFile(new File(destinationPath).getName(), ".part");

// Imposta il file tporaneo perchè sia cancellato automaticamente alla chiusura dell'applicazione
tpFile.deleteOnExit();

Tutti i files tporanei generati dai segmenti saranno utilizzati al termine del trasferimento per generare il fie di destinazione.
Se l’inizializzazione va a buon fine l’utente può iniziare il trasferimento. Alla pressione del bottone di avvio del trasferimento sulla toolbar viene eseguito il thread principale che regola il download. L’esecuzione del thread implica l’invocazione del metodo run() della classe MainTranferThread il quale per prima cosa avvia a sua volta l’esecuzione dei threads precedentente inizializzati per il download dei segmenti:

// Avvia tutti i threads dei segmenti di trasferimento.
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++) {
segmentsThreads[segmentIndex].start();
}

 

Fatto ciò, dopo aver inizializzato alcuni contatori utili, inizia il ciclo di monitoraggio del trasferimento; durante tale ciclo il thread principale controlla lo stato di tutti i trasferimenti dei segmenti per rilevare eventuali errori, inoltre calcola il totale dei dati trasferiti. Se viene rilevato un errore in anche solo uno dei segmenti il trasferimento si interrompe. Il ciclo di controllo termina correttamente solamente quando tutti i segmenti sono stati scaricati senza errori:

// Azera il contatore del totale dei bytes scaricati per poi poterlo ricalcolare in base ai
// bytes trasferiti dai segmenti.
bytesDownloaded = 0;

// Imposta il flag di controllo dello stato dei segmenti.
// la variabile allSegmentFinished assumerà il valore "true" sono quando il trasferimento
// di tutti i segmenti sarà completo
boolean allSegmentFinished = false;

// Impsta lo stato del download come "Traferimento"
tranferStatus = Utils.STATUS_RUNNING;

// Controlla ciclicamente lo stato del trasferimento di tutti i segmenti
// fino a quando non sono stati completati tutti o fino al verificarsi
// di un errore.
while (allSegmentFinished == false) {
// Imposta il flag di completamento a "true" per consentire il termine del ciclo
// in caso il trasferimento di tutti i segmenti sia terminato.
allSegmentFinished = true;

// Variabile ausiliaria utilizzata per il calcolo della dimensione totale dei dati
// scaricati.
int segmentsBytesDownloaded = 0;

// Inizia il controllo di ciascun segmento
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++) {
// Se si è verificato un errore durante il download del segmento...
if (segmentsThreads[segmentIndex].getTranferStatus() == Utils.STATUS_ERROR) {
// ...imposta lo stato generale ad "Errore" e termina il controllo.
tranferStatus = Utils.STATUS_ERROR;
return;
}

// Aggiunge al contatore ausiliario dei bytes scaricati il totale dei bytes trasferiti
// dal segmento in esame.
segmentsBytesDownloaded += segmentsThreads[segmentIndex].getBytesDownloaded();

// Aggiorna il flag di completamento tramite moperazione logica AND,
// controllando se il segmento in esame è termnato
allSegmentFinished = allSegmentFinished && (segmentsThreads[segmentIndex].getTranferStatus() == Utils.STATUS_COMPLETED);
}

// Aggiorna il totale dei bytes scaricati.
bytesDownloaded = segmentsBytesDownloaded;
}

Se il trasferimento di tutti i segmenti è stato completato senza errori il thread principale si occupa di ricostruire il file di destinazione a partire dai files temporanei creati da ciascun segmento:

// Se il trasferimento dei singoli segmenti è terminato correttamente si procede con la ricostruzione
// del file di destinazione a partire dai singoli segmenti. Lo stato del trasferimento è posto a
// "Ricostruzione"
tranferStatus = Utils.STATUS_REBUILD;

// Utiliziamo la variabile che morizza la dimensione totale dei dati scaricati per monitorare
// lo stato di avanzamento della ricostruzione.
bytesDownloaded = 0;

try {
// Crea una istanza di un oggetto RandomAccessFile per poter scrivere sul file relativo al percorso di destinazione.
RandomAccessFile destinationWriter = new RandomAccessFile(destinationPath, "rw");

// Inizia il ciclo di lettura dei segmenti
for (int segmentIndex = 0; segmentIndex < segmentsCount; segmentIndex++) {
// Crea una istanza di RandomAccessFile per la lettura del file tporaneo contenente i dati trasferiti
// dal segmento in esame.
RandomAccessFile segmentFileReader = new RandomAccessFile(segmentsThreads[segmentIndex].getDestinationTpPath(), "r");

// Legge il primo byte dal file tporaneo del segmento.
int byteReaded = segmentFileReader.read();

while (byteReaded != -1) {
// scrive l'ultimo byte letto nel file di destinazione...
destinationWriter.write(byteReaded);
bytesDownloaded++;

// ...quindi legge il byte successivo.
byteReaded = segmentFileReader.read();
}

// Terminata la lettura di tutti i dati del segmento attuale chiude il file
// tporaneo in lettura
segmentFileReader.close();
}

// Terminata la ricotruzione senza errori chiude il file di destinazione.
destinationWriter.close();
} catch (IOException ex) {
// Se si verifica un errore durante la ricostruzione del file di destinazione avvisa l'utente,
// imposta lo statto di errore prima di terminare.
JOptionPane.showMessageDialog(null, "Si è verificato un errore durante la connessione ricostruzione del file di destinazione.\nIl download non potrà essere eseguito.\n\nDettagli: " + ex.getLocalizedMessage(), "Errore di ricostruzione della destinazione", JOptionPane.ERROR_MESSAGE);
tranferStatus = Utils.STATUS_ERROR;
return;
}

// Se il trasferimento e la ricostruzione sono andati entrambi a buon fine imposta lo stato del
// trasferimento a "Completato"
tranferStatus = Utils.STATUS_COMPLETED;

Il campo “Range”

Ok, fino a qui è tutto abbastanza semplice, no? Però la cosa interessante è: come facciamo a “dire” al server HTTP che vogliamo scaricare un segmento di un file che vada dal byte N al byte N+K? Semplice, utilizziamo il campo Range dell’header HTTP che ci consente di specificare l’intervallo (range, per l’appunto) dei dati che vogliamo scaricare all’interno del file richiesto. La sintassi da utilizzare ‘ la seguente:

Range=byte iniziale-byte finale

Nel nostro caso ciascun segmento, all’avvio del thread e quindi all’invocazione del metodo run(), stabilisce una connessione con il server all’URL remoto e, prima di aprire uno stream di input per la lettura, aggiunge alla richiesta il campo Range correttamente inizializzato con la posizione del primo byte del segmento e quella dell’ultimo calcolata come posizione del primo byte + lunghezza del segmento:

// Tenta di creare un nuovo oggetto URL dalla stringa passata come indirizzo durante
// l'inizializzazione. Se la creazione va a buon fine apre una connessione con l'indirizzo
URLConnection connection = new URL(sourceURL).openConnection();

// Crea una nuova stringa da passare all'header di richiesta HTTP che sarà inoltrato al server
// prima di richiedere lo stream per il trasferimento.
// La proprietà di richiesta nell'header deve avere la forma:
//
// Range: primo byte da leggere-ultimo byte da leggere
//
// che in questo caso sarà:
//
// Range: segmentOffset-(segmentOffset + segmentsSize)
String bytesRange = "bytes=" + Integer.toString(segmentOffset) + "-" + Integer.toString(segmentOffset + segmentsSize);

// Aggiunge la stringa contente la richiesta del Range da leggere dal file di origine
// all'header HTTP.
connection.addRequestProperty("Range", bytesRange);

// Richiede uno stream di lettura per l'intervallo specificato tramite la connessione stabilita
// in precedenza
InputStream connectionInputStream = connection.getInputStream();

Il “trucco” è tutto quì! Infatti una volta aperto lo stream che legga il giusto intervallo si procede come già visto nel secondo articolo della serie “Java ed il protocollo HTTP”, l’unica differenza è che in questo caso utilizzero un file ad accesso casuale anzichè un FileWriter:

// Azzera il contatatori utilizzati durante il trasferimento.
int byteReaded = 0;
bytesDownloaded = 0;

// Imposta lo stato a "Trasferimento"
tranferStatus = Utils.STATUS_RUNNING;

// Legge il primo bayte del segmento corrispondente al byte "segmentOffset" nel file sorgente
byteReaded = connectionInputStream.read();

// Continua a leggere un byte per volta fino a quando non è stata raggiunta la lunghezza del segmento
// o non c'è più niente da leggere (in realtà non sarebbe necessario, ma evita sovrapposizioni di bytes)
while ((bytesDownloaded < segmentsSize) && (byteReaded != -1)) {
// Scrive l'ultimo byte letto nel file tporaneo di destinazione...
writer.write(byteReaded);
bytesDownloaded++;

// ...e legge il byte successivo.
byteReaded = connectionInputStream.read();
}

// Se il trasferimento va a buon fine chiude il file tporaneo.
writer.close();
// Chiude lo stream di input.
connectionInputStream.close();
// Imposta lo stato di trasferimento del segmento a "Completato"
tranferStatus = Utils.STATUS_COMPLETED;

Conclusioni

L’esempio proposto, proprio in quanto inteso come esempio didattico, di certo non offre ne le prestazioni ottimali possibili e ne le funzionalità solite dei programmi che eseguono download multi-threads. Alcune delle possibilità di ottimizzazione ed implementazione di funzionalità aggiuntive sono, ad esempio:

  • Solitamente i software di download multi-thread cercano di sfruttare diversi mirrors per il download dei segmenti in modo da distribuire il carico su più servers.
  • Ogni segmento potrebbe scaricare più di un byte per volta in modo da sfruttare la maggior quantità possibile di banda.
  • Il file finale, anziché essere ricostruito da thread principale di download, potrebbe essere ricostruito dai vari segmenti in modo parallelo.
  • Nonostante sia definito uno stato di “Pausa” questo non è implementato. Lascio a chi interessato l’implementazione (per altro estremamente semplice) della funzionalità.
  • Sarebbe opportuno poter salvare lo stato di un download per poterlo sospendere e riprendere in un secondo momento.

La lista potrebbe essere ancora lunga. Chiunque volesse provare ad implementare unove funzionalità più farlo tranquillamente partendo dal codice sorgente dell’esempio che si può trovare nella sezione seguente.

Riferimenti

Per una migliore comprensione il codice è ampiamente commentat, inoltre all’interno della cartella “dist/javadoc” è possibile trovatre la documentazione delle API rese disponibili dalle classi del progetto.

Come per i precedenti articoli, anche in questo caso il codice sorgente ed i files di progetto per l’IDE NetBeans 6.0 sono disponibili per il download coperti da licenza GPL: SimpleMultithreadDownloader (Sorgenti - Files progetto NetBeans 6) (0)

Per quanti volessero approfondire l” argomento HTTP è utile consultare questi siti:

Informazioni sull”HTTP in Wikipedia: Pagina su Wikipedia dedicata al protocollo HTTP

RFC 1945: Specifiche della versione 1.0 del protocollo HTTP

RFC 2616: Specifiche della versione 1.1 del protocollo HTTP

Scrivi un commento

Nota: I commenti devono essere approvati da un moderatore. Questo potrebbe rallentare la pubblicazione del commento. Non è necessario reinviare il tuo commento. Per favore pazienta.

Creato Da: Tommaso Frazzetto su modello del template Starrynight
Tutto il contenuto del blog è coperto da licenza Creative Commons