Articles Index
JDC Performance Tips and Firewall Tunneling Techniques
By Tony Squier & Steven Meloan
Based on the JavaSM Developer
ConnectionSM presentation,
Tips
and Tricks for Java Developers given at JavaOne 1998, this article
offers a variety of performance tuning and firewall tunneling techniques
used by the Java Developer Connection (JDC) engineering team. These
techniques are illustrated by source code that you can
download.
Performance--Applet Download Speed
An important factor affecting any Applet's download performance is
the number of times that it has to request data from the server. One
performance-enhancing tip is to package Applet images into a single
class file.
Normally, if an Applet has six image buttons, that translates to six
additional requests sent back to the web server in order to load those
image files. That might not seem like much on an internal network,
but given connections of lesser speed and reliability, those additional
requests can have a significant negative impact. The ultimate goal is
to load the Applet as quickly as possible.
One way of storing images into a class file is to use some type of
ASCII encoding scheme--such as
X-PixMap (XPM).
In this way, rather than maintaining the images as GIF files on the server,
they are instead encoded as Strings and stored in a single class file.
The following code sample uses packages from the JavaCup winner at
JavaOne '96. These classes--XImageSource and XpmParser--provide all
that is necessary to read a standard XPM file (and can be found at
SunSite).
In terms of the initial encoding process, there are many graphics tools
one can use to create XPM files. On Solaris there is ImageTool, along
with a variety of other
GNU image
packages.
Below are the button images used in the following example:
These are the same buttons used in the Java Developer Connection GroupReader
Applet, and can be seen in their encoded String form in the
XPM definition of the images.
It is assumed there are six member variables used in the sample class
MyApplet. These variables are: _reply, _post,
_reload, _catchup, _back10,
_reset, and _faq.
The loading of the images is demonstrated in the following code.
For each image, the Toolkit is used to create an Image from the XPM
Image Source object.
Toolkit kit = Toolkit.getDefaultToolkit();
Image image;
image =
kit.createImage (new XImageSource (_reply));
image =
kit.createImage (new XImageSource (_post));
image =
kit.createImage (new XImageSource (_reload));
image =
kit.createImage (new XImageSource (_catchup));
image =
kit.createImage (new XImageSource (_back10));
image =
kit.createImage (new XImageSource (_reset));
image =
kit.createImage (new XImageSource (_faq));
The above technique reduces network traffic, because each
image definition is part of a single class file. The alternative
technique, using GIF files (see below), requires a request
back to the web server for each image loaded.
Image image;
image = getImage ("reply.gif");
image = getImage ("post.gif");
image = getImage ("reload.gif");
image = getImage ("catchup.gif");
image = getImage ("back10.gif");
image = getImage ("reset.gif");
image = getImage ("faq.gif");
In essence, by using XPM encoded images, the class size becomes larger,
but the number of network requests become fewer. By making the XPM
image definitions part of your Applet class file, you make the image
load process part of the regular loading of the Applet class file--with
no extra classes!
These images can then be used to create buttons or other UI components.
If you use Java Foundation Class (JFC) components, such as JButton, the
Images can be used as follows:
ImageIcon icon = new ImageIcon (
kit.createImage
(new XImageSource (_reply)));
JButton button =
new JButton (icon, "Reply");
Another means of improving Applet download performance is in the use of
JAR (Java ARchive) files. By using the archive tag in the HTML page for
a given Applet, one can specify a list of JAR files that contain all the
Applet's resources. The advantage of using JAR files is that you can put
all of an Applet's related files into a single file for download. The
disadvantage is that if you have multiple JAR files, the ClassLoader is
going to load each JAR file when your Applet is started. Thus, if
you have infrequently used resources, the JAR file containing those files
is going to be downloaded anyway--regardless of whether those resources
will be used.
One way to get around the JAR file problem is to put only those files that
are frequently used into the archive. That way, the truly necessary files
are downloaded in a single request. Removing infrequently used resources
from the JAR files ensures that they will only be downloaded when needed.
Thread Pools--Runtime Performance
The Java Developer Connection Applet servers use Thread Pooling
extensively in order to improve performance. This technique is also
used in the Java Web Server. For a background on multithreading, see
A Simple, Multithreaded
Web Server.
The driving force behind the Thread Pooling is the fact that the Thread
startup process is, in terms of system resources, an expensive operation.
Thus, it's more efficient performance-wise to create a ready supply of
Threads up-front, and then to store that pool of sleeping Threads. These
suspended Threads are then awakened whenever new tasks are ready to be
performed.
The following code samples detail one means of implementing Thread Pooling.
In the Pool's constructor (see Pool.java, and below), the WorkerThreads are
first initialized and started. The call to start() executes WorkerThread's
run() method. Within run(), the wait() call causes the Thread to
suspend--waiting for future work to arrive. The sleeping Thread is then
pushed onto the stack.
Worker worker;
WorkerThread w;
for ( int i = 0; i < _max; i++ ) {
worker = (Worker)_workerClass.newInstance();
w = new WorkerThread ("Worker#"+i, worker);
w.start();
_waiting.push (w);
}
WorkerThread has two methods, wake() and run() (run having been discussed
above). When work comes in, the wake() method is called, which assigns the
data and notifies the sleeping Thread (the one initialized by the Pool)
to resume running. The wake() method's call to notify() causes the blocked
Thread to fall out of its wait state, and the HttpServerWorker's run() method
is then executed. Once the work is done, the WorkerThread is either put back
onto the Stack (assuming the Thread Pool is not full) or simply terminates.
synchronized void wake (Object data) {
_data = data;
notify();
}
synchronized public void run(){
boolean stop = false;
while (!stop){
if ( _data == null ){
try{
wait();
}catch (InterruptedException e){
e.printStackTrace();
continue;
}
}
if ( _data != null ){
_worker.run(_data);
}
_data = null;
stop = !(_push (this));
}
}
Incoming work, at its highest level, is handled by performWork() (see
Pool.java). Here, as work comes in, an existing WorkerThread is popped
off of the Stack (or a new one is created if the Pool happens to be empty).
The sleeping WorkerThread is then activated by a call to its wake() method.
public void performWork (Object data)
throws InstantiationException{
WorkerThread w = null;
synchronized (_waiting){
if ( _waiting.empty() ){
try{
w = new WorkerThread ("additional worker",
(Worker)_workerClass.newInstance());
w.start();
}catch (Exception e){
throw new InstantiationException (
"Problem creating instance of
Worker.class: "
+ e.getMessage());
}
}else{
w = (WorkerThread)_waiting.pop();
}
}
w.wake (data);
}
To witness the Pool class code in action, see the discussion on the
HttpServer class in the next section on firewall tunneling.
try{8888
_pool = new Pool (poolSize,
HttpServerWorker.class);
}catch (Exception e){
e.printStackTrace();
throw new InternalError (e.getMessage());
}
In the HttpServer's constructor (above) a new Pool instance is created
to service HttpServerWorker instances. These instances are created and
stored as part of the WorkerThread data. When a WorkerThread is activated
(via a call to wake()), the HttpServerWorker instance is invoked using
it's run() method (see HttpServerWorker.java).
The following code appears in the main service routine (run()) of
the HttpServer (see HttpServer.java). Each time a request comes in,
the data is initialized and the Thread starts it's work. Note: If the
process of creating a new Hashtable for each WorkerThread presents too
much overhead, simply modify the code so as not to use the Worker
abstraction.
try{
Socket s = _serverSocket.accept();
Hashtable data = new Hashtable();
data.put ("Socket", s);
data.put ("HttpServer", this);
_pool.performWork (data);
}catch (Exception e){
e.printStackTrace();
}88
Firewall Tunneling
Firewall tunneling techniques were first presented on the Java Developer
Connection as part of a discussion of the Chat Applet (see JDC article
Burrowing Through
Firewalls). The JDC engineering team has since expanded upon these
techniques, and addressed other issues, in its new firewall tunneling
framework. The following discussion incorporates many of the classes
covered in the performance sections above.
The challenge addressed by firewall tunneling is how to offer those
users accessing the Internet via an HTTP Proxy the same level of
"interaction" available to those with direct connections. Using the
following techniques, the JDC achieves "multiplexed" communication
for its Applets, letting them send and receive messages through the
firewall.
The following sections detail the HTTP tunneling technique.
Client-to-Server Communication
Sending a simple message from the client to the server is achieved using the
URLConnection class.
The client first creates a new URLConnection to send the request to the
server (see HttpClient.java). The server responds and the connection is
then closed. In general, this direction of communication presents very
few problems, making the code rather simple.
synchronized public byte[] send (
byte data[]) throws IOException
{
byte buffer[];
// Establish a connection
URL url = new URL (_url);
URLConnection connection = url.openConnection();
connection.setUseCaches (false);
connection.setDoOutput(true);
// Write out the data
DataOutputStream dataOut =
new DataOutputStream (
new BufferedOutputStream (
connection.getOutputStream()));
dataOut.writeInt (HttpClient.DATA);
dataOut.writeInt (data.length);
dataOut.write (data);
dataOut.flush();
dataOut.close();
int length;
DataInputStream input = new DataInputStream (
new BufferedInputStream (
connection.getInputStream()));
int type = input.readInt();
if ( type == HttpClient.DATA ) {
length = input.readInt();
buffer = new byte[length];
input.readFully (buffer);
} else {
buffer = null;
throw new IOException (
"Unknown Response Type");
}
input.close();
return buffer;
}
The important things to note in the code are:
-
The URLConnection must be configured (as specified in
the API) so as not to use caches, and to allow posting -
setUseCaches(false) and setDoOutput(true).
-
For performance reasons, both the URLConnection's
Input/Output streams are accessed using BufferedIO streams.
-
The data reads and writes include extra information about
the length and type of data being read or written.
On the server side, the code is quite similar (see HttpServerWorker.java).
public void run(Object data) {
Socket socket =
(Socket)((Hashtable)data).get ("Socket");
HttpServer server =
(HttpServer)((Hashtable)data).get("HttpServer");
try {
DataInputStream input = new DataInputStream (new
BufferedInputStream(socket.getInputStream()));
String line = input.readLine();
if (line.toUpperCase().startsWith ("POST") ){
for ( ; (line=input.readLine()).length() > 0; )
;
int type = input.readInt()
switch (type){
case HttpClient.DATA :{
int length = input.readInt();
byte buffer[] = new byte[length];
input.readFully (buffer);
ByteArrayOutputStream dataOut
= new ByteArrayOutputStream();
server.notifyListener (new
ByteArrayInputStream
(buffer), dataOut);
DataOutputStream output = new
DataOutputStream
(new BufferedOutputStream(
socket.getOutputStream()));
output.writeBytes (_httpResponse);
output.writeInt (HttpClient.DATA);
output.writeInt (dataOut.toByteArray().length);
output.write (dataOut.toByteArray());
output.flush();
input.close();
output.close();
socket.close();
break;
}
default :{
System.err.println ("Invalid type:
" + type);
}
}
} else {
System.err.println ("Invalid HTTP request: " +
line);
}
} catch (IOException e) {
e.printStackTrace();
try {
socket.close();
} catch (Exception e) {
}
}
}
The HttpWorkerServer class implements Worker from the Thread Pooling
package. The data object passed into the run method includes the Socket
and other information. The important things to note in the code are:
-
The request must be handled as a real HTTP request--checking
for proper HTTP headers.
-
As in the client-side code, the Socket's IO streams use
BufferedIO streams.
-
When sending the response, the proper HTTP headers must be
included.
The client-to-server communication technique is fairly straightforward.
However, when setting up a pending connection between the server and
client (to allow the server to initiate messages), the code gets a bit
more complicated.
Server-to-Client Communication
Allowing a server to send messages to the client requires that both
sides of the connection agree upon a handshaking protocol. One of
the easiest ways to set up a connection is to have the client create a
URLConnection, which the server keeps open until it is ready to
send data.
Setting up the client side is fairly straightforward, as shown in
the following code sample (see HttpClient.java).
connection = _sendPending();
DataOutputStream output = new
DataOutputStream (connection.getOutputStream());
output.writeInt (PENDING);
output.flush();
output.close();
output = null;
int code =
((HttpURLConnection)connection).getResponseCode();
switch (code) {
case 200: // HTTP_OK{
DataInputStream input = new
DataInputStream
(connection.getInputStream());
length = input.readInt();
byte buffer[] = new
byte[length];
if ( length >= 0 ){
input.readFully (buffer);
_listener.service ( new
ByteArrayInputStream (buffer));
} else {
System.err.println("Invalid length:
" + length);
}
buffer = null;
input.close();
input = null;
}
case 504:
//HTTP_GATEWAY_TIMEOUT{
connection = _sendPending();
break;
}
default: //OTHER HTTP_SERVER
ERRORS{
System.out.println ("Invalid
code:"+code);
}
}
Several issues must be addressed when using the pending-connection
approach. The client's only thread, as shown in the run() method, is
responsible for maintaining the pending connection with the server.
For resource reasons, this thread isn't started until a listener is
first added to the HttpClient (see TestClient.java).
Once started, the pending request is sent using the _sendPending()
method. A unique value (PENDING) is sent so the server will know
that the message is a pending one. It is then essential that the
client respond appropriately to the HTTP response codes which can
come back from the many network sources (such as HTTP Proxies).
Note that the above simplified code anticipates only two possible
response codes: HTTP_GATEWAY_TIMEOUT and HTTP_OK. These two codes
are the most important because they determine whether the client
has received viable data, or whether it needs to send another pending
connection request. In the case of HTTP_OK, the client safely reads
the data from the HttpServer. Given an HTTP_GATEWAY_TIMEOUT code,
however, the client must assume that the pending connection has
been closed by one of the HTTP Proxies in the connection, and that it
needs to resend the pending connection to the HttpServer. All other
codes in the above sample, by default, cause the pending thread to
stop.
On the server side, the Socket from the pending connection must
be maintained for each client (see HttpServerWorker.java). One way of
keeping track of clients is to use a Vector:
case HttpClient.PENDING :{
server.addClient (socket);
break;
}
The above code provides an additional case for the pending connection,
and calls the HttpServer's addClient() method (see HttpServe.java) to
store the Socket reference.
synchronized void addClient (Socket s){
_clients.addElement(s);
}
To send a message back to the client, the server simply enumerates
through its list of Sockets and writes out the data.
In this simplified version of the code, there is very little error
handling--much more can and should be added. Another important feature
that should ideally be built into the server code is a means by which
to make it tolerant of slow clients. If the pending connection isn't
immediately available, it's not a good idea to simply ignore the
client--far better to wait a bit and see whether the "slow" client
is simply taking its time in sending the pending connection.
synchronized public void send (byte data[]){
Enumeration elements = _clients.elements();
while ( elements.hasMoreElements() ){
Socket s = (Socket)elements.nextElement();
try{
DataOutputStream output = new
DataOutputStream(new BufferedOutputStream
(s.getOutputStream()));
int length;
writeResponse (output);
output.writeInt (data.length);
output.write (data);
output.flush();
output.close();
}catch (IOException e){
e.printStackTrace();
}
finally{
try{
s.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
_clients.removeAllElements();
}
Tony Squier is a JDC tools engineer. He developed the JDC
registration-management code. He invites any feedback you have
about the article or the code, such as enhancements or potential
bugs. If you have comments please send them to:
Tony Squier
Steven Meloan is a writer, journalist, and former software developer.
His work has appeared in Wired, Rolling Stone, BUZZ, San Francisco
Examiner, ZDTV's "The Site," and American Cybercast's
"The Pyramid."
Reader Feedback
Tell us what you think of this article.