Tuesday, June 12, 2007

Java Web Start connection problem

Anyone who has deployed any large application using Java Web Start 1.5 or 1.6 over a slow WAN or through some kind of SSL proxy has noticed the extreme number of connections that can be initiated during application startup. The first deploy of the application works as expected and all the JAR files is downloaded. But the sencond time the user clicks to start the application, Java Web Start will flood the network with connections to the server.

Investigation the problem revelas an interesting piece of code in the class com.sun.javaw.LaunchDownload and the method updateCheck, this function is called once for every JAR file in the JNLP description file. The code does the update check by starting a new thread and doing a HTTP request to the web server to check if the JAR file has been updated.
This means that if we have a JNLP file with 200 JAR files, the code will start 200 threads, creating 200 individual connections to the server!

The solution can be found in the same function as well, as the first lines of the function reads:

// no update check for versioned resource
if (version != null) return;


This means that if we put version attributes on the JAR files we will not invoke the badly written code. To add the version attribute is simple, just add it to the JAR element of the JNLP file:

<jar href="/application/foo.jar" version="”1.0” />


When Java Web Start finds this attribute on a JAR element in the JNLP file it will send this version string along with the GET request to the server. Like:

GET /application/foo.jar?version-id=1.0

To handle this on the server we have two options, if we are running a J2EE environment the JDK contains a JNLP servlet that can respond to this request and also return the required x-java-jnlp-version-id custom header, or we could implement the same functionality in some other kind of server side language.

I choose to implement the JAR server in PHP/Apache since we have noticed that the J2EE container we are using is not very good at serving large amounts of data.

To have Apache invoke my script for every JAR file requested from the server, I added the following lines to the httpd.conf file.

AddType application/java-archive .jar

Action application/java-archive /cgi-bin/jar_send.php

The script is very simple; we have chosen to store the versioned JAR archives
as /application/foo_x_y.jar for version x.y. This makes the script very simple to implement, and make deploying very simple as well. Another strategy could be to store a complete version of the application under /x.y/application/foo.jar and have all JAR archives in the JNLP file reference the same version.

Here is the code for the jar_send.php script:


<?

// Make sure we have got a version-id argument
if (isset($_GET["version-id"])) {
$version = $_GET["version-id"];
} else {
$version = null;
}

// Retreive the requested file
$file = $_SERVER["PATH_TRANSLATED"];

// If not version is requested, or version is 1.0, send foo.jar
if ($version == null || $version == "1.0") {
$path = $file;
} else {
// If version 1.1 is requested, send foo_1_1.jar
$x = strrpos($file,".jar");
$path = substr($file,0,$x);
$path .= "_";
$path .= str_replace(".","_",$version);
$path .= ".jar";
}

// Make sure the file exists
if (!is_file($path)) {
header("HTTP/1.0 404 NOT FOUND");
print $path;
die;
}

// Open the file
$f = fopen($path, 'rb');
if ($f == null) {
header("HTTP/1.0 404 NOT FOUND");
print $path;
die;
}

// Send the JNLP custom header
header("x-java-jnlp-version-id: $version");

// Inform Apache about how much data we are going to send
header("Content-Length: ".(string)(filesize($path)));

// Send the data, 8K blocks
while(!feof($f) &&(connection_status()==0)) {
print(fread($f, 1024*8));
flush();
}

// Close the file
fclose($f);

?>

No comments: