HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

With J2SE Version 1.4, Java finally has a scalable I/O API. Not that the old API was an absolute failure (Java's tremendous success in the application server market refutes this), but some of the old API's properties led to drastic restrictions. The worst one was the blocking I/O.

To write data over a socket, you have to call the write() method of an associated OutputStream. This call returns only after you've written all the necessary bytes. Given that the send buffers are full and the connection is slow, this might take a while. If your program operates only with a single thread, other connections have to wait, even if they're ready to process write() calls. To work around this problem, you have to associate a thread with each socket. This way one thread can work while another one is blocked due to I/O-related tasks.

Threads aren't as heavyweight as real processes. But, depending on the underlying platform, they're not resource savers either. Each thread uses a certain amount of memory and, apart from that, many threads imply many thread-context switches, which aren't cheap.

Java needed a new API to separate the all-too-happy marriage of socket and thread. This finally happened with the new I/O API (java.nio.*).

In this article I show how you can write a simple Web server with both the new and the old API. Since HTTP, the Web's protocol, is not as trivial as it used to be, I'll realize only some simple central features. Therefore, the programs shown here are neither secure nor protocol-conforming.

Old School Httpd
Let's look at the old-school HTTP server first (see Listing 1). Since I need only a single class for this realization, it's quickly explained. In the main() method, a ServerSocket is instantiated and bound to port 8080. Of course, you'd usually bind a Web server to port 80, but on Unix systems you can only do this with superuser rights. Fortunately, not everyone has them, which is why I chose to use port 8080.

public static void main() throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
for (int i=0; i < Integer.parseInt(args[0]); i++) {
new Httpd(serverSocket);
}
}
Then a number of Httpd objects are created and initialized with the shared ServerSocket. In the Httpd's constructor, I make sure all instances have a meaningful name, set a default protocol, and start the server by executing the start() method of its superclass Thread. This leads to an asynchronous call to the run() method, in which an infinite loop is located.

In this infinite loop, the ServerSocket's blocking accept() method is called. When a client connects to port 8080 of the server, the accept() method will return a socket object. Associated with each socket are an Input- and an OutputStream. Both are used in the following call to the handleRequest() method. In this method the client's request is read, checked, and an appropriate response is sent back. If it's a legitimate request, the requested file is sent back using sendFile(). If it's not, the client will receive a corresponding error message (sendError()). To keep things simple, I won't discuss the specifics of the protocol.

while (true) {
...
socket = serverSocket.accept();
...
handleRequest();
...
socket.close();
}
Now let's think about this realization for a second. Does it perform well? On the whole, yes. Certainly I could optimize the request parsing - the StringTokenizer doesn't have a reputation for being extremely fast. But at least I turned off the TCP delay (slow-start algorithm), which is unsuitable for short connections, and the sending of the file is buffered. But even more important, all threads operate independently of each other. The native, and therefore fast, accept() method decides which thread accepts a new connection. Apart from the ServerSocket object, the threads don't share any resources that might need to be synchronized. This solution is fast but, unfortunately, not very scalable, as threads are definitely a limited resource.

Nonblocking Httpd
Let's look at another solution that uses the new I/O package. It's a bit more complicated and requires the cooperation of different threads. It consists of four classes (see Figure 1):

Figure 1
Figure 1
  1. NIOHttpd (see Listing 2)
  2. Acceptor (see Listing 3)
  3. Connection (see Listing 4)
  4. ConnectionSelector (see Listing 5)
NIOHttpd basically launches the server. Just as in Httpd, a server socket is bound to port 8080. The important difference is that this time I use a java.nio.channels.ServerSocketChannel instead of a ServerSocket. I need to open the channel with a factory method before binding it explicitly to the port using the bind() method. Then I instantiate a ConnectionSelector and an Acceptor. Doing so, each ConnectionSelector is registered with an Acceptor. In addition, the Acceptor is provided with the ServerSocketChannel.

public static void main() throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
ConnectionSelector cs = new ConnectionSelector();
new Acceptor(ssc, cs);
}
Figure 2 depicts the concurrent execution of the Acceptor and ConnectionSelector threads. To understand the interaction between the two threads, let's first take a closer look at the Acceptor. Its task is to accept incoming connections and register them with the ConnectionSelector. Already in the constructor, the superclass's start() method is called as the required infinite loop is in the run() method. In this loop a blocking accept() method is called that will eventually return a socket object - almost exactly as in Httpd. But this time it's a ServerSocketChannel's accept() method, not a ServerSocket's. Finally, with the obtained socketChannel as an argument, a connection object is created and registered with the ConnectionSelector using its queue() method.

