Multipart Download Implementierung

Ich entwickle schon seit Längerem ein Downloadprogramm für diverse Videoplattformen (YouTube, Veoh, Metacafe, …), und es funktioniert auch tadellos. Mein Problem ist allerdings der Download der Videos. Ich lade bisher alle Videos per BufferedInputStream mit einer Puffergröße von 6 KiB (1024*6) herunter. Für ein YouTube-Video von einer Größe von ~35 MB brauche ich mit meiner Methode ca. 6 Minuten. Mit DownThemAll (DownloadManager-AddOn für Firefox) hingegen brauche ich hingegen nichtmal 2 Minuten!

Ich würde diesen Multipart-Download-Algorithmus gerne in mein Programm integrieren, allerdings finde ich diesbezüglich keinerlei nützliche Anhaltspunkte. Ich weiß nichteinmal wie ich mit Java nur einen bestimmten Teil einer online verfügbaren Datei anfordere. :frowning:

Ich glaub das musst du von Hand machen, dh du schickst den HTTP Request selbst zum Server und nicht über URL Connection

Ah, muss ich das quasi im HTTP-Header angeben ab welcher Stelle ich die Datei haben will?

Ja, sieh dir am besten dazu die RFC von HTTP an

Puh, nach langem Suchen endlich gefunden. Das Zauberwort heisst Range.
URLConnection.addRequestProperty( "Range", "bytes=<from>-<to>" );

:smiley:

Ich hab mir die RFCs übrigens durchgeschaut - sie sind absolut grässlich! Zum Glück gibts etwas weniger lesefeindliche Ausgaben auf anderen Websites ^^

haha, RFCs sind aber die top Info Quelle :wink:

Soda, mein Downloadmanager ist im Prinzip schon fertig programmiert, nur funktioniert er leider nicht wie er sollte.

Ich teile die Datei in drei gleichgroße Teile und lade sie separat in temporäre Dateien um sie anschließend zu einer zusammenzufügen. Allerdings werden bei jedem der 3 Pakete um einiges weniger Daten vom Server gesendet als angefordert!

Ich hab mir von allen 3 Teildownloads die Header-Fields ausgeben lassen:

+++ CONNECTION 0 +++
null:
	HTTP/1.1 206 Partial Content
ETag:
	"394b-4e5566-46d1db8197d40"
Date:
	Mon, 06 Jul 2009 11:52:03 GMT
Content-Length:
	1711223
Last-Modified:
	Wed, 24 Jun 2009 20:11:57 GMT
Keep-Alive:
	timeout=5, max=100
Accept-Ranges:
	bytes
Connection:
	Keep-Alive
Content-Type:
	text/plain
Server:
	Apache/2.2.11 (Unix) DAV/2 mod_ssl/2.2.11 OpenSSL/0.9.8i PHP/5.2.8 mod_apreq2-20051231/2.6.0 mod_perl/2.0.4 Perl/v5.10.0
Content-Range:
	bytes 0-1711222/5133670


+++ CONNECTION 1 +++
null:
	HTTP/1.1 206 Partial Content
ETag:
	"394b-4e5566-46d1db8197d40"
Date:
	Mon, 06 Jul 2009 11:52:03 GMT
Content-Length:
	1711223
Last-Modified:
	Wed, 24 Jun 2009 20:11:57 GMT
Keep-Alive:
	timeout=5, max=100
Accept-Ranges:
	bytes
Connection:
	Keep-Alive
Content-Type:
	text/plain
Server:
	Apache/2.2.11 (Unix) DAV/2 mod_ssl/2.2.11 OpenSSL/0.9.8i PHP/5.2.8 mod_apreq2-20051231/2.6.0 mod_perl/2.0.4 Perl/v5.10.0
Content-Range:
	bytes 1711223-3422445/5133670


+++ CONNECTION 2 +++
null:
	HTTP/1.1 206 Partial Content
