JAVA性能技巧和防火墙隧道技术
基于JavaSM Developer ConnectionSM 的描述,依照1998年JavaOne大会上关于Java开发技巧和窍门(Tips
and Tricks for Java Developers)的讲座内容,本文给出了Java开发者组织(Java
Developer Connection--JDC)工程小组所使用的大量性能调整和防火墙隧道技术。这些技术通过原码配合图例进行说明,原码可以下载。
性能——Applet下载速度
影响Applet下载性能的一个重要因素是它向服务器发出请求数据的次数。一个增强性能的技巧是将Applet图像打包进一个单一的类文件中。
通常,如果Applet拥有六个图形按钮,就需要额外发送六个请求到WEB服务器以便下载这些图像文件。在内部网上,这看起来并没有什么不便,但是,考虑速度和可靠性都较低的连接,这些额外的请求可能具有严重的不利影响。因为最终目的是尽快下载Applet。
将图像存储在一个类文件中的方法之一是使用ASCII编码的某些方案——例如X-PixMap(XPM)。在这个方案中,不是以GIF文件格式将图像保存在服务器上,而是将图像文件编码为字符串,并且存储在一个单一的类文件中。
下面的代码示例中使用了JavaOne'96中JavaCup获胜者的软件包。这些类——XImageSource和XpmParser——提供了需要用来读取标准XPM文件的所有方法(可以在SunSite中找到)。作为初始化编码过程,可以用多种图形工具来创建XPM文件。在Solaris上是ImageTool,以及许多其他的GNU图像包。
在接下来的例子中将用到下面的按钮图像:
在Java Developer Connection的GroupReader Applet中也有同样的按钮,并且可以看到以XPM方式对图像进行编码后的字符串形式。
假定在示例类MyApplet中使用了六个成员变量。这些变量是:_reply, _post, _reload, _catchup, _back10,
_reset,和_faq。
以下的代码实现了图像的载入。对于每幅图像,Toolkit被用于从XPM图像源对象中创建图像。
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));
以上的技术减少了网络流量,因为每幅图像的定义都是单一类文件的一部分。另一种技术是使用GIF文件(参看下面),它需要为每幅载入的图像发送一个请求到Web服务器。
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");
事实上,采用XPM编码的图像,使类文件在尺寸上增大了,但是网络请求的数目减少了。通过使XPM图像被定义成为Applet类文件的一部分,就使得图像载入过程成为Applet类文件常规载入的一部分,而不需要额外的类!
这样,这些图像就可以被用来创建按钮或者其他的UI部件。如果采用Java基本类(Java Foundation Class--JFC)的部件,如JButton,图像可以使用如下:
ImageIcon icon = new ImageIcon (
kit.createImage (new XImageSource (_reply)));
JButton button = new JButton (icon, "Reply");
提高Applet下载性能的另一种方法是使用JAR(Java ARchive)文件。通过在HTML页面上为Applet使用这种文档标志,就可以指定JAR文件列表来包含所有的Applet资源。使用JAR文件的好处在于,可以将所有与Applet相关的文件放入一个文件中用于下载。不利的地方是,如果拥有多个JAR文件,类载入器(ClassLoader)在Applet启动时,将载入每个JAR文件。这样,当不是很频繁地使用资源时,包含这些文件的JAR文件无论如何都会被下载——不管这些资源是否被使用。
解决这个JAR文件问题的方法是只将那些频繁使用的文件放入文档(JAR文件)。这样,真正需要的文件将在一个请求中下载。将非频繁使用的资源从JAR文件中卸出就保证了它们只在需要的时候被下载。
线程池(Thread Pools)——运行时性能
JDC的Applet(Java Developer Connection Applet)服务器广泛地使用线程池以提高性能。这项技术也被用在Java
Web 服务器(Java Web Server)上。关于多线程的背景知识,请参看相关的文章。
线程池后面的驱动力量在于,从系统资源的角度来看,线程的启动过程是个大开销的工作。这样,创建即将进入前台的就绪线程的一个储备,然后将睡眠线程用线程池进行存储是更有效的性能方案。
下面的代码详细说明了实现线程池的一种方法。在线程池构造者中(参看Pool.java及以下部分),WorkerThread第一个被初始化并且启动。对start()的调用执行了WorkerThread的run()方法。在run()中,wait()调用使得线程挂起——等待下一步操作到来。然后,睡眠线程被推入堆栈。
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拥有两个方法,wake()和run()(run已经在上面进行了讨论)。当操作到来时,调用wake()方法,该方法为数据赋值并且通知睡眠线程(由线程池初始化的那个)重新运行。wake()方法对notify()的调用导致被阻塞的线程脱离等待状态,然后执行HttpServerWorker的run()方法。当这项操作完成后,WorkerThread或者被放回堆栈(假设线程池未满),或者简单地终止。
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));
}
}
在最高层次上,接下来的工作由performWork()处理(参看Pool.java)。这里,当操作到来时,将从堆栈中弹出一个现存的WorkerThread(如果线程池恰巧为空,则创建一个新的)。然后,睡眠的WorkerThread被一个使用其wake()方法的调用所激活。
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);
}
为了表明线程池类代码在运转,参看下一节中关于HttpServer类的防火墙隧道技术的讨论。
try{8888
_pool = new Pool (poolSize, HttpServerWorker.class);
}catch (Exception e){
e.printStackTrace();
throw new InternalError (e.getMessage());
}
在上面的HttpServer构造者中,一个新线程池实例被创建来用于维护HttpServerWorker实例。这些实例是作为WorkerThread数据的一部分被创建和存储的。当激活WorkerThread(通过对wake()的调用)的时候,就通过HttpServerWorker的run()方法调用了后者的一个实例(参看HttpServerWorker.java)。
下面的代码出现在HttpServer的主服务例程(run())中(参看HttpServer.java)。每次当一个请求到来时,线程就初始化其数据并且开始其操作。注意:如果为每个WorkerThread创建新哈希表的过程使用了过多的系统开销,就需要修改这段代码而不使用Worker抽象类。
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
防火墙隧道
防火墙隧道技术最先是在JDC中,作为聊天Applet讨论的一部分出现的(参看JDC文章Burrowing
Through Firewalls)。JDC工程小组在自己新的防火墙隧道框架中扩展了这些技术,并且解决了其他相应的问题。接下来的讨论涉及了上面性能讨论小节中的许多类。
防火墙隧道所要解决的挑战是,如何赋予通过HTTP代理访问因特网的用户与直接连接的用户相同等级的可用的交互性。采用下面的技术,JDC为Applet完成了复用通讯,即让它们通过防火墙发送和接收信息。
下面的小节详细描述了HTTP隧道技术。
客户端到服务器的通讯
从客户端向服务器发送一个简单的信息是通过使用URLConnection类实现的。
首先,客户端创建一个新的URLConnection,以向服务端发送请求(参看HttpClient.java)。服务端响应,然后关闭连接。通常,这个方向上的通讯很少出现问题,就使得代码相应比较简单。
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;
}
代码中应该注意的要点是:
URLConnection必须被设置(如API中所描述的),以使其不使用高速缓存,并且允许置入(posting)——setUseCaches(false)和setDoOutput(true)。
由于性能的关系,URLConnection的输入输出流采用BufferedIO流进行访问。
读取和写入的数据包含所读写数据的类型和长度等额外信息。
在服务端,代码大致相同(参看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) {
}
}
}
HttpWorderServer类从线程池软件包中实现了Worker。传送到run方法的数据对象包含套接字对象和其他信息。代码中值得注意的要点是:
每个请求必须作为真实的HTTP请求处理——检查正确的HTTP头。
类似于客户端的代码,套接字的IO流采用BufferedIO流。
当发送响应时,必须包含正确的HTTP头。
客户端到服务的通讯比较简单。然而,当在客户端和服务器之间设置一个挂起连接(以允许服务端发起信息连接),代码就变得有些复杂。
服务器到客户端的通讯
允许服务器向客户端发送信息就要求连接的双方在握手协议上达成一致。建立连接的一种最容易的方法是由客户端创建一个URLConnection,由服务器保持其连接状态直到其准备好发送数据。
对客户端的设置相当的简单,正如以下的示例代码(参看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;
break;
}
case 504: { // HTTP_GATEWAY_TIMEOUT
connection = _sendPending();
break;
}
default: { // OTHER HTTP_SERVER ERRORS
System.out.println ("Invalid code:"
+ code);
}
}
当采用挂起连接的方法时,有几个问题必须解决。如同在run()方法中所显示的,客户端线程负责维护与服务器端挂起的连接。出于资源的考虑,该线程直到一个监听者首次被加入到HttpClient时才启动(参看TestClient.java)。
一旦启动,挂起的请求就通过_sendPending方法发送出去。一个唯一值(PENDING)也被送出,这样服务器就将知到这条消息是挂起消息。接下来的要点是客户端恰当地对HTTP的响应代码作出反应,这些响应代码可能来自多种网络源(例如HTTP代理)。
请注意上面简化的代码只等待两个可能的响应代码:HTTP_GATEWAY_TIMEOUT和HTTP_OK。这两个代码是最重要的,因为它们确定了客户端是否已经收到可行的数据,或者需要发送另一个挂起连接请求。在HTTP_OK的情形下,客户端可以很安全地从HttpServer读取数据。然而,如果是HTTP_GATEWAY_TIMEOUT代码,客户端必须假定挂起连接已经被连接路由中的一个HTTP代理关闭了,因此需要重新发送挂起连接到Http服务器。缺省情况下,在上面示例中的所有其他代码都将致使挂起线程停止。
在服务器端,来自于挂起连接的套接字对象必须为每个客户端保留(参看HttpServerWorker.java)。一种跟踪这些客户端的方法是使用Vector对象:
case HttpClient.PENDING :{
server.addClient (socket);
break;
}
上面的代码为挂起连接提供了额外的例子,并且调用HttpServer的addClient()方法(参看HttpServe.java)来存储套接字对象引用。
synchronized void addClient (Socket s){
_clients.addElement(s);
}
为了发送信息回客户端,服务器只要简单地通过对套接字列表进行枚举并且送出数据。
在该代码的简化版本中,基本上没有出错处理——可以而且应该增加更多的处理。应该加入服务端器代码的另一个重要特性是对慢客户端的容错的方法。如果挂起连接不是立即可用的,简单地忽略客户端不是一个好方法——更好的是等待一会儿,看看是否慢客户端正把时间花在发送挂起连接上。
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是一位JDC工具工程师。他开发了JDC注册管理代码。他欢迎读者关于这篇文章或代码的任何反馈,例如更好的代码或者潜在的bug。如果有这方面的反馈,请发送到:tony.squier@eng.sun.com。
Steven Meloan是位作家,新闻记者,以前是软件开发人员。他的作品刊登在Wired, Rolling Stone, BUZZ, San
Francisco Examiner, ZDTV的"The Site,"和American Cybercast的"The Pyramid."。