Library Zone Articles
External Articles
Byte Size

Discovery Zone Catalogue
Diary
Links
Bookstore
Interactive Zone Ask the Gurus
Discussion Groups
Newsletters
Feedback
Etc Cartoons
Humour
COMpetition
Advertising ASP Web Ring ASP Web Ring

The Developer's Resource & Community Site
COM XML ASP Java & Misc. NEW: VS.NET
International This Week Forums Author Central Find a Job
Buy the Book: Java Network Programming, 2nd Edition

Java Network Programming, 2nd Edition: Sockets for Servers

Reproduced with kind permision of O'Reilly & Associates: www.oreilly.com

Contents:
Sockets for Servers

In this chapter:
The ServerSocket Class
Some Useful Servers

The last chapter discussed sockets from the standpoint of clients: programs that open a Socket to a server that's listening for connections. However, client sockets themselves aren't enough; clients aren't much use unless they can talk to a server, and if you think about it, the sockets we discussed in the last chapter aren't sufficient for writing servers. To create a Socket, you need to know the Internet host to which you want to connect. When you're writing a server, you don't know in advance who will contact you, and even if you did, you wouldn't know when that host wanted to contact you. In other words, servers are like receptionists who sit by the phone and wait for incoming calls. They don't know who will call or when, only that when the phone rings, they have to pick it up and talk to whoever is there. We can't program that behavior with the Socket class alone. Granted, there's no reason that clients written in Java have to talk to Java servers--in fact, a client doesn't care what language the server was written in or what platform it runs on. However, if Java didn't let us write servers, there would be a glaring hole in its capabilities.

Fortunately, there's no such hole. Java provides a ServerSocket class to allow programmers to write servers. Basically, a server socket's job is to sit by the phone and wait for incoming calls. More technically, a ServerSocket runs on the server and listens for incoming TCP connections. Each ServerSocket listens on a particular port on the server machine. When a client Socket on a remote host attempts to connect to that port, the server wakes up, negotiates the connection between the client and the server, and opens a regular Socket between the two hosts. In other words, server sockets wait for connections while client sockets initiate connections. Once the server Socket has set up the connection, the server uses a regular Socket object to send data to the client. Data always travels over the regular socket.

The ServerSocket Class

The ServerSocket class contains everything you need to write servers in Java. It has constructors that create new ServerSocket objects, methods that listen for connections on a specified port, and methods that return a Socket object when a connection is made so that you can send and receive data. In addition, it has methods to set various options and the usual miscellaneous methods such as toString( ).

The basic life cycle of a server is:

  1. A new ServerSocket is created on a particular port using a ServerSocket( ) constructor.
  2. The ServerSocket listens for incoming connection attempts on that port using its accept( ) method. accept( ) blocks until a client attempts to make a connection, at which point accept( ) returns a Socket object connecting the client and the server.
  3. Depending on the type of server, either the Socket's getInputStream( ) method, getOutputStream( ) method, or both are called to get input and output streams that communicate with the client.
  4. The server and the client interact according to an agreed-upon protocol until it is time to close the connection.
  5. The server, the client, or both close the connection.
  6. The server returns to step 2 and waits for the next connection.

If step 4 is likely to take a long or indefinite amount of time, traditional Unix servers such as wu-ftpd create a new process to handle each connection so that multiple clients can be serviced at the same time. Java programs should spawn a thread to interact with the client so that the server can be ready to process the next connection sooner. A thread places a far smaller load on the server than a complete child process. In fact, the overhead of forking too many processes is why the typical Unix FTP server can't handle more than roughly 400 connections without slowing to a crawl. On the other hand, if the protocol is simple and quick and allows the server to close the connection when it's through, then it will be more efficient for the server to process the client request immediately without spawning a thread.

The operating system stores incoming connection requests addressed to a particular port in a first-in, first-out queue. The default length of the queue is normally 50, though this can vary from operating system to operating system. Some operating systems (though not Solaris) have a maximum queue length, typically five. On these systems, the queue length will be the largest possible value less than or equal to 50. After the queue fills to capacity with unprocessed connections, the host refuses additional connections on that port until slots in the queue open up. Many (though not all) clients will try to make a connection multiple times if their initial attempt is refused. Managing incoming connections and the queue is a service provided by the operating system; your program does not need to worry about it. Several ServerSocket constructors allow you to change the length of the queue if its default length isn't large enough; however, you won't be able to increase the queue beyond the maximum size that the operating system supports:

The Constructors

There are three public ServerSocket constructors:

public ServerSocket(int port) throws IOException, BindException
public ServerSocket(int port, int queueLength) 
 throws IOException, BindException
public ServerSocket(int port, int queueLength, InetAddress bindAddress) 
 throws IOException

These constructors let you specify the port, the length of the queue used to hold incoming connection requests, and the local network interface to bind to. They pretty much all do the same thing, though some use default values for the queue length and the address to bind to. Let's explore these in order.

public ServerSocket(int port) throws IOException, BindException

This constructor creates a server Socket on the port specified by the argument. If you pass 0 for the port number, the system selects an available port for you. A port chosen for you by the system is sometimes called an anonymous port since you don't know its number. For servers, anonymous ports aren't very useful because clients need to know in advance which port to connect to; however, there are a few situations (which we will discuss later) in which an anonymous port might be useful.

For example, to create a server Socket that would be used by an HTTP server on port 80, you would write:


try {
  ServerSocket httpd = new ServerSocket(80);
}
catch (IOException e) {
  System.err.println(e);
}

The constructor throws an IOException (specifically, a BindException) if the Socket cannot be created and bound to the requested port. An IOException when creating a ServerSocket almost always means one of two things. Either another server socket, possibly from a completely different program, is already using the requested port, or you're trying to connect to a port from 1 to 1023 on Unix without root (superuser) privileges.

You can use this constructor to write a variation on the PortScanner programs of the previous chapter. Example 11-1 checks for ports on the local machine by attempting to create ServerSocket objects on them and seeing on which ports that fails. If you're using Unix and are not running as root, this program works only for ports 1,024 and above.

Example 11-1: Look for Local Ports

import java.net.*;
import java.io.*;
 
public class LocalPortScanner {
 
  public static void main(String[] args) {
    
    for (int port = 1; port <= 65535; port++) {
 
      try {
        // the next line will fail and drop into the catch block if
        // there is already a server running on the port
        ServerSocket server = new ServerSocket(port);
      }
      catch (IOException e) {
        System.out.println("There is a server on port " + port + ".");
      } // end try
 
    } // end for
 
  }
  
}

Here's the output I got when running LocalPortScanner on my NT workstation:

D:\JAVA\JNP2\examples\11> java LocalPortScanner
There is a server on port 135.
There is a server on port 1025.
There is a server on port 1026.
There is a server on port 1027.
There is a server on port 1028.

public ServerSocket(int port, int queueLength) throws IOException, BindException

This constructor creates a ServerSocket on the specified port with a queue length of our choosing. If the machine has multiple network interfaces or IP addresses, then it listens on this port on all those interfaces and IP addresses. The queueLength argument sets the length of the queue for incoming connection requests--that is, how many incoming connections can be stored at one time before the host starts refusing connections. Some operating systems have a maximum queue length, typically five. If you try to expand the queue past that maximum number, the maximum queue length is used instead. If you pass 0 for the port number, the system selects an available port.

For example, to create a server Socket on port 5,776 that would hold up to 100 incoming connection requests in the queue, you would write:

try {
  ServerSocket httpd = new ServerSocket(5776, 100);
}
catch (IOException e) {
  System.err.println(e);
}

The constructor throws an IOException (specifically, a BindException) if the Socket cannot be created and bound to the requested port. An IOException when creating a ServerSocket almost always means one of two things. Either the specified port is already in use, or you do not have root privileges on Unix and you're trying to connect to a port from 1 to 1,023.

public ServerSocket(int port, int queueLength,
InetAddress bindAddress) throws BindException, IOException

This constructor, which is available only in Java 1.1 and later, creates a ServerSocket on the specified port with the specified queue length. This ServerSocket binds only to the specified local IP address. This constructor is useful for servers that run on systems with several IP addresses (a common practice at web server farms) because it allows you to choose the address to which you'll listen. That is, this ServerSocket listens only for incoming connections on the specified address; it won't listen for connections that come in through the host's other addresses. The other constructors bind to all local IP addresses by default.

For example, metalab.unc.edu is a particular SPARCstation in North Carolina. It's connected to the Internet with the IP address 152.2.254.81. The same SPARCstation is also called www.gigabit-ethernet.org, but with a different IP address (152.2.254.82). To create a server Socket that listens on port 5,776 of metalab.unc.edu but not on port 5,776 of www.gigabit-ethernet.org, you would write:

try {
  ServerSocket httpd = new ServerSocket(5776, 10, 
   InetAddress.getHostByName("metalab.unc.edu"));
}
catch (IOException e) {
  System.err.println(e);
}

The constructor throws an IOException (again, really a BindException) if the Socket cannot be created and bound to the requested port. A BindException when creating a ServerSocket almost always means one of two things. Either the specified port is already in use, or you do not have root privileges on Unix and you're trying to connect to a port from 1 to 1,023.

Accepting and Closing Connections

A ServerSocket generally operates in a loop that repeatedly accepts connections. Each pass through the loop invokes the accept( ) method. This returns a Socket object representing the connection between the remote client and the local server. Interaction with the client takes place through this Socket object. When the transaction is finished, the server should invoke the Socket object's qclose( ) method and get ready to process the next incoming connection. However, when the server needs to shut down and not process any further incoming connections, you should invoke the ServerSocket object's qclose( ) method.

public Socket accept( ) throws IOException

When server setup is done and you're ready to accept a connection, call the ServerSocket's accept( ) method. This method "blocks": it stops the flow of execution and waits until a client connects. When a client does connect, the accept( ) method returns a Socket object. You use the streams returned by this Socket's getInputStream( ) and getOutputStream( ) methods to communicate with the client. For example:

ServerSocket server = new ServerSocket(5776);
while (true) {
  Socket connection = server.accept(  );
  OutputStreamWriter out 
   = new OutputStreamWriter(connection.getOutputStream(  ));
  out.write("You've connected to this server. Bye-bye now.\r\n");        
  connection.close(  );
}

If you don't want your program to halt while it waits for a connection, put the call to accept( ) in a separate thread.

When you add exception handling, the code becomes somewhat more convoluted. It's important to distinguish between exceptions thrown by the ServerSocket, which should probably shut down the server and log an error message, and exceptions thrown by a Socket, which should just close that active connection. Exceptions thrown by the accept( ) method are an intermediate case that can go either way. To do this, you'll need to nest your try blocks. Finally, most servers will want to make sure that all sockets they accept are closed when they're finished. Even if the protocol specifies that clients are responsible for closing connections, clients do not always strictly adhere to the protocol. The call to qclose( ) also has to be wrapped in a try block that catches an IOException. However, if you do catch an IOException when closing the socket, ignore it. It just means that the client closed the Socket before the server could. Here's a slightly more realistic example:

try {
  ServerSocket server = new ServerSocket(5776);
  while (true) {
    Socket connection = server.accept(  );
    try {
      OutputStreamWriter out 
       = new OutputStreamWriter(connection.getOutputStream(  ));
      out.write("You've connected to this server. Bye-bye now.\r\n");        
      connection.close(  );
   }
   catch (IOException e) {
     // This tends to be a transitory error for this one connection;
     // e.g. the client broke the connection early. Consequently,
     // we don't want to break the loop or print an error message.
     // However, you might choose to log this exception in an error log.
   }
   finally {
     // Most servers will want to guarantee that sockets are closed
     // when complete. 
     try {
       if (connection != null) connection.close(  );
     }
     catch (IOException e) {}
   }
}
catch (IOException e) {
  System.err.println(e);
}

Example 11-2 implements a simple daytime server, as per RFC 867. Since this server just sends a single line of text in response to each connection, it processes each connection immediately. More complex servers should spawn a thread to handle each request. In this case, the overhead of spawning a thread would be greater than the time needed to process the request.

NOTE: If you run this program on a Unix box, you need to run it as root in order to connect to port 13. If you don't want to or can't run it as root, change the port number to something above 1024, say 1313.

Example 11-2: A Daytime Server

import java.net.*;
import java.io.*;
import java.util.Date;
  
public class DaytimeServer {
  
  DEFAULT_PORT DEFAULT_PORT = 13;
 
  public static void main(String[] args) {
 
   int port = DEFAULT_PORT;     
   if (args.length > 0) {
     try {
        port = Integer.parseInt(args[0]);
        if (port < 0 || port >= 65536) {
          System.out.println("Port must between 0 and 65535");
          return;      
        }
     }   
     catch (NumberFormatException e) {
       // use default port
     }  
 
   }     
 
   try {
    
     ServerSocket server = new ServerSocket(port);
      
     Socket connection = null;
     while (true) {
        
       try {
         connection = server.accept(  );
         OutputStreamWriter out 
          = new OutputStreamWriter(connection.getOutputStream(  ));
         Date now = new Date(  );
         out.write(now.toString(  ) +"\r\n");
         out.flush(  );      
         connection.close(  );
       }
       catch (IOException e) {}
       finally {
         try {
           if (connection != null) connection.close(  );
         }
         catch (IOException e) {}          
       }
         
     }  // end while
       
   }  // end try
   catch (IOException e) {
     System.err.println(e);
   } // end catch
 
  } // end main
 
} // end DaytimeServer

Example 11-2 is straightforward. The first three lines import the usual packages, java.io and java.net, as well as java.util.Date so we can get the time. There is a single DEFAULT_PORT field (i.e., a constant) in the class DEFAULT_PORT, which is set to the well-known port for a daytime server (port 13). The class has a single method, main( ), which does all the work. If the port is specified on the command-line, then it's read from args[0]. Otherwise, the default port is used.

The outer try block traps any IOExceptions that may arise while the ServerSocket server is constructed on the daytime port or when it accepts connections. The inner try block watches for exceptions thrown while the connections are accepted and processed. The accept( ) method is called within an infinite loop to watch for new connections; like many servers, this program never terminates but continues listening until an exception is thrown or you stop it manually.[1]

When a client makes a connection, accept( ) returns a Socket, which is stored in the local variable connection, and the program continues. We call getOutputStream( ) to get the output stream associated with that Socket and then chain that output stream to a new OutputStreamWriter, out. To get the current Date, we construct a new Date object and send it to the client by writing its string representation on out with write( ).

Finally, after the data is sent or an exception has been thrown, we close connection inside the finally block. Always close a Socket when you're finished with it. In the previous chapter, we said that a client shouldn't rely on the other side of a connection to close the socket. That goes triple for servers. Clients can time out or crash; users can cancel transactions; networks can go down in high-traffic periods. For any of these or a dozen more reasons, you cannot rely on clients to close sockets, even when the protocol requires them to (which it doesn't in this case).

Sending binary, nontext data is not significantly harder. Example 11-3 demonstrates with a time server. This follows the time protocol outlined in RFC 868. When a client connects, the server sends a 4-byte, big-endian, unsigned integer specifying the number of seconds that have passed since 12:00 A.M., January 1, 1900 GMT (the epoch). The current time can be retrieved simply by creating a new Date object. However, since the Date class counts milliseconds since 12:00 A.M., January 1, 1970 GMT rather than seconds since 12:00 A.M., January 1, 1900 GMT, some conversion is necessary.

Example 11-3: A Time Server

import java.net.*;
import java.io.*;
import java.util.Date;
  
public class TimeServer {
  
  DEFAULT_PORT DEFAULT_PORT = 37;
 
  public static void main(String[] args) {
        
   int port = DEFAULT_PORT;     
   if (args.length > 0) {
     try {
        port = Integer.parseInt(args[0]);
        if (port < 0 || port >= 65536) {
          System.out.println("Port must between 0 and 65535");
          return;      
        }
     }   
     catch (NumberFormatException e) {}  
   }     
 
   // The time protocol sets the epoch at 1900,
   // the java Date class at 1970. This number 
   // converts between them.
    
   long differenceBetweenEpochs = 2208988800L;
    
   try {
     ServerSocket server = new ServerSocket(port);
       while (true) {
         Socket connection = null;
         try {
           connection = server.accept(  );
           OutputStream out = connection.getOutputStream(  );
           Date now = new Date(  );
           long msSince1970 = now.getTime(  );
           long secondsSince1970 = msSince1970/1000;
           long secondsSince1900 = secondsSince1970 
            + differenceBetweenEpochs;
           byte[] time = new byte[4];
           time[0] 
            = (byte) ((secondsSince1900 & 0x00000000FF000000L) >> 24);
           time[1] 
            = (byte) ((secondsSince1900 & 0x0000000000FF0000L) >> 16);
           time[2] 
            = (byte) ((secondsSince1900 & 0x000000000000FF00L) >> 8);
           time[3] = (byte) (secondsSince1900 & 0x00000000000000FFL);
           out.write(time);
           out.flush(  );      
         } // end try
         catch (IOException e) {
         } // end catch
         finally {
           if (connection != null) connection.close(  ); 
         }
       }  // end while
   }  // end try
   catch (IOException e) {
     System.err.println(e);
   } // end catch
 
  } // end main
 
} // end TimeServer

As with the TimeClient of the previous chapter, most of the effort here goes into working with a data format (32-bit unsigned integers) that Java doesn't natively support.

public void qclose( ) throws IOException

If you're finished with a server socket, you should close it, especially if your program is going to continue to run for some time. This frees up the port for other programs that may wish to use it. Closing a ServerSocket should not be confused with closing a Socket. Closing a ServerSocket frees a port on the local host, allowing another server to bind to the port; closing a Socket breaks the connection between the local and the remote hosts.

Server sockets are closed automatically when a program dies, so it's not absolutely necessary to close them in programs that terminate shortly after the ServerSocket is no longer needed. Nonetheless, it doesn't hurt. For example, the main loop of the LocalPortScanner program might be better written like this so that it doesn't temporarily occupy most of the ports on the system:


for (int port = 1; port <= 65535; port++) {
 
  try {
    // the next line will fail and drop into the catch block if
    // there is already a server running on the port
    ServerSocket server = new ServerSocket(port);
    server.close(  );
  }
  catch (IOException e) {
    System.out.println("There is a server on port " + port + ".");
  } // end try
 
} // end for

The get Methods

The ServerSocket class provides two getter methods to tell you the local address and port occupied by the server socket. These are useful if you've opened a server Socket on an anonymous port and/or an unspecified network interface. This would be the case, for one example, in the data connection of an FTP session.

public InetAddress getInetAddress( )

This method returns the address being used by the server (the local host). If the local host has a single IP address (as most do), then this is the address returned by InetAddress.getLocalHost( ). If the local host has more than one IP address, then the specific address returned is one of the host's IP addresses. You can't predict which address you will get. For example:

try {
  ServerSocket httpd = new ServerSocket(80);
  InetAddress ia = httpd.getInetAddress(  );
}
catch (IOException e) {
}

public int getLocalPort( )

The ServerSocket constructors allow you to listen on an unspecified port by passing 0 for the port number. This method lets you find out what port you're listening on. You might use this in a peer-to-peer multisocket program where you already have a means to inform other peers of your location. Or a server might spawn several smaller servers to perform particular operations. The well-known server could inform clients what ports they can find the smaller servers on. Of course, you can also use getLocalPort( ) to find a non-anonymous port, but why would you need to? Example 11-4 demonstrates.

Example 11-4: A Random Port


import java.net.*;
import java.io.*;
 
public class RandomPort {
 
  public static void main(String[] args) {
 
    try {
      ServerSocket server = new ServerSocket(0);
      System.out.println("This server runs on port " 
       + server.getLocalPort(  ));
    }
    catch (IOException e) {
      System.err.println(e);
    }
 
  }
 
}

Here's the output of several runs:

D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1154
D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1155
D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1156

At least on this VM, the ports aren't really random; but they are at least indeterminate until runtime.

Socket Options

The only Socket option supported for server sockets is SO_TIMEOUT. SO_TIMEOUT is the amount of time, in milliseconds, that accept( ) waits for an incoming connection before throwing a java.io.InterruptedIOException. If SO_TIMEOUT is 0, then accept( ) will never time out. The default is to never time out.

Using SO_TIMEOUT is rather rare. You might need it if you were implementing a complicated and secure protocol that required multiple connections between the client and the server where some responses needed to occur within a fixed amount of time. Most servers are designed to run for indefinite periods of time and therefore use the default timeout value, which is 0 (never time out).

public void setSoTimeout(int timeout) throws SocketException

The setSoTimeout( ) method sets the SO_TIMEOUT field for this server Socket object. The countdown starts when accept( ) is invoked. When the timeout expires, accept( ) throws an InterruptedIOException. You should set this option before calling accept( ); you cannot change the timeout value while accept( ) is waiting for a connection. The timeout argument must be greater than or equal to zero; if it isn't, the method throws an IllegalArgumentException. For example:


try {
  ServerSocket server = new ServerSocket(2048);
  server.setSoTimeout(30000); // block for no more than 30 seconds
  try {
    Socket s = server.accept(  );
    // handle the connection
    // ...
  }
  catch (InterruptedIOException e) {
    System.err.println("No connection within 30 seconds");
  }
  finally {
    server.close(  );
  }
catch (IOException e) {
  System.err.println("Unexpected IOException: " + e);
}

public int getSoTimeout( ) throws IOException

The getSoTimeout( ) method returns this server socket's current SO_TIMEOUT value. For example:

public void printSoTimeout(ServerSocket server) {
 
  int timeout = server.getSoTimeOut(  );
  if (timeout > 0) {
    System.out.println(server + " will time out after " 
     + timeout + "milliseconds.");
  }
  else if (timeout == 0) {
    System.out.println(server + " will never time out.");
  }
  else {
    System.out.println("Impossible condition occurred in " + server);
    System.out.println("Timeout cannot be less than zero." );
  }
 
}

The Object Methods

jServerSocket overrides only one of the standard methods from java.lang.Object, toString( ). Thus, equality comparisons test for strict identity, and server sockets are problematic in hash tables. Normally, this isn't a large problem.

public String toString( )

A String returned by ServerSocket's toString( ) method looks like this:

ServerSocket[addr=0.0.0.0,port=0,localport=5776]

In current implementations, addr is always 0.0.0.0 and port is always 0. Presumably, these may become something more interesting in the future. The localport is the local port on which the server is listening for connections.

Implementation

The ServerSocket class provides two methods for changing the default implementation of server sockets. I'll describe them only briefly here, since they're primarily intended for implementers of Java virtual machines rather than application programmers.

public static synchronized void setSocketFactory (SocketImpl Factory fac) throws IOException

This method sets the system's server SocketImplFactory, which is the factory used to create ServerSocket objects. This is not the same factory that is used to create client Socket objects, though the syntax is similar; you can have one factory for Socket objects and a different factory for ServerSocket objects. You can set this factory only once in a program, however. A second attempt to set the SocketImplFactory throws a SocketException.

Protected final void implAccept(Socket s) throws IOException

Subclasses of ServerSocket use this method to implement accept( ). You pass an unconnected Socket object to implaccept( ). (Doing this requires you to subclass Socket as well since the standard java.net.Socket class doesn't provide a means to create unconnected sockets.) When the method returns, the Socket argument s is connected to a client.

Some Useful Servers

This section shows several servers you can build with server sockets. It starts with a server you can use to test client responses and requests, much as you use Telnet to test server behavior. Then we present three different HTTP servers, each with a different special purpose and each slightly more complex than the previous one.

Client Tester

In the previous chapter, you learned how to use Telnet to experiment with servers. There's no equivalent program to test clients, so let's create one. Example 11-5 is a program called ClientTester that runs on a port specified on the command-line, shows all data sent by the client, and allows you to send a response to the client by typing it on the command line. For example, you can use this program to see the commands that Netscape Navigator sends to a server.

NOTE: Clients are rarely as forgiving about unexpected server responses as servers are about unexpected client responses. If at all possible, try to run the clients that connect to this program on a Unix system or some other platform that is moderately crash-proof. Don't run them on a Mac or Windows 98, which are less stable.

This program uses two threads: one to handle input from the client and the other to send output from the server. Using two threads allows the program to handle input and output simultaneously: it can be sending a response to the client while receiving a request--or, more to the point, it can send data to the client while waiting for the client to respond. This is convenient because different clients and servers talk in unpredictable ways. With some protocols, the server talks first; with others, the client talks first. Sometimes the server sends a one-line response; often, the response is much larger. Sometimes the client and the server talk at each other simultaneously. Other times, one side of the connection waits for the other to finish before it responds. The program must be flexible enough to handle all these cases. Example 11-5 shows the code.

Example 11-5: A Client Tester


import java.net.*;
import java.io.*;
import com.macfaq.io.SafeBufferedReader; // from Chapter 4
 
 
public class ClientTester {
 
  public static void main(String[] args) {
 
    int port;
    
    try {
      port = Integer.parseInt(args[0]);
    }  
    catch (Exception e) {
      port = 0;
    }
    
    try {
      ServerSocket server = new ServerSocket(port, 1);
      System.out.println("Listening for connections on port " 
       + server.getLocalPort(  ));
 
      while (true) {
        Socket connection = server.accept(  );
        try {
          System.out.println("Connection established with " 
           + connection);
          Thread input = new InputThread(connection.getInputStream(  ));
          input.start(  );
          Thread output 
           = new OutputThread(connection.getOutputStream(  ));
          output.start(  );
          // wait for output and input to finish 
          try {
            input.join(  );
            output.join(  );
          }
          catch (InterruptedException e) {
          }
        }
        catch (IOException e) {
          System.err.println(e); 
        }
        finally {
          try {
            if (connection != null) connection.close(  );
          }
          catch (IOException e) {}
        }
      }
    }
    catch (IOException e) {
      e.printStackTrace(  );
    }
  
  }
 
}
 
class InputThread extends Thread {
  
  InputStream in;
  
   public InputThread(InputStream in) {
     this.in = in;
   }
 
   public void run(  )  {
   
     try {     
       while (true) {
         int i = in.read(  );
         if (i == -1) break;
         System.out.write(i);
       }
     }
     catch (SocketException e) {
       // output thread closed the socket
     }
     catch (IOException e) {
       System.err.println(e);
     }
     try {
       in.close(  );
     }
     catch (IOException e) { 
     } 
 
  }
 
}
 
class OutputThread extends Thread {
  
  Writer out;
    
  public OutputThread(OutputStream out) {
    this.out = new OutputStreamWriter(out);
  }
 
  public void run(  ) {
 
    String line;
    BufferedReader in 
     = new SafeBufferedReader(new InputStreamReader(System.in));
    try {
      while (true) {
        line = in.readLine(  );
        if (line.equals(".")) break;
        out.write(line +"\r\n");
        out.flush(  );
      }   
    }
    catch (IOException e) { 
    } 
    try {
      out.close(  );
    }
    catch (IOException e) { 
    } 
    
   }
 
}

The client tester application is split into three classes: ClientTester, InputThread, and OutputThread. The ClientTester class reads the port from the command-line, opens a ServerSocket on that port, and listens for incoming connections. Only one connection is allowed at a time, because this program is designed for experimentation, and a slow human being has to provide all responses. Consequently, we set an unusually short queue length of 1. Further connections will be refused until the first one has been closed.

An infinite while loop waits for connections with the accept( ) method. When a connection is detected, its InputStream is used to construct a new InputThread, and its OutputStream is used to construct a new OutputThread. After starting these threads, we wait for them to finish by calling their join( ) methods.

The InputThread is contained almost entirely in the run( ) method. It has a single field, in, which is the InputStream from which data will be read. Data is read from in one byte at a time. Each byte read is written on System.out. The run( ) method ends when the end of stream is encountered or an IOException is thrown. The most likely exception here is a SocketException thrown because the corresponding OutputThread closed the connection.

The OutputThread reads input from the local user sitting at the terminal and sends that data to the client. Its constructor has a single argument, an output stream for sending data to the client. OutputThread reads input from the user on System.in, which is chained to an instance of the SafeBufferedReader class developed in Chapter 4, Java I/O. The OutputStream that was passed to the constructor is chained to an OutputStreamWriter for convenience. The run( ) method for OutputThread reads lines from the SafeBufferedReader, and copies them onto the OutputStreamWriter, which sends them to the client. A period typed on a line by itself signals the end of user input. When this occurs, run( ) exits the loop and out is closed. This has the effect of also closing the socket, so that a SocketException is thrown in the input thread, which also exits.

For example, here's the output when Netscape Communicator 4.6 for Windows connected to this server:


D:\JAVA\JNP2\examples\11> java ClientTester 80
Listening for connections on port 80
Connection established with 
Socket[addr=localhost/127.0.0.1,port=1033,localport=80]
GET / HTTP 1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.6 [en] (WinNT; I)
Host: localhost
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Encoding: gzip
Accept-Language: en
Accept-Charset: iso-8859-1,*,utf-8
 
 <html><body><h1>Hello Client!</h2></body></html>
 .

Even minimal exploration of clients can reveal some surprising things. For instance, I didn't know until I wrote this example that Netscape Navigator 4.6 can read .gz files just as easily as it can read HTML files. That may be useful for serving large text files full of redundant data.

HTTP Servers

HTTP is a large protocol. As you saw in Chapter 3, Basic Web Concepts, a full-featured HTTP server must respond to requests for files, convert URLs into filenames on the local system, respond to POST and GET requests, handle requests for files that don't exist, interpret MIME types, launch CGI programs, and much, much more. However, many HTTP servers don't need all of these features. For example, many sites simply display an "under construction" message. Clearly, Apache is overkill for a site like this. Such a site is a candidate for a custom server that does only one thing. Java's network class library makes writing simple servers like this almost trivial.

Custom servers aren't useful only for small sites. High-traffic sites like Yahoo! are also candidates for custom servers because a server that does only one thing can often be much faster than a general purpose server such as Apache or Netscape. It is easy to optimize a special purpose server for a particular task; the result is often much more efficient than a general purpose server that needs to respond to many different kinds of requests. For instance, icons and images that are used repeatedly across many pages or on high-traffic pages might be better handled by a server that read all the image files into memory on startup, and then served them straight out of RAM rather than having to read them off disk for each request. Furthermore, this server could avoid wasting time on logging if you didn't want to track the image request separately from the requests for the pages they were included in.

Finally, Java isn't a bad language for feature-full web servers meant to compete with the likes of Apache or AOLServer. Although CPU-intensive Java programs are demonstrably slower than CPU-intensive C and C++ programs, even when run under a JIT, most HTTP servers are limited by bandwidth, not by CPU speed. Consequently, Java's other advantages, such as its half-compiled/half-interpreted nature, dynamic class loading, garbage collection, and memory protection, really get a chance to shine. In particular, sites that make heavy use of dynamic content through CGI scripts, PHP pages, or other mechanisms can often run much faster when reimplemented on top of a pure or mostly pure Java web server. Indeed, there are several production web servers written in Java such as the W3C's testbed server Jigsaw (https://www.w3.org/Jigsaw/). Many other web servers written in C now include substantial Java components to support the Java Servlet API and Java Server Pages. On many sites, these are replacing the traditional CGIs, ASPs, and server-side includes, mostly because the Java equivalents are faster and less resource-intensive. I'm not going to explore these technologies here since they easily deserve a book of their own. I refer interested readers to Jason Hunter's Java Servlet Programming (O'Reilly & Associates, Inc., 1998). However, it is important to note that servers in general and web servers in particular are one area where Java really is competitive with C.

A single-file server

Our investigation of HTTP servers begins with a server that always sends out the same file, no matter who or what the request. This is shown in Example 11-6, SingleFileHTTPServer. The filename, local port, and content encoding are read from the command line. If the port is omitted, port 80 is assumed. If the encoding is omitted, ASCII is assumed.

Example 11-6: An HTTP Server That Chunks Out the Same File


import java.net.*;
import java.io.*;
import java.util.*;
 
public class SingleFileHTTPServer extends Thread {
 
  private byte[] content;
  private byte[] header;
  private int port = 80;
 
  public SingleFileHTTPServer(String data, String encoding, 
   String MIMEType, int port) throws UnsupportedEncodingException {    
    this(data.getBytes(encoding), encoding, MIMEType, port);
  }
 
  public SingleFileHTTPServer(byte[] data, String encoding, 
   String MIMEType, int port) throws UnsupportedEncodingException {
    
    this.content = data;
    this.port = port;
    String header = "HTTP 1.0 200 OK\r\n"
     + "Server: OneFile 1.0\r\n"
     + "Content-length: " + this.content.length + "\r\n"
     + "Content-type: " + MIMEType + "\r\n\r\n";
    this.header = header.getBytes("ASCII");
 
  }
 
  
  public void run(  ) {
  
    try {
      ServerSocket server = new ServerSocket(this.port); 
      System.out.println("Accepting connections on port " 
        + server.getLocalPort(  ));
      System.out.println("Data to be sent:");
      System.out.write(this.content);
      while (true) {
        
        Socket connection = null;
        try {
          connection = server.accept(  );
          OutputStream out = new BufferedOutputStream(
                                  connection.getOutputStream(  )
                                 );
          InputStream in   = new BufferedInputStream(
                                  connection.getInputStream(  )
                                 );
          // read the first line only; that's all we need
          StringBuffer request = new StringBuffer(80);
          while (true) {
            int c = in.read(  );
            if (c == '\r' || c == '\n' || c == -1) break;
            request.append((char) c);
            // If this is HTTP 1.0 or later send a MIME header
 
          }
          if (request.toString(  ).indexOf("HTTP/") != -1) {
            out.write(this.header);
          }         
          out.write(this.content);
          out.flush(  );
        }  // end try
        catch (IOException e) {   
        }
        finally {
          if (connection != null) connection.close(  ); 
        }
        
      } // end while
    } // end try
    catch (IOException e) {
      System.err.println("Could not start server. Port Occupied");
    }
 
  } // end run
 
 
  public static void main(String[] args) {
 
    try {
        
      String contentType = "text/plain";
      if (args[0].endsWith(".html") || args[0].endsWith(".htm")) {
        contentType = "text/html";
      }
      
      InputStream in = new FileInputStream(args[0]);
      ByteArrayOutputStream out = new ByteArrayOutputStream(  );
      int b;
      while ((b = in.read(  )) != -1) out.write(b);
      byte[] data = out.toByteArray(  );
        
      // set the port to listen on
      int port;
      try {
        port = Integer.parseInt(args[1]);
        if (port < 1 || port > 65535) port = 80;
      }  
      catch (Exception e) {
        port = 80;
      }  
      
      String encoding = "ASCII";
      if (args.length >= 2) encoding = args[2]; 
       
      Thread t = new SingleFileHTTPServer(data, encoding,
       contentType, port);
      t.start(  );         
 
    }
    catch (ArrayIndexOutOfBoundsException e) {
      System.out.println(
       "Usage: java SingleFileHTTPServer filename port encoding");
    }
    catch (Exception e) {
      System.err.println(e);
    }
  
  }
 
}

The constructors set up the data to be sent along with an HTTP header that includes information about content length and content encoding. The header and the body of the response are stored in byte arrays in the desired encoding so that they can be blasted to clients very quickly.

The SingleFileHTTPServer class itself is a subclass of Thread. Its run( ) method processes incoming connections. Chances are this server will serve only small files and will support only low-volume web sites. Since all the server needs to do for each connection is check whether the client supports HTTP 1.0 and spew one or two relatively small byte arrays over the connection, chances are this will be sufficient. On the other hand, if you find clients are getting refused, you could use multiple threads instead. A lot depends on the size of the file being served, the peak number of connections expected per minute, and the thread model of Java on the host machine. Using multiple threads would be a clear win for a server that was even slightly more sophisticated than this one.

The run( ) method creates a ServerSocket on the specified port. Then it enters an infinite loop that continually accepts connections and processes them. When a Socket is accepted, an InputStream reads the request from the client. It looks at the first line to see whether it contains the string HTTP. If it sees this, the server assumes that the client understands HTTP 1.0 or later and therefore sends a MIME header for the file; then it sends the data. If the client request doesn't contain the string HTTP, the server omits the header, sending the data by itself. Finally, the server closes the connection and tries to accept the next connection.

The main( ) method just reads parameters from the command line. The name of the file to be served is read from the first command-line argument. If no file is specified or the file cannot be opened, an error message is printed and the program exits. Assuming the file can be read, its contents are read into the byte array data. A reasonable guess is made about the content type of the file, and that guess is stored in the contentType variable. Next, the port number is read from the second command-line argument. If no port is specified, or if the second argument is not an integer from 0 to 65,535, then port 80 is used. The encoding is read from the third command-line argument if present. Otherwise, ASCII is assumed. (Surprisingly, some VMs don't support ASCII, so you might want to pick 8859-1 instead.) Then these values are used to construct a SingleFileHTTPServer object and start it running. This is only one possible interface. You could easily use this class as part of some other program. If you added a setter method to change the content, you could easily use it to provide simple status information about a running server or system. However, that would raise some additional issues of thread safety that Example 11-5 doesn't have to address because it's immutable.

Here's what you see when you connect to this server via Telnet; the specifics depend on the exact server and file:


%  telnet macfaq.dialup.cloud9.net 80
Trying 168.100.203.234...
Connected to macfaq.dialup.cloud9.net.
Escape character is '^]'.
 GET / HTTP 1.0
HTTP 1.0 200 OK
Server: OneFile 1.0
Content-length: 959
Content-type: text/html
 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
<TITLE>Under Construction</TITLE>
</HEAD>
 
<BODY>
...

A redirector

Another simple but useful application for a special-purpose HTTP server is redirection. In this section, we develop a server that redirects users from one web site to another--for example, from cnet.com to home.cnet.com. Example 11-7 reads a URL and a port number from the command-line, opens a server Socket on the port, then redirects all requests that it receives to the site indicated by the new URL, using a 302 FOUND code. Chances are this server is fast enough not to require multiple threads. Nonetheless, threads might be mildly advantageous, especially on a high-volume site on a slow network connection. And this server does a lot of string processing, one of Java's most notorious performance bottlenecks. But really for purposes of example more than anything, I've made the server multithreaded. In this example, I chose to use a new thread rather than a thread pool for each connection. This is perhaps a little simpler to code and understand but somewhat less efficient. In Example 11-8, we'll look at an HTTP server that uses a thread pool.

Example 11-7: An HTTP Redirector


import java.net.*;
import java.io.*;
import java.util.*;
 
public class Redirector implements Runnable {
 
  private int port;
  private String newSite;
  
  public Redirector(String site, int port) {
    this.port = port;
    this.newSite = site;
  }
 
  public void run(  ) {
    
    try {
      
      ServerSocket server = new ServerSocket(this.port); 
      System.out.println("Redirecting connections on port " 
        + server.getLocalPort(  ) + " to " + newSite);
        
      while (true) {
        
        try {
          Socket s = server.accept(  );
          Thread t = new RedirectThread(s);
          t.start(  );
        }  // end try
        catch (IOException e) {   
        }
        
      } // end while
      
    } // end try
    catch (BindException e) {
      System.err.println("Could not start server. Port Occupied");
    }         
    catch (IOException e) {
      System.err.println(e);
    }         
 
  }  // end run
 
  class RedirectThread extends Thread {
        
    private Socket connection;
        
    RedirectThread(Socket s) {
      this.connection = s;    
    }
        
    public void run(  ) {
      
      try {
        
        Writer out = new BufferedWriter(
                      new OutputStreamWriter(
                       connection.getOutputStream(  ), "ASCII"
                      )
                     );
        Reader in = new InputStreamReader(
                     new BufferedInputStream( 
                      connection.getInputStream(  )
                     )
                    );
                    
        // read the first line only; that's all we need
        StringBuffer request = new StringBuffer(80);
        while (true) {
          int c = in.read(  );
          if (c == '\r' || c == '\n' || c == -1) break;
          request.append((char) c);
        }
        // If this is HTTP 1.0 or later send a MIME header
        String get = request.toString(  );
        int firstSpace = get.indexOf(' ');
        int secondSpace = get.indexOf(' ', firstSpace+1);
        String theFile = get.substring(firstSpace+1, secondSpace);
        if (get.indexOf("HTTP") != -1) {
          out.write("HTTP1.0 302 FOUND\r\n");
          Date now = new Date(  );
          out.write("Date: " + now + "\r\n");
          out.write("Server: Redirector 1.0\r\n");
          out.write("Location: " + newSite + theFile + "\r\n");        
          out.write("Content-type: text/html\r\n\r\n");                 
          out.flush(  );                
        }
        // Not all browsers support redirection so we need to 
        // produce HTML that says where the document has moved to.
        out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
        out.write("<BODY><H1>Document moved</h2>\r\n");
        out.write("The document " + theFile  
         + " has moved to\r\n<A href=\"" + newSite + theFile + "\">" 
         + newSite  + theFile 
         + "</A>.\r\n Please upDate your bookmarks<P>");
        out.write("</BODY></HTML>\r\n");
        out.flush(  );
 
      } // end try
      catch (IOException e) {
      }
      finally {
        try {
          if (connection != null) connection.close(  );
        }
        catch (IOException e) {}  
      }     
  
    }  // end run
    
  }
 
  public static void main(String[] args) {
 
    int thePort;
    String theSite;
    
    try {
      theSite = args[0];
      // trim trailing slash
      if (theSite.endsWith("/")) {
        theSite = theSite.substring(0, theSite.length(  )-1);
      }
    }
    catch (Exception e) {
      System.out.println(
       "Usage: java Redirector https://www.newsite.com/ port");
      return;
    }
    
    try {
      thePort = Integer.parseInt(args[1]);
    }  
    catch (Exception e) {
      thePort = 80;
    }  
      
    Thread t = new Thread(new Redirector(theSite, thePort));
    t.start(  );
  
  }  // end main
 
}

To start the redirector on port 80 and redirect incoming requests to https://metalab.unc.edu/xml, you would type:


D:\JAVA\JNP2\examples\11> java Redirector https://metalab.unc.edu/xml
Redirecting connections on port 80 to https://metalab.unc.edu/xml

If you connect to this server via Telnet, this is what you'll see:


%  telnet macfaq.dialup.cloud9.net 80
Trying 168.100.203.234...
Connected to macfaq.dialup.cloud9.net.
Escape character is '^]'.
 GET / HTTP 1.0
HTTP 1.0 302 FOUND
Date: Wed Sep 08 11:59:42 PDT 1999
Server: Redirector 1.0
Location: https://metalab.unc.edu/xml/
Content-type: text/html
 
<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>
<BODY><H1>Document moved</h2>
The document / has moved to
<A href="https://metalab.unc.edu/xml/">https://metalab.unc.edu/xml/</A>.
 Please upDate your bookmarks<P></BODY></HTML>
Connection closed by foreign host.

If, however, you connect with a reasonably modern web browser, you should be sent to https://metalab.unc.edu/xml with only a slight delay. You should never see the HTML added after the response code; this is provided to support older browsers that don't do redirection automatically.

The main( ) method provides a very simple interface that reads the URL of the new site to redirect connections to and the local port to listen on. It uses this information to construct a Redirector object. Then it uses the resulting Runnable object (Redirector implements Runnable) to spawn a new thread and start it. If the port is not specified, Redirector listens on port 80. If the site is omitted, Redirector prints an error message and exits.

The run( ) method of Redirector binds the server Socket to the port, prints a brief status message, and then enters an infinite loop in which it listens for connections. Every time a connection is accepted, the resulting Socket object is used to construct a RedirectThread. This RedirectThread is then started. All further interaction with the client takes place in this new thread. The run( ) method of Redirector then simply waits for the next incoming connection.

The run( ) method of RedirectThread does most of the work. It begins by chaining a Writer to the Socket's output stream, and a Reader to the Socket's input stream. Both input and output are buffered. Then the run( ) method reads the first line the client sends. Although the client will probably send a whole MIME header, we can ignore that. The first line contains all the information we need. This line looks something like this:

GET /directory/filename.html HTTP 1.0

It is possible that the first word will be POST or PUT instead or that there will be no HTTP version. The second "word" is the file the client wants to retrieve. This must begin with a slash (/). Browsers are responsible for converting relative URLs to absolute URLs that begin with a slash; the server does not do this. The third word is the version of the HTTP protocol the browser understands. Possible values are nothing at all (pre-HTTP 1.0 browsers), HTTP 1.0 (most current browsers), or HTTP 1.1.

To handle a request like this, Redirector ignores the first word. The second word is attached to the URL of the target server (stored in the field newSite) to give a full redirected URL. The third word is used to determine whether to send a MIME header; MIME headers are not used for old browsers that do not understand HTTP 1.0. If there is a version, a MIME header is sent; otherwise, it is omitted.

Sending the data is almost trivial. The Writer out is used. Since all the data we send is pure ASCII, the exact encoding isn't too important. The only trick here is that the end-of-line character for HTTP requests is \r\n--a carriage return followed by a linefeed.

The next lines each send one line of text to the client. The first line printed is:

HTTP 1.0 302 FOUND

This is an HTTP 1.0 response code that tells the client to expect to be redirected. The second line is a Date: header that gives the current time at the server. This line is optional. The third line is the name and version of the server; this is also optional but is used by spiders that try to keep statistics about the most popular web servers. (It would be very surprising to ever see Redirector break into single digits in lists of the most popular servers.) The next line is the Location: header, which is required for this server. It tells the client where it is being redirected to. Last is the standard Content-type: header. We send the content type text/html to indicate that the client should expect to see HTML. Finally, a blank line is sent to signify the end of the header data.

Everything after this will be HTML, which is processed by the browser and displayed to the user. The next several lines print a message for browsers that do not support redirection, so those users can manually jump to the new site. That message looks like:


<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>
<BODY><H1>Document moved</h2>
The document / has moved to
<A href="https://metalab.unc.edu/xml/">https://metalab.unc.edu/xml/</A>.
 Please upDate your bookmarks<P></BODY></HTML>
 

Finally, the connection is closed and the thread dies.

A full-fledged HTTP server

Enough with special-purpose HTTP servers. This section develops a full-blown HTTP server, called JHTTP, that can serve an entire document tree, including images, applets, HTML files, text files, and more. It will be very similar to the SingleFileHTTPServer, except that it pays attention to the GET requests. This server is still fairly lightweight; after looking at the code, we'll discuss other features you might want to add.

Since this server may have to read and serve large files from the filesystem over potentially slow network connections, we'll change its approach. Rather than processing each request as it arrives in the main thread of execution, we'll place incoming connections in a pool. Separate instances of a RequestProcessor class will remove the connections from the pool and process them. Example 11-8 shows the main JHTTP class. As in the previous two examples, the main( ) method of JHTTP handles initialization, but other programs could use this class themselves to run basic web servers.

Example 11-8:The JHTTP Web Server


import java.net.*;
import java.io.*;
import java.util.*;
 
public class JHTTP extends Thread {
 
  private File documentRootDirectory;
  private String indexFileName = "index.html";
  private ServerSocket server;
  private int numThreads = 50;
    
  public JHTTP(File documentRootDirectory, int port, 
   String indexFileName) throws IOException {
    
    if (!documentRootDirectory.isDirectory(  )) {
      throw new IOException(documentRootDirectory 
       + " does not exist as a directory"); 
    }
    this.documentRootDirectory = documentRootDirectory;
    this.indexFileName = indexFileName;
    this.server = new ServerSocket(port);
  }
 
  public JHTTP(File documentRootDirectory, int port) 
   throws IOException {
    this(documentRootDirectory, port, "index.html");
  }
 
  public JHTTP(File documentRootDirectory) throws IOException {
    this(documentRootDirectory, 80, "index.html");
  }
 
  public void run(  ) {
  
    for (int i = 0; i < numThreads; i++) {
      Thread t = new Thread(
       new RequestProcessor(documentRootDirectory, indexFileName));
      t.start(  );   
    }
    System.out.println("Accepting connections on port " 
     + server.getLocalPort(  ));
    System.out.println("Document Root: " + documentRootDirectory);
    while (true) {
      try {
        Socket request = server.accept(  );
        RequestProcessor.processRequest(request);
      }
      catch (IOException e) { 
      }   
    }
    
  }
  
  public static void main(String[] args) {
 
    // get the Document root
    File docroot;
    try {
      docroot = new File(args[0]);
    }
    catch (ArrayIndexOutOfBoundsException e) {
      System.out.println("Usage: java JHTTP docroot port indexfile");
      return;
    }
    
    // set the port to listen on
    int port;
    try {
      port = Integer.parseInt(args[1]);
      if (port < 0 || port > 65535) port = 80;
    }  
    catch (Exception e) {
      port = 80;
    }  
    
    try {            
      JHTTP webserver = new JHTTP(docroot, port);
      webserver.start(  );
    }
    catch (IOException e) {
      System.out.println("Server could not start because of an " 
       + e.getClass(  ));
      System.out.println(e);
    }
  
  }
 
}

The main( ) method of the JHTTP class sets the document root directory from args[0]. The port is read from args[1], or 80 is used for a default. Then a new JHTTP thread is constructed and started. The JHTTP thread spawns 50 RequestProcessor threads to handle requests, each of which will retrieve incoming connection requests from the RequestProcessor pool as they become available. The JHTTP thread repeatedly accepts incoming connections and puts them in the RequestProcessor pool.

Each connection is handled by the run( ) method of the RequestProcessor class shown in Example 11-9. This method waits until it can get a Socket out of the pool. Once it does that, it gets input and output streams from the Socket and chains them to a reader and a writer. The reader reads the first line of the client request to determine the version of HTTP that the client supports--we want to send a MIME header only if this is HTTP 1.0 or later--and what file is requested. Assuming the method is GET, the file that is requested is converted to a filename on the local filesystem. If the file requested was a directory (i.e., its name ended with a slash), we add the name of an index file. We use the canonical path to make sure that the requested file doesn't come from outside the document root directory. Otherwise, a sneaky client could walk all over the local filesystem by including .. in URLs to walk up the directory hierarchy. This is all we'll need from the client, though a more advanced web server, especially one that logged hits, would read the rest of the MIME header the client sends.

Next the requested file is opened and its contents are read into a byte array. If the HTTP version is 1.0 or later, we write the appropriate MIME headers on the output stream. To figure out the content type, we call the guessContentTypeFromName( ) method to map file extensions such as .html onto MIME types such as text/html. The byte array containing the file's contents is written onto the output stream, and the connection is closed. Exceptions may be thrown at various places if, for example, the file cannot be found or opened. If an exception occurs, we send an appropriate HTTP error message to the client instead of the file's contents.

Example 11-9: The Thread Pool That Handles HTTP Requests


import java.net.*;
import java.io.*;
import java.util.*;
    
public class RequestProcessor implements Runnable {
    
  private static List pool = new LinkedList(  );
  private File documentRootDirectory;
  private String indexFileName = "index.html";
  
  public RequestProcessor(File documentRootDirectory, 
   String indexFileName) {
        
    if (documentRootDirectory.isFile(  )) {
      throw new IllegalArgumentException(
       "documentRootDirectory must be a directory, not a file");   
    }
    this.documentRootDirectory = documentRootDirectory;
    try {
      this.documentRootDirectory 
       = documentRootDirectory.getCanonicalFile(  );
    }
    catch (IOException e) {
    }
    if (indexFileName != null) this.indexFileName = indexFileName;
  }
  
  public static void processRequest(Socket request) {
    
    synchronized (pool) {
      pool.add(pool.size(  ), request);
      pool.notifyAll(  );
    }
 
  }  
  
  public void run(  ) {
        
    // for security checks
    String root = documentRootDirectory.getPath(  );
      
    while (true) {       
      Socket connection;
      synchronized (pool) {         
        while (pool.isEmpty(  )) {
          try {
            pool.wait(  );
          }
          catch (InterruptedException e) {
          }
        }
        connection = (Socket) pool.remove(0); 
      }
 
      try {            
        String filename;
        String contentType;   
        OutputStream raw = new BufferedOutputStream(
                            connection.getOutputStream(  )
                           );         
        Writer out = new OutputStreamWriter(raw);
        Reader in = new InputStreamReader(
                     new BufferedInputStream(
                      connection.getInputStream(  )
                     ),"ASCII"
                    );
        StringBuffer requestLine = new StringBuffer(  );
        int c;
        while (true) {
          c = in.read(  );
          if (c == '\r' || c == '\n') break;
          requestLine.append((char) c);
        }
        
        String get = requestLine.toString(  );
        
        // log the request 
        System.out.println(get);
        
        StringTokenizer st = new StringTokenizer(get);
        String method = st.nextToken(  );
        String version = "";
        if (method.equals("GET")) {
          filename = st.nextToken(  );
          if (filename.endsWith("/")) filename += indexFileName;
          contentType = guessContentTypeFromName(filename);
          if (st.hasMoreTokens(  )) {
            version = st.nextToken(  );
          }
 
          File theFile = new File(documentRootDirectory, 
           filename.substring(1,filename.length(  )));
          if (theFile.canRead(  ) 
              // Don't let clients outside the document root
           && theFile.getCanonicalPath(  ).startsWith(root)) {
            DataInputStream fis = new DataInputStream(
                                   new BufferedInputStream(
                                    new FileInputStream(theFile)
                                   )
                                  );
            byte[] theData = new byte[(int) theFile.length(  )];
            fis.readFully(theData);
            fis.close(  );
            if (version.startsWith("HTTP ")) {  // send a MIME header
              out.write("HTTP 1.0 200 OK\r\n");
              Date now = new Date(  );
              out.write("Date: " + now + "\r\n");
              out.write("Server: JHTTP 1.0\r\n");
              out.write("Content-length: " + theData.length + "\r\n");
              out.write("Content-type: " + contentType + "\r\n\r\n");
              out.flush(  );
            }  // end try
        
            // send the file; it may be an image or other binary data 
            // so use the underlying output stream 
            // instead of the writer
            raw.write(theData);
            raw.flush(  );
          }  // end if
          else {  // can't find the file
            if (version.startsWith("HTTP ")) {  // send a MIME header
              out.write("HTTP 1.0 404 File Not Found\r\n");
              Date now = new Date(  );
              out.write("Date: " + now + "\r\n");
              out.write("Server: JHTTP 1.0\r\n");
              out.write("Content-type: text/html\r\n\r\n");
            } 
            out.write("<HTML>\r\n");
            out.write("<HEAD><TITLE>File Not Found</TITLE>\r\n");
            out.write("</HEAD>\r\n");
            out.write("<BODY>");
            out.write("<H1>HTTP Error 404: File Not Found</h2>\r\n");
            out.write("</BODY></HTML>\r\n");
            out.flush(  );
          }
        }
        else {  // method does not equal "GET"
          if (version.startsWith("HTTP ")) {  // send a MIME header
            out.write("HTTP 1.0 501 Not Implemented\r\n");
            Date now = new Date(  );
            out.write("Date: " + now + "\r\n");
            out.write("Server: JHTTP 1.0\r\n");
            out.write("Content-type: text/html\r\n\r\n"); 
          }       
          out.write("<HTML>\r\n");
          out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n");
          out.write("</HEAD>\r\n");
          out.write("<BODY>");
          out.write("<H1>HTTP Error 501: Not Implemented</h2>\r\n");
          out.write("</BODY></HTML>\r\n");
          out.flush(  );
        }
      }
      catch (IOException e) {
      }
      finally {
        try {
          connection.close(  );        
        }
        catch (IOException e) {} 
      }
      
    } // end while
 
  } // end run
 
  public static String guessContentTypeFromName(String name) {
    if (name.endsWith(".html") || name.endsWith(".htm")) {
      return "text/html";
    }
    else if (name.endsWith(".txt") || name.endsWith(".java")) {
      return "text/plain";
    }
    else if (name.endsWith(".gif")) {
      return "image/gif";
    }
    else if (name.endsWith(".class")) {
      return "application/octet-stream";
    }
    else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
      return "image/jpeg";
    }
    else return "text/plain";
  }  
 
} // end RequestProcessor

This server is functional but still rather austere. Here are a few features you might want to think about adding:

  • A server administration interface
  • Support for CGI programs and/or the Java Servlet API
  • Support for other request methods, such as POST, HEAD, and PUT
  • A log file in the common web log file format
  • Server-side includes and/or Java Server Pages
  • Support for multiple document roots, so that individual users can have their own sites

Finally, you should spend a little time thinking about ways to optimize this server. If you really want to use JHTTP to run a high-traffic site, there are a couple of things you can do to speed this server up. The first and most important is to use a Just-in-Time (JIT) compiler such as HotSpot. JITs can improve program performance by as much as an order of magnitude or more. The second thing you should do is implement smart caching. Keep track of the requests you've received, and store the data from the most frequently requested files in a Hashtable so that they're kept in memory. Use a low-priority thread to update this cache.


1. The command for stopping a program manually depends on your system; under Unix, NT, and many other systems, CTRL-C will do the job. If you are running the server in the background on a Unix system, stop it by finding the server's process ID and killing it with the kill command ( kill pid ).


Visit Built Pro

Contribute to IDR:

To contribute an article to IDR, a click here.

To contact us at IDevResource.com, use our feedback form, or email us.

To comment on the site contact our webmaster.

Promoted by CyberSavvy UK - website promotion experts

All content © Copyright 2000 IDevResource.com, Disclaimer notice

Visit our NEW WTL Section

Visit the IDR Bookstore!

WTL Architecture by Richard Grimes

Visit the IDR Bookstore!

Java COM integration