ETag:
	"394b-4e5566-46d1db8197d40"
Date:
	Mon, 06 Jul 2009 11:52:03 GMT
Content-Length:
	1711224
Last-Modified:
	Wed, 24 Jun 2009 20:11:57 GMT
Keep-Alive:
	timeout=5, max=100
Accept-Ranges:
	bytes
Connection:
	Keep-Alive
Content-Type:
	text/plain
Server:
	Apache/2.2.11 (Unix) DAV/2 mod_ssl/2.2.11 OpenSSL/0.9.8i PHP/5.2.8 mod_apreq2-20051231/2.6.0 mod_perl/2.0.4 Perl/v5.10.0
Content-Range:
	bytes 3422446-5133669/5133670

Das ist die Methode für den Downloadvorgang: ```/**
* Download a file over a network stream via special HTTP-technique.
*
* @param videoURL URL of target online file
* @throws IOException
*/
private void multipartDownload( URL videoURL, int contentLength, int parts ) throws IOException {
String mbFileSize = Double.toString( getMegaByte(contentLength) );
int loadedBytes = 0;
int partLength = contentLength/parts;

	int[] bufLen = new int[parts];
	byte[] buf = new byte[BUFFER_SIZE];
	
	File[] partFiles = new File[parts];
	URLConnection[] partConnections = new URLConnection[parts];
	BufferedOutputStream[] partOutputStreams = new BufferedOutputStream[parts];
	BufferedInputStream[] partInputStreams = new BufferedInputStream[parts];
	
	System.out.println( "Content-Length: " + contentLength );
	System.out.println( "Part-Length: " + partLength + " (" + parts + ")");
	
	// activate progress bar
	if ( this.progressBar != null ) {
		this.progressBar.setMaximum( contentLength+1 );
	}
	
	for ( int i=0; i < parts; i++ ) {
		partFiles** = File.createTempFile( this.output.getName(), ".part"+i );
		partOutputStreams** = new BufferedOutputStream( new FileOutputStream(partFiles**) );
		
		partConnections** = this.getURLConnection( videoURL );
		if ( i+1 == parts ) {
			partConnections**.setRequestProperty( "Range", "bytes=" + (partLength*i) + "-" + (contentLength-1) ); // last index != content length!!!
		} else {
			partConnections**.setRequestProperty( "Range", "bytes=" + (partLength*i) + "-" + (partLength*i+partLength-1) );
		}
		
		System.out.println( "

+++ CONNECTION " + i + " +++" );
listHeaderFields( partConnections** );

		partInputStreams** = new BufferedInputStream( partConnections**.getInputStream() );
	}
	
	while ( loadedBytes < contentLength ) {
		for ( int i=0; i < parts; i++ ) {
			if ( bufLen** != -1 ) {
				if ( (bufLen**=partInputStreams**.read(buf)) != -1 ) {
					// write bytes to harddisk
					partOutputStreams**.write( buf, 0, bufLen** );
					loadedBytes += bufLen**;
				} else {
					partInputStreams**.close();
					partOutputStreams**.close();
				}
			}
		}
		
		// update progress bar
		if ( this.progressBar != null ) {
			this.progressBar.setValue( loadedBytes );
			this.progressBar.setString( getMegaByte(loadedBytes) + " von " + mbFileSize + " MB geladen" );
		}
	}
	
	if ( this.progressBar != null )
		this.progressBar.setString( "Downloadsegmente werden zusammengefuegt ..." );
	
	// merge part files
	this.mergeParts( partFiles );
	
	if ( this.progressBar != null ) {
		this.progressBar.setString( "Download komplett!" );
		this.progressBar.setValue( contentLength+1 );
	}
}```

Hat jemand eine Ahnung was hier falsch läuft?
In diesem Fall sind die Teildateien immer um 7.287 Bytes zu klein.

Mein Tipp, nimm dir eine Textdatei und lad die runter, da siehst du am Besten wo Daten fehlen.
Eine Idee auch wenn ich das nicht ganz glaub, mach mal vor dem Schließen ein flush.

Shit, das gibts doch nicht! Ich habs jetzt mit einer kleinen Textdatei versucht (~500 Zeichen) und die Teildateien enthalten alle 0 Byte!
Jetzt hab ich mir mit System.out.write( buf, 0, bufLen** ); zusätzlich den Inhalt ausgeben lassen und alles hat seine Ordnung. Die Datei wird ordnungsgemäß in der Standardausgabe ausgegeben, nur in die Datei wird nichts geschrieben.

Für mich ergibt das keinen Sinn …

EDIT: Ich konnte das Problem jetzt endlich lösen. Jedesmal (!) wenn der Puffer in den Stream geschrieben wird, muss man gleich danach flush() ausführen um Datenverlust zu verhindern. Allerdings ergibt das für mich weiterhin keinen Sinn, ich hatte dieses Problem vorher noch nie!

Dieses Multipart-Downloading ist allerdings echt der Hammer. Bei 8 gleichzeitigen Verbindungen erreiche ich eine Geschwindigkeitserhöhung von über 600 %!!

probier es doch mal mit mehreren Threads :wink:
das sollte noch schneller gehen und vorallem den Code übersichtlicher machen

Apropos Threads: Daran habe ich auch schon gedacht, aber ich muss mir erst ein Konzept zurecht legen WIE ich das dann am besten deichsle. Ich hab einen Intel Core Duo. Machen demnach mehr als zwei Threads überhaupt Sinn? Haben mehrere Threads wirklich einen Nutzen obwohl nicht entsprechend viele CPU-Kerne vorhanden sind?

ja macht sinn :wink:
weil erstmal lastet ein Thread deine CPU ja nicht aus und die Grenze ist ja nicht die CPU sondern die Verbindung
erst bei einer 3 Stelligen Threadanzahl könnte deine CPU ausgelastet sein :smiley:

Okay, gut zu wissen. Ich habe allerdings gelesen dass alles über 20 Threads sinnlos ist.

Ich muss übrigens noch eine Lösung integrieren, falls ein Server auf eine Anfrage zu lange nicht mehr reagiert (ist mir gerade eben passiert).

naja es kommt immer drauf an was die Threads machen, wenn sie rechenintensive Dinge machen, dann können zuviele Threads schädlich sein.

Das Programm ist ziemlich cool, würdest du es veröffentlichen (+ Sourcen :smiley: ) ?

Ich hab’s jetzt mit Threads (1 Thread pro Verbindung) und 16 gleichzeitigen Verbindungen gelöst - das fetzt wie Sau! :smiley:

@Revenant
Ja, ich bin mit meinem VideoLoader (ich weiß, etwas einfallslos, aber was sind schon Namen) ist gerade beim 2. RC. Ich muss den Code noch besser dokumentieren und jedem Java-File den GPL-Passus einimpfen. Wenn das geregelt ist und alle nennenswerten Schwächen ausgebügelt wurden, werd ich das Programm unter der GPL 3 veröffentlichen :slight_smile:

Super :slight_smile: sag auf jeden Fall Bescheid!

Das Archiv enthält den Quelltext, das kompilierte Programm und jeweils ein Startscript für Windows und Unix-Systeme. Bei Verwendung in eigenen Projekten bitte die Lizenzvorschriften beachten!

Für Hinweise zur Verbesserung des Codes bin ich jederzeit offen und dankbar. Natürlich kann auch im Sinne der Lizenz neuer Code beigesteuert werden. :slight_smile:

EDIT: RC5 ist fertig. Er enthält ein hoffentlich nicht illegales Feature: den Episodensucher.
videoloader-0.1rc5.zip

Wenn es jemand benutzen möchte; ich bin über Feedback immer dankbar!