Figure 2
Figure 2

while (true) {
...
socketChannel = serverSocketChannel.accept();
connectionSelector.queue(new Connection(socketChannel));
...
}
To summarize: the Acceptor can only accept and register connections with a ConnectionSelector in an endless loop.

Like Acceptor, the ConnectionSelector is also a thread. In its constructor a queue is instantiated and a java.nio.channels.Selector is opened using the factory method Selector.open(). The Selector is probably the most important part of the server. It allows me to register connections and to obtain a list of those connections that are ready for reading or writing.

After the start() method is called in the constructor, the endless loop in run() is executed. In this loop I call the Selector's select() method. This method blocks until either one of the registered connections is ready for I/O operations or the Selector's wakeup() method is called.

while (true) {
...
int i = selector.select();
registerQueuedConnections();
...
// handle connections...
}
It's crucial to understand that while the ConnectionSelector thread executes select(), no Acceptor thread can register connections with the Selector, because the corresponding methods are synchronized. Therefore I use a queue, to which the Acceptor thread adds connections as needed.

public void queue(Connection connection) {
synchronized (queue) {
queue.add(connection);
}
selector.wakeup();
}
Right after queuing a connection, the Acceptor calls the Selector's wakeup() method. This causes the ConnectionSelector thread to resume execution and return from the blocking select() call. Since the Selector is not blocked anymore, the ConnectionSelector can now register the connection from the queue. It happens the following way in registerQueuedConnections():

if (!queue.isEmpty()) {
synchronized (queue) {
while (!queue.isEmpty()) {
Connection connection =
(Connection)queue.remove(queue.size()-1);
connection.register(selector);
}
}
}
Selector Registration Using Keys
At this point I have to focus on the Connection's register() method. Until now I've talked about a connection that's registered with a Selector. This is a bit simplified. Instead, a java.nio.channels.SocketChannel object is registered with a Selector, but only for specific I/O operations. After registration, a java.nio.channels.SelectionKey is returned. This key can be associated with arbitrary objects using its attach() method. To get a connection with a key, I attach the Connection object to the key. By doing so I can indirectly obtain a Connection from the Selector.

public void register(Selector selector)
throws IOException {
key = socketChannel.register(selector,
SelectionKey.OP_READ);
key.attach(this);
}
Getting back to the ConnectionSelector, the select() method's return value indicates how many connections are ready for I/O operations. If the return value is zero, I skip the rest and return to the select() call. Otherwise, I iterate over the selection keys, which I obtained as Set by calling selectedKeys(). From the keys I get the previously attached Connection objects and call their readRequest() or writeResponse() methods. Which method is actually called depends on whether the connections were registered for read or write operations.

This eventually brings me back to the Connection class. It represents the connection and handles all the protocol's specifics. In its constructor the provided SocketChannel is set to nonblocking mode. This is essential for the server. Then a couple of default values are set and the buffer requestLineBuffer is allocated. As the allocation of direct buffers is somewhat expensive and I'm using a new buffer for each connection, I use java.nio.ByteBuffer.allocate() instead of ByteBuffer.allocateDirect(). If I reuse the buffer, a direct buffer could prove to be more efficient.

public Connection(SocketChannel socketChannel)
throws IOException {
this.socketChannel = socketChannel;
...
socketChannel.configureBlocking(false);
requestLineBuffer = ByteBuffer.allocate(512);
...
}
After all initializations are done and the SocketChannel is ready for reading, the readRequest() method is called by the ConnectionSelector. Using socketChannel.read(requestLineBuffer), all available bytes are read into the buffer. If the full line can't be read, I return to the calling ConnectionSelector and thus allow another connection to take over. However, if the whole line is read, it's time to interpret the request just as I did in Httpd. If it's a legitimate request, I create a java.nio.Channels.FileChannel for the requested file and call the method prepareForResponse().

private void prepareForResponse() throws IOException {
StringBuffer responseLine = new StringBuffer(128);
...
responseLineBuffer = ByteBuffer.wrap(
responseLine.toString().getBytes("ASCII")
);
key.interestOps(SelectionKey.OP_WRITE);
key.selector().wakeup();
}
prepareForResponse() builds the response line and (if necessary) headers as well as error messages, and writes this data to responseLineBuffer. This ByteBuffer is a thin wrapper around a byte array that was created using the factory method ByteBuffer.wrap(byte[]). After generating the data that I want to write, I need to notify the ConnectionSelector that from now on I want to write data rather than read it. This is achieved by calling the selection key's method interestedOps(SelectionKey.OP_WRITE). To guarantee that the selector quickly realizes the connection's change of interest, I call its wakeup() method.

Now the ConnectionSelector calls the connection's writeResponse() method. First, the responseLineBuffer is written to the socket channel. If the entire content of the buffer can be written, and if I still have to send the requested file, I call the transferTo() method of the FileChannel that I opened before. transferTo() potentially transfers data very efficiently from a file to a channel. How efficiently depends on the underlying operating system. In any case, only as many bytes are transferred as can be written to the target channel without blocking. To be on the safe side and to ensure fairness between connections, I set an upper limit of 64KB.

If all data is transferred, close() does the clean-up work. Here, the deregistering of the Connection is important. This is achieved by calling the selection key's cancel() method.

public void close() {
...
if (key != null) key.cancel();
...
}
Again I wonder: How does this realization perform? And again I can answer: it performs well.

In principle, one Acceptor and one ConnectionSelector are sufficient to keep an arbitrary number of connections open. Thus this realization shines in the category of scalability. But as the two threads have to communicate through the synchronized queue() method, they might block each other. There are two ways out of this dilemma:

  1. A better realization of the queue
  2. Multiple Acceptor/ConnectionSelector pairs
One solution could be realized by using a LinkedQueue (see Concurrent Programming in Java by Doug Lea). This data structure is synchronized with two independent locks - one for the head and one for the tail. This ensures that adding and removing threads don't block each other. Only if the queue is empty is there a possibility of mutual blocking, but this can be avoided with an extra check.

In comparison to this elegant approach, my second solution qualifies for the "brute force" category. The load is balanced with multiple Acceptor/ConnectionSelector pairs and the synchronization problem isn't solved, but is somewhat reduced. Unfortunately, this causes additional costs for context switches. Compared to Httpd, fewer threads are needed.

One disadvantage to NIOHttpd, in comparison to Httpd, is that for each request, a new Connection object with buffers is created. This leads to an additional CPU cycle burning caused by the garbage collector. How large these extra costs are depends on the VM. However, Sun doesn't tire of emphasizing that with Hotspot, short-lived objects are not a problem anymore.

Comparative Number Games
How much better does NIOHttpd scale than Httpd? Let's play with a couple of numbers, but before I go into media res, be warned: the formulas and the numbers I'm going to find are highly speculative. Only the concepts' performance is estimated. Important context variables like thread synchronization, context switches, paging, hard disk speed, and caches are not considered.

First I estimate how long it takes to process r simultaneous requests for files with size s bytes, if the client bandwidth is b bytes/second. In the case of Httpd, this obviously depends directly on the number of threads t, as only t requests can be processed at a time. I assume that a corresponding formula looks like Formula 1. c is the constant cost for parsing, etc., that has to be paid for every request. In addition, I assume I can read data faster from the disk than I can write it to the socket, my bandwidth is greater than the sum of the clients' bandwidth, and the CPU is not fully utilized. Therefore the server-side bandwidth, caches, and hard disk speed are not part of the equation.

Formula 1
Formula 1

However, NIOHttpd is not dependent on t. The transfer time l depends mostly on the client bandwidth b, the size of the file s, and the previously mentioned constant costs c. This leads to Formula 2, which estimates the minimum transfer time for NIOHttpd.

Formula 2
Formula 2

The quotient d (see Formula 3) is of interest since it measures the relationship of the performances of NIOHttpd and Httpd.

Formula 3
Formula 3

After closer examination (...and some rows of data), it becomes apparent that if s, b, t, and c are constant, d grows toward a limit. This limit can be easily calculated using Formula 4, which measures the limit of d for r -> .

Formula 4
Formula 4

Thus, besides the number of threads and constant costs, the connection's length s/b has tremendous influence on d. The longer the connection exists, the smaller d is, and the advantage of NIOHttpd compared to Httpd is greater. Table 1 and Figure 3 show that NIOHttpd can be 126 times faster than Httpd, given that c=10ms, t=100, s=1mb, and b=8kb/s. NIOHttpd has a big advantage if the connection stays open for a long time. If the connection is short, e.g., in a local 100Mb network, the advantage is only 10% provided the files are large. If the files are small, the difference won't be detectable.

Figure 3
Figure 3

Table 1
Table 1

In these calculations it's assumed that the constant costs of NIOHttpd and Httpd are about the same and no new costs are introduced by the different ways the servers have been implemented. As mentioned before, this comparison only holds under ideal conditions.

This is sufficient, however, to give you the idea that either concept might be beneficial. It should be noted that most Web files are small, but HTTP-1.1-clients try to keep the connection open as long as possible (with a keep-alive or persistent connection). Often, connections that will never again transfer any data are kept open. In a server with one thread per connection this leads to an incredible waste of resources. So, especially for HTTP servers, the scalability can be increased dramatically by using the new I/O API.

Conclusion
With the new I/O API you can build highly scalable servers. In comparison to the old API, it's a bit more complex and requires a better understanding of multithreading and synchronization. Also, the documentation needs improvement. But if you've gotten over these hurdles, the new API proves to be a useful and necessary improvement of the Java 2 platform.

References

  • HTTP 1.1: www.w3.org/Protocols/rfc2616/rfc2616.html
  • Lea, D. (1999). Concurrent Programming in Java: Design Principles and Patterns. Second Edition. Addison-Wesley. http://gee.cs.oswego.edu/dl/cpj

    Author Bio
    Hendrik Schreiber works as senior consultant for innoQ Deutschland GmbH (www.innoq.com) in Ratingen, Germany. His main area of interest is the architecture of modern Java-based solutions using J2EE. He is a coauthor of Java Server and Servlets: Building Portable Web Applications, published by Addison-Wesley. [email protected]

    	
    
    
    Listing 1: Simple web server that uses the old I/O API
    
    package com.innoq.httpd;
    
    import java.io.*;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.NoSuchElementException;
    import java.util.StringTokenizer;
    
    /**
     * Simple multithreaded Http daemon that exclusively uses
     * the 'old ' I/O API.
     *
     * @author [email protected]
    */
    public class Httpd extends Thread {
    
       private static int _no; // instance counter
       private ServerSocket serverSocket;
       private byte[] buf = new byte[1024 * 8];
       private String protocol;
       private InputStream in;
       private OutputStream out;
       private String uri;
    
       /**
        * Starts a Httpd thread.
        */
       public Httpd(ServerSocket serverSocket)
             throws IOException {
          super("Httpd " + (_no++));
          this.serverSocket = serverSocket;
          // default protocol-version
          protocol = "HTTP/0.9";
          start();
       }
    
    
       /**
        * Waits for incoming connections and then calls
        * handleRequest.
        */
       public void run() {
          Socket socket = null;
          while (true) {
             try {
                socket = serverSocket.accept();
                // disable Nagle's algorithm
                // for better performance
                socket.setTcpNoDelay(true);
                in = socket.getInputStream();
                out = socket.getOutputStream();
                handleRequest();
             } catch (Exception e) {
                // something went wrong...
                e.printStackTrace();
             } finally {
                // clean-up
                if (socket != null) {
                   try {
                      // this also closes
                      // the in- and outputstream.
                      socket.close();
                   } catch (IOException ioe) {
                      /* ignore */
                   }
                }
                socket = null;
             }
          }
       }
    
       /**
        * Reads the request and sends either the file
        * or an error message back to the client.
        */
       private void handleRequest() throws IOException {
          try {
             // read only 512 bytes - the line should
             // not be longer anyway
             int length = in.read(buf, 0, 512);
             if (length == 512) {
                sendError(414, "Request URI too long.");
                return;
             }
             // we assume ASCII as character set,
             // therefore we can use the deprecated but
             // faster String constructor.
             String requestline = new String(buf, 0, 0, length);
             StringTokenizer st = new
                StringTokenizer(requestline, " \r\n");
             String method = st.nextToken();
             uri = st.nextToken();
             if (st.hasMoreTokens()) {
                protocol = st.nextToken();
             }
             File file = new File(uri.substring(1));
             if (!method.equals("GET")) {
                sendError(405, "Method " + method
                   + " is not supported.");
             } else if (!file.exists() || file.isDirectory()) {
                sendError(404, "Resource " + uri
                   + " was not found.");
             } else if (!file.canRead()) {
                sendError(403, "Forbidden: " + uri);
             } else {
                sendFile(file);
             }
          } catch (NoSuchElementException nsee) {
             // we didn't read enough tokens
             sendError(400, "Bad request.");
          } catch (Exception e) {
             try {
                sendError(500, "Internal Server Error.");
             } catch (IOException ioe) {
                /* ignore */
             }
          }
       }
    
       /**
        * Sends an error message to the client.
        */
       private void sendError(int httpStatus,
             String httpMessage) throws IOException {
          StringBuffer errorMessage = new StringBuffer(128);
          if (!protocol.equals("HTTP/0.9")) {
             errorMessage.append("HTTP/1.0 " + httpStatus
                + " " + httpMessage + "\r\n\r\n");
          }
          errorMessage.append("<HTML><BODY><H1>"
             + httpMessage + "</H1></BODY></HTML>");
          out.write(errorMessage.toString().getBytes("ASCII"));
          out.flush();
       }
    
       /**
        * Sends the requested file to the client.
        */
       private void sendFile(File file) throws IOException {
          InputStream filein = null;
          try {
             filein = new FileInputStream(file);
             if (!protocol.equals("HTTP/0.9")) {
                // write status code and header
                out.write(("HTTP/1.0 200 OK\r\nContent-Type: "
                   + Httpd.guessContentType(uri)
                   + "\r\n\r\n").getBytes("ASCII"));
             }
             int length = 0;
             while ((length = filein.read(buf)) != -1) {
                out.write(buf, 0, length);
             }
             out.flush();
          } finally {
             if (filein != null)
                try {
                   filein.close();
                } catch (IOException ioe) {
                   /* ignore */
                }
          }
       }
    
       /**
        * Returns the content-type of a given resource.
        */
       public static String guessContentType(String uri) {
          // Hack. This should be done with a
          // configuration file.
          uri = uri.toLowerCase();
          if (uri.endsWith(".html") || uri.endsWith(".htm")) {
             return "text/html";
          } else if (uri.endsWith(".txt")) {
             return "text/plain";
          } else if (uri.endsWith(".jpg")
                || uri.endsWith(".jpeg")) {
             return "image/jpeg";
          } else if (uri.endsWith(".gif")) {
             return "image/gif";
          } else {
             return "unknown";
          }
       }
    
       /**
        * Starts the Http daemon with n threads. The first
        * argument is n.
        */
       public static void main(String[] args)
             throws IOException {
          ServerSocket serverSocket = new ServerSocket(8080);
          for (int i=0; i < Integer.parseInt(args[0]); i++) {
             new Httpd(serverSocket);
          }
       }
    }
    
    Listing 2: The class NIOHttpd serves as a launcher 
    
    for the actual workers Acceptor (Listing 3) and ConnectionSelector (Listing 5)
    package com.innoq.httpd; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.ServerSocketChannel; /** * Http daemon that uses the new I/O API. Connections are * accepted by {@link Acceptor} threads. They are then * registered as {@link Connection}s with a {@link * java.nio.channels.Selector}. {@link ConnectionSelector} * threads call the connection's work-methods * until the connections de-register themselves. * * @author [email protected] * @see Acceptor * @see ConnectionSelector * @see Connection */ public class NIOHttpd { /** * Returns the content-type of a given resource. */ public static String guessContentType(String uri) { // Hack. This should be done with a // configuration file. uri = uri.toLowerCase(); if (uri.endsWith(".html") || uri.endsWith(".htm")) { return "text/html"; } else if (uri.endsWith(".txt")) { return "text/plain"; } else if (uri.endsWith(".jpg") || uri.endsWith(".jpeg")) { return "image/jpeg"; } else if (uri.endsWith(".gif")) { return "image/gif"; } else { return "unknown"; } } /** * Starts the Http daemon with 2n threads. The * first argument is n. */ public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind( new InetSocketAddress(8080) ); for (int i = 0; i < Integer.parseInt(args[0]); i++) { ConnectionSelector connectionSelector = new ConnectionSelector(); Acceptor acceptor = new Acceptor( serverSocketChannel, connectionSelector ); } } } Listing 3: Acceptor accepts incoming connections
    and registers them with the ConnectionSelector (Listing 5)
    package com.innoq.httpd; import java.net.SocketChannel; import java.nio.channels.ServerSocketChannel; /** * Acceptors call the accept() method of a {@link * ServerSocketChannel}, instantiate a {@link Connection} * object with the accepted connection, and register this * connection with the {@link ConnectionSelector}. * * @author [email protected] */ class Acceptor extends Thread { private static int _no; // Instance counter private ServerSocketChannel serverSocketChannel; private ConnectionSelector connectionSelector; /** * Starts this Acceptor thread. */ public Acceptor(ServerSocketChannel serverSocketChannel, ConnectionSelector connectionSelector) { super("Acceptor " + (_no++)); this.serverSocketChannel = serverSocketChannel; this.connectionSelector = connectionSelector; start(); } /** * Accepts connections and regsiters them with * {@link ConnectionSelector#queue(Connection)}. */ public void run() { while (true) { SocketChannel socketChannel = null; try { socketChannel = serverSocketChannel.accept(); connectionSelector.queue( new Connection(socketChannel) ); } catch (Exception e) { e.printStackTrace(); // clean-up, if necessary if (socket != null) { try { socket.close(); } catch (Exception ee) { /* ignore */ } } } } } } Listing 4: Connection represents the connection
    and deals with the protocol's specifics
    package com.innoq.httpd; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.StringTokenizer; import java.util.NoSuchElementException; /** * Represents the connection, during which the request is * read and a response is written. * * @author [email protected] * @see ConnectionSelector * @see Acceptor */ class Connection { private SocketChannel socketChannel; private ByteBuffer requestLineBuffer; private ByteBuffer responseLineBuffer; private int endOfLineIndex; private SelectionKey key; private FileChannel fileChannel; private long filePos; private long fileLength; private int httpStatus; private String httpMessage; private String uri; private String protocol; /** * Initializes this connection with a SocketChannel. * Here also the channel is set to non-blocking mode. */ public Connection(SocketChannel socketChannel) throws IOException { this.socketChannel = socketChannel; // disable Nagle's algorithm for better performance socketChannel.socket().setTcpNoDelay(true); // the channel shall _not_ block socketChannel.configureBlocking(false); requestLineBuffer = ByteBuffer.allocate(512); // default http status code: OK httpStatus = 200; // default http status message httpMessage = "OK"; // default protocol version protocol = "HTTP/0.9"; } /** * Register this connection with the provided Selector. * Initially we are only interested in read operations * ({@link SelectionKey.OP_READ}). * This method is called by {@link ConnectionSelector}. */ public void register(Selector selector) throws IOException { key = socketChannel.register(selector, SelectionKey.OP_READ); // deposit this connection in its selection key key.attach(this); } /** * Reads the request. If somethings goes wrong, an error * code is set. * If the whole request read, * {@link #prepareForResponse()} is called. */ public void readRequest() throws IOException { try { if (!requestLineBuffer.hasRemaining()) { setError(414, "Request URI too long."); prepareForResponse(); return; } socketChannel.read(requestLineBuffer); if (!isRequestLineRead()) { return; } requestLineBuffer.flip(); byte[] b = new byte[endOfLineIndex]; requestLineBuffer.get(b); String requestline = new String(b, 0); StringTokenizer st = new StringTokenizer(requestline, " \r\n"); String method = st.nextToken(); uri = st.nextToken(); File file = new File(uri.substring(1)); if (st.hasMoreTokens()) { protocol = st.nextToken(); } if (!method.equals("GET")) { setError(405, "Method " + method + " is not supported."); } else if (!file.exists() || file.isDirectory()) { setError(404, "Resource " + uri + " was not found."); } else if (!file.canRead()) { setError(403, "Forbidden: " + uri); } else { fileLength = file.length(); fileChannel = new FileInputStream(file).getChannel(); } prepareForResponse(); } catch (NoSuchElementException nsee) { // we didn't read enough tokens setError(400, "Bad request."); } catch (Exception e) { // something else went wrong setError(500, "Internal Server Error."); prepareForResponse(); e.printStackTrace(); } } /** * Creates a buffer that contains the response line, * headers and in case of an error an HTML message. */ private void prepareForResponse() throws IOException { StringBuffer responseLine = new StringBuffer(128); // write response line if Http >= 1.0 if (!protocol.equals("HTTP/0.9")) { responseLine.append("HTTP/1.0 " + httpStatus + " " + httpMessage + "\r\n"); // In case of an error, we don't need headers if (httpStatus != 200) { responseLine.append("\r\n"); } else { // Header for the file responseLine.append("Content-Type: " + NIOHttpd.guessContentType(uri) + "\r\n\r\n"); } } if (httpStatus != 200) { // Error message for the user responseLine.append("<HTML><BODY><H1>" + httpMessage + "</H1></BODY></HTML>"); } responseLineBuffer = ByteBuffer.wrap( responseLine.toString().getBytes("ASCII") ); key.interestOps(SelectionKey.OP_WRITE); key.selector().wakeup(); } /** * Inidcates, whether the request line was read. */ private boolean isRequestLineRead() { for (; endOfLineIndex < requestLineBuffer.limit(); endOfLineIndex++) { if (requestLineBuffer.get(endOfLineIndex) == '\r') return true; } return false; } /** * Writes the responseLineBuffer and - if necessary - * the requested file to the client. After all data * has been written, the selection key is cancelled and * the connection is closed. */ public void writeResponse() throws IOException { // write the response buffer if (responseLineBuffer.hasRemaining()) { socketChannel.write(responseLineBuffer); } // if the complete response buffer has been written, // we are either done (in case of an error) or // we need to send the file. if (!responseLineBuffer.hasRemaining()) { if (httpStatus != 200) { close(); } else { filePos += fileChannel.transferTo( filePos, (int)Math.min(64*1024, fileLength-filePos), socketChannel ); if (filePos == fileLength) { close(); } } } } /** * Sets an error. */ private void setError(int httpStatus, String httpMessage) { this.httpStatus = httpStatus; this.httpMessage = httpMessage; } /** * Cancels the selection key and closes all open * channels. */ public void close() { try { if (key != null) key.cancel(); } catch (Exception e) { /* ignore */ } try { if (socketChannel != null) socketChannel.close(); } catch (Exception e) { /* ignore */ } try { if (fileChannel != null) fileChannel.close(); } catch (Exception e) { /* ignore */ } } } Listing 5: ConnectionSelector selects the
    connections, which are ready for I/O operations
    package com.innoq.httpd; import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Calls the work-methods of registered {@link Connection}s, * if the corresponding channels are ready for the specified * operations. * * @author [email protected] */ class ConnectionSelector extends Thread { private static int _no; // instance counter private Selector selector; private List queue; /** * Instantiates and starts this ConnectionSelector. The * {@link Selector} is instantiated, too. */ public ConnectionSelector() throws IOException { super("ConnectionSelector " + (_no++)); selector = Selector.open(); queue = new ArrayList(); start(); } /** * Queues a connection and calls {@link * Selector#wakeup()}, so that a {@link SelectionKey} can * be created and the connection can be registered. * * @see #registerQueuedConnections() */ public void queue(Connection connection) { synchronized (queue) { queue.add(connection); } // make sure that select() wakes up and the queued // connections is taken care of. selector.wakeup(); } /** * Registers all queued connections with the Selector. * * @see Connection#register(Selector) */ private void registerQueuedConnections() throws IOException { // the synchronized block is a bottleneck, therefore // it should be avoided if possible if (!queue.isEmpty()) { synchronized (queue) { while (!queue.isEmpty()) { Connection connection = (Connection)queue.remove(queue.size()-1); connection.register(selector); } } } } /** * Calls {@link Selector#select()} in an inifnite loop. * When the call to select() returns, all queued * connections are registered with the Selector. Then * {@link Connection#readRequest()} or {@link * Connection#writeResponse()} respectively * are called for connections that are ready for read or * write operations. */ public void run() { while (true) { try { int i = selector.select(); registerQueuedConnections(); if (i > 0) { Set set = selector.selectedKeys(); Iterator connectionIterator = set.iterator(); while (connectionIterator.hasNext()) { SelectionKey key = (SelectionKey)connectionIterator.next(); Connection connection = (Connection)key.attachment(); try { if (key.interestOps() == SelectionKey.OP_READ) { connection.readRequest(); } else { connection.writeResponse(); } } catch (IOException ioe) { connection.close(); } catch (Throwable t) { connection.close(); t.printStackTrace(); } } } } catch (Throwable t) { t.printStackTrace(); } } } }

    All Rights Reserved
    Copyright ©  2004 SYS-CON Media, Inc.
      E-mail: [email protected]

    Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.