|
Based on the Java Developer Connection 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 SpeedAn 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.
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.
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:
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 PerformanceThe 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.
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.
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.
To witness the Pool class code in action, see the discussion on the HttpServer class in the next section on firewall tunneling.
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.
Firewall TunnelingFirewall tunneling techniques were first presented on the Java Developer Connection as part of a discussion of the Chat Applet. 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 CommunicationSending 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.
The important things to note in the code are:
On the server side, the code is quite similar (see HttpServerWorker.java).
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 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 CommunicationAllowing 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).
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.
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." | ||||||||||||||||||||
Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.
|
| ||||||||||||