12.3 Buffers

     

In Chapter 4, I recommended that you always buffer your streams. Almost nothing has a greater impact on the performance of network programs than a big enough buffer. In the new I/O model, however, you're no longer given the choice. All I/O is buffered. Indeed the buffers are fundamental parts of the API. Instead of writing data onto output streams and reading data from input streams, you read and write data from buffers. Buffers may appear to be just an array of bytes as in buffered streams. However, native implementations can connect them directly to hardware or memory or use other, very efficient implementations .

From a programming perspective, the key difference between streams and channels is that streams are byte-based while channels are block-based . A stream is designed to provide one byte after the other, in order. Arrays of bytes can be passed for performance. However, the basic notion is to pass data one byte at a time. By contrast, a channel passes blocks of data around in buffers. Before bytes can be read from or written to a channel, the bytes have to be stored in a buffer, and the data is written or read one buffer at a time.

The second key difference between streams and channels/buffers is that channels and buffers tend to support both reading and writing on the same object. This isn't always true. For instance, a channel that points to a file on a CD-ROM can be read but not written. A channel connected to a socket that has shutdown input could be written but not read. If you try to write to a read-only channel or read from a write-only channel, an UnsupportedOperationException will be thrown. However, more often that not network programs can read from and write to the same channels.

Without worrying too much about the underlying details (which can vary hugely from one implementation to the next , mostly a result of being tuned very closely to the host operating system and hardware), you can think of a buffer as a fixed- size list of elements of a particular, normally primitive data type, like an array. However, it's not necessarily an array behind the scenes. Sometimes it is; sometimes it isn't. There are specific subclasses of Buffer for all of Java's primitive data types except boolean: ByteBuffer , CharBuffer , ShortBuffer , IntBuffer , LongBuffer , FloatBuffer , and DoubleBuffer . The methods in each subclass have appropriately typed return values and argument lists. For example, the DoubleBuffer class has methods to put and get doubles. The IntBuffer class has methods to put and get ints. The common Buffer superclass only provides methods that don't need to know the type of the data the buffer contains. (The lack of primitive-aware generics really hurts here.) Network programs use ByteBuffer almost exclusively, although occasionally one program might use a view that overlays the ByteBuffer with one of the other types.

Besides its list of data, each buffer tracks four key pieces of information. All buffers have the same methods to set and get these values, regardless of the buffer's type:


position

The next location in the buffer that will be read from or written to. Like most indexes in Java, this starts counting at 0 and has a maximum value one less than the size of the buffer. It can be set or gotten with these two methods:

 public final int    position( ) public final Buffer position(int newPosition) 


capacity

The maximum number of elements the buffer can hold. This is set when the buffer is created and cannot be changed thereafter. It can be read with this method:

 public final int  capacity( ) 


limit

The last location in the buffer that can hold data. You cannot write or read past this point without changing the limit, even if the buffer has more capacity. It is set and gotten with these two methods:

 public final int    limit( ) public final Buffer limit(int newLimit) 


mark

A client-specified index in the buffer. It is set at the current position by invoking the mark() method. The current position is set to the marked position by invoking reset( ) :

 public final Buffer mark( ) public final Buffer reset( ) 

No, I can't explain why these methods (and several similar methods in the java.nio packages) don't follow the standard Java get Foo () / set Foo ( ) naming convention. Blame it on the smoke-filled chat rooms in the Java Community Process.


Unlike reading from an InputStream , reading from a buffer does not actually change the buffer's data in any way. It's possible to set the position either forwards or backwards so you can start reading from a particular place in the buffer. Similarly, a program can adjust the limit to control the end of the data that will be read. Only the capacity is fixed.

The common Buffer superclass also provides a few other methods that operate by reference to these common properties.

The clear( ) method "empties" the buffer by setting the position to zero and the limit to the capacity. This allows the buffer to be completely refilled:

 public final Buffer clear( ) 

However, the clear( ) method does not remove the old data from the buffer. It's still present and could be read using absolute get methods or changing the limit and position again.

The rewind( ) method sets the position to zero, but does not change the limit:

 public final Buffer rewind( ) 

This allows the buffer to be reread.

The flip( ) method sets the limit to the current position and the position to zero:

 public final Buffer flip( ) 

It is called when you want to drain a buffer you've just filled.

Finally, there are two methods that return information about the buffer but don't change it. The remaining() method returns the number of elements in the buffer between the current position and the limit. The hasRemaining( ) method returns true if the number of remaining elements is greater than zero:

 public final int     remaining( ) public final boolean hasRemaining( ) 

12.3.1 Creating Buffers

The buffer class hierarchy is based on inheritance but not really on polymorphism, at least not at the top level. You normally need to know whether you're dealing with an IntBuffer or a ByteBuffer or a CharBuffer or something else. You write code to one of these subclasses, not to the common Buffer superclass. However, at the level of IntBuffer / ByteBuffer / CharBuffer , etc., the classes are polymorphic. These classes are abstract too, and you use a factory method to retrieve an implementation-specific subclass such as java.nio.HeapByteBuffer . However, you only treat the actual object as an instance of its superclass, ByteBuffer in this case.

Each typed buffer class has several factory methods that create implementation-specific subclasses of that type in various ways. Empty buffers are normally created by allocate methods. Buffers that are prefilled with data are created by wrap methods. The allocate methods are often useful for input while the wrap methods are normally used for output.

12.3.1.1 Allocation

The basic allocate( ) method simply returns a new, empty buffer with a specified fixed capacity. For example, these lines create byte and int buffers, each with a size of 100:

 ByteBuffer buffer1 = ByteBuffer.allocate(100); IntBuffer  buffer2 = IntBuffer.allocate(100); 

The cursor is positioned at the beginning of the buffer; that is, the position is 0. A buffer created by allocate( ) will be implemented on top of a Java array, which can be accessed by the array( ) and arrayOffset( ) methods. For example, you could read a large chunk of data into a buffer using a channel and then retrieve the array from the buffer to pass to other methods:

 byte[] data1 = buffer1.array( ); int[]  data2 = buffer2.array( ); 

The array( ) method does expose the buffer's private data, so use it with caution. Changes to the backing array are reflected in the buffer and vice versa. The normal pattern here is to fill the buffer with data, retrieve its backing array, and then operate on the array. This isn't a problem as long as you don't write to the buffer after you've started working with the array.

12.3.1.2 Direct allocation

The ByteBuffer class (but not the other buffer classes) has an additional allocateDirect() method that may not create a backing array for the buffer. The VM may implement a directly allocated ByteBuffer using direct memory access to the buffer on an Ethernet card, kernel memory, or something else. It's not required, but it's allowed, and this can improve performance for I/O operations. From an API perspective, the allocateDirect( ) is used exactly like allocate( ) :

 ByteBuffer buffer1 = ByteBuffer.allocateDirect(100); 

Invoking array( ) and arrayOffset() on a direct buffer will throw an UnsupportedOperationException . Direct buffers may be faster on some virtual machines, especially if the buffer is large ( roughly a megabyte or more). However, direct buffers are more expensive to create than indirect buffers, so they should only be allocated when the buffer is expected to be around for awhile. The details are highly VM-dependent. As is generally true for most performance advice, you probably shouldn't even consider using direct buffers until measurements prove performance is an issue.

12.3.1.3 Wrapping

If you already have an array of data that you want to output, you'll normally wrap a buffer around it, rather than allocating a new buffer and copying its components into the buffer one at a time. For example:

 byte[] data = "Some data".getBytes("UTF-8"); ByteBuffer buffer1 = ByteBuffer.wrap(data); char[] text = "Some text".toCharArray( ); CharBuffer buffer2 = CharBuffer.wrap(text); 

Here, the buffer contains a reference to the array, which serves as its backing array. Buffers created by wrapping are never direct. Again, changes to the array are reflected in the buffer and vice versa, so don't wrap the array until you're finished with it.

12.3.2 Filling and Draining

Buffers are designed for sequential access. Besides its list of data, each buffer has a cursor indicating its current position. The cursor is an int that counts from zero to the number of elements in the buffer; the cursor is incremented by one when an element is read from or written to the buffer. It can also be positioned manually. For example, suppose you want to reverse the characters in a string. There are at least a dozen different ways to do this, including using string buffers, [3] char[] arrays, linked lists, and more. However, if we were to do it with a CharBuffer , we might begin by filling a buffer with the data from the string:

[3] By the way, a StringBuffer is not a buffer in the sense of this section. Aside from the very generic notion of buffering, it has nothing in common with the classes being discussed here.

 String s = "Some text"; CharBuffer buffer = CharBuffer.wrap(s); 

We can only fill the buffer up to its capacity. If we tried to fill it past its initially set capacity, the put( ) method would throw a BufferOverflowException . Similarly, if we now tried to get( ) from the buffer, there'd be a BufferOverflowException . Before we can read the data out again, we need to flip the buffer:

 buffer.flip( ); 

This repositions the cursor at the start of the buffer. We can drain it into a new string:

 String result = ""; while (buffer.hasRemaining( )) {   result+= buffer.get( ); } 

Buffer classes also have absolute methods that fill and drain at specific positions within the buffer without updating the cursor. For example, ByteBuffer has these two:

 public abstract byte       get(int index) public abstract ByteBuffer put(int index, byte b) 

These both throw IndexOutOfBoundsException if you try to access a position past the limit of the buffer. For example, using absolute methods, you could reverse a string into a buffer like this:

 String s = "Some text"; CharBuffer buffer = CharBuffer.allocate(s.length( )); for (int i = 0; i < s.length( ); i++) {   buffer.put(s.length( ) - i - 1, s.charAt(i)); } 

12.3.3 Bulk Methods

Even with buffers it's often faster to work with blocks of data rather than filling and draining one element at a time. The different buffer classes have bulk methods that fill and drain an array of their element type.

For example, ByteBuffer has put( ) and get( ) methods that fill and drain a ByteBuffer from a preexisting byte array or subarray:

 public ByteBuffer get(byte[] dst, int offset, int length) public ByteBuffer get(byte[] dst) public ByteBuffer put(byte[] array, int offset, int length) public ByteBuffer put(byte[] array) 

These put methods insert the data from the specified array or subarray, beginning at the current position. The get methods read the data into the argument array or subarray beginning at the current position. Both put and get increment the position by the length of the array or subarray. The put methods throw a BufferOverflowException if the buffer does not have sufficient space for the array or subarray. The get methods throw a BufferUnderflowException if the buffer does not have enough data remaining to fill the array or subarrray. These are runtime exceptions.

12.3.4 Data Conversion

All data in Java ultimately resolves to bytes. Any primitive data type int , double , float , etc.can be written as bytes. Any sequence of bytes of the right length can be interpreted as a primitive datum. For example, any sequence of four bytes corresponds to an int or a float (actually both, depending on how you want to read it). A sequence of eight bytes corresponds to a long or a double . The ByteBuffer class (and only the ByteBuffer class) provides relative and absolute put methods that fill a buffer with the bytes corresponding to an argument of primitive type (except boolean) and relative and absolute get methods that read the appropriate number of bytes to form a new primitive datum:

 public abstract char       getChar( ) public abstract ByteBuffer putChar(char value) public abstract char       getChar(int index) public abstract ByteBuffer putChar(int index, char value) public abstract short      getShort( ) public abstract ByteBuffer putShort(short value) public abstract short      getShort(int index) public abstract ByteBuffer putShort(int index, short value) public abstract int        getInt( ) public abstract ByteBuffer putInt(int value) public abstract int        getInt(int index) public abstract ByteBuffer putInt(int index, int value) public abstract long       getLong( ) public abstract ByteBuffer putLong(long value) public abstract long       getLong(int index) public abstract ByteBuffer putLong(int index, long value) public abstract float      getFloat( ) public abstract ByteBuffer putFloat(float value) public abstract float      getFloat(int index) public abstract ByteBuffer putFloat(int index, float value) public abstract double     getDouble( ) public abstract ByteBuffer putDouble(double value) public abstract double     getDouble(int index) public abstract ByteBuffer putDouble(int index, double value) 

In the world of new I/O, these methods do the job performed by DataOutputStream and DataInputStream in traditional I/O. These methods do have an additional ability not present in DataOutputStream and DataInputStream . You can choose whether to interpret the byte sequences as big-endian or little-endian ints, floats, doubles, etc. By default, all values are read and written as big-endian; that is, most significant byte first. The two order( ) methods inspect and set the buffer's byte order using the named constants in the ByteOrder class. For example, you can change the buffer to little-endian interpretation like so:

 if (buffer.order( ).equals(ByteOrder.BIG_ENDIAN)) {   buffer.order(ByteOrder.LITLLE_ENDIAN); } 

Suppose instead of a chargen protocol, you want to test the network by generating binary data. This test can highlight problems that aren't apparent in the ASCII chargen protocol, such as an old gateway configured to strip off the high order bit of every byte, throw away every 2 30 byte, or put into diagnostic mode by an unexpected sequence of control characters. These are not theoretical problems. I've seen variations on all of these at one time or another.

You could test the network for such problems by sending out every possible int . This would, after about 4.2 billion iterations, test every possible four-byte sequence. On the receiving end, you could easily test whether the data received is expected with a simple numeric comparison. If any problems are found, it is easy to tell exactly where they occurred. In other words, this protocol (call it Intgen ) behaves like this:

  1. The client connects to the server.

  2. The server immediately begins sending four-byte, big-endian integers, starting with 0 and incrementing by 1 each time. The server will eventually wrap around into the negative numbers .

  3. The server runs indefinitely. The client closes the connection when it's had enough.

The server would store the current int in a 4-byte long direct ByteBuffer . One buffer would be attached to each channel. When the channel becomes available for writing, the buffer is drained onto the channel. Then the buffer is rewound and the content of the buffer is read with getInt( ) . The program then clears the buffer, increments the previous value by one, and fills the buffer with the new value using putInt() . Finally, it flips the buffer so it will be ready to be drained the next time the channel becomes writable. Example 12-3 demonstrates .

Example 12-3. Intgen server
 import java.nio.*; import java.nio.channels.*; import java.net.*; import java.util.*; import java.io.IOException; public class IntgenServer {        public static int DEFAULT_PORT = 1919;      public static void main(String[] args) {        int port;     try {       port = Integer.parseInt(args[0]);     }     catch (Exception ex) {       port = DEFAULT_PORT;        }     System.out.println("Listening for connections on port " + port);          ServerSocketChannel serverChannel;     Selector selector;     try {       serverChannel = ServerSocketChannel.open( );       ServerSocket ss = serverChannel.socket( );       InetSocketAddress address = new InetSocketAddress(port);       ss.bind(address);       serverChannel.configureBlocking(false);       selector = Selector.open( );       serverChannel.register(selector, SelectionKey.OP_ACCEPT);     }     catch (IOException ex) {       ex.printStackTrace( );       return;        }          while (true) {              try {         selector.select( );       }       catch (IOException ex) {         ex.printStackTrace( );         break;       }                Set readyKeys = selector.selectedKeys( );       Iterator iterator = readyKeys.iterator( );       while (iterator.hasNext( )) {                  SelectionKey key = (SelectionKey) iterator.next( );         iterator.remove( );         try {           if (key.isAcceptable( )) {             ServerSocketChannel server = (ServerSocketChannel ) key.channel( );             SocketChannel client = server.accept( );             System.out.println("Accepted connection from " + client);             client.configureBlocking(false);             SelectionKey key2 = client.register(selector, SelectionKey.                                                                      OP_WRITE);             ByteBuffer output = ByteBuffer.allocate(4);             output.putInt(0);             output.flip( );             key2.attach(output);           }           else if (key.isWritable( )) {             SocketChannel client = (SocketChannel) key.channel( );             ByteBuffer output = (ByteBuffer) key.attachment( );             if (! output.hasRemaining( )) {               output.rewind( );               int value = output.getInt( );               output.clear( );               output.putInt(value+1);               output.flip( );             }             client.write(output);           }         }         catch (IOException ex) {           key.cancel( );           try {             key.channel( ).close( );           }           catch (IOException cex) {}         }                }     }      }    } 

12.3.5 View Buffers

If you know the ByteBuffer read from a SocketChannel contains nothing but elements of one particular primitive data type, it may be worthwhile to create a view buffer . This is a new Buffer object of appropriate type such as DoubleBuffer , IntBuffer , etc., which draws its data from an underlying ByteBuffer beginning with the current position. Changes to the view buffer are reflected in the underlying buffer and vice versa. However, each buffer has its own independent limit, capacity, mark, and position. View buffers are created with one of these six methods in ByteBuffer :

 public abstract ShortBuffer  asShortBuffer( ) public abstract CharBuffer   asCharBuffer( ) public abstract IntBuffer    asIntBuffer( ) public abstract LongBuffer   asLongBuffer( ) public abstract FloatBuffer  asFloatBuffer( ) public abstract DoubleBuffer asDoubleBuffer( ) 

For example, consider a client for the Intgen protocol. This protocol is only going to read ints, so it may be helpful to use an IntBuffer rather than a ByteBuffer . Example 12-4 demonstrates. For variety, this client is synchronous and blocking, but it still uses channels and buffers.

Example 12-4. Intgen client
 import java.nio.*; import java.nio.channels.*; import java.net.*; import java.io.IOException; public class IntgenClient {        public static int DEFAULT_PORT = 1919;      public static void main(String[] args) {        if (args.length == 0) {       System.out.println("Usage: java IntgenClient host [port]");        return;     }          int port;     try {       port = Integer.parseInt(args[1]);     }     catch (Exception ex) {       port = DEFAULT_PORT;        }          try {       SocketAddress address = new InetSocketAddress(args[0], port);       SocketChannel client  = SocketChannel.open(address);       ByteBuffer    buffer  = ByteBuffer.allocate(4);       IntBuffer     view    = buffer.asIntBuffer( );              for (int expected = 0; ; expected++) {         client.read(buffer);         int actual = view.get( );         buffer.clear( );         view.rewind( );                  if (actual != expected) {           System.err.println("Expected " + expected + "; was " + actual);           break;         }         System.out.println(actual);       }          }     catch (IOException ex) {         ex.printStackTrace( );        }      } } 

There's one thing to note here. Although you can fill and drain the buffers using the methods of the IntBuffer class exclusively, data must be read from and written to the channel using the original ByteBuffer of which the IntBuffer is a view. The SocketChannel class only has methods to read and write ByteBuffer s. It cannot read or write any other kind of buffer. This also means you need to clear the ByteBuffer on each pass through the loop or the buffer will fill up and the program will halt. The positions and limits of the two buffers are independent and must be considered separately. Finally, if you're working in non-blocking mode, be careful that all the data in the underlying ByteBuffer is drained before reading or writing from the overlaying view buffer. Non-blocking mode provides no guarantee that the buffer will still be aligned on an int / double / char /etc. boundary following a drain. It's completely possible for a non-blocking channel to write half the bytes of an int or a double. When using non-blocking I/O, be sure to check for this problem before putting more data in the view buffer.

12.3.6 Compacting Buffers

Most writable buffers support a compact( ) method:

 public abstract ByteBuffer   compact( ) public abstract IntBuffer    compact( ) public abstract ShortBuffer  compact( ) public abstract FloatBuffer  compact( ) public abstract CharBuffer   compact( ) public abstract DoubleBuffer compact( ) 

(If it weren't for invocation chaining, these six methods could have been replaced by one method in the common Buffer superclass.) Compacting shifts any remaining data in the buffer to the start of the buffer, freeing up more space for elements. Any data that was in those positions will be overwritten. The buffer's position is set to the end of the data so it's ready for writing more data.

Compacting is an especially useful operation when you're copying reading from one channel and writing the data to another using non-blocking I/O. You can read some data into a buffer, write the buffer out again, then compact the data so all the data that wasn't written is at the beginning of the buffer, and the position is at the end of the data remaining in the buffer, ready to receive more data. This allows the reads and writes to be interspersed more or less at random with only one buffer. Several reads can take place in a row, or several writes follow consecutively. If the network is ready for immediate output but not input (or vice versa), the program can take advantage of that. This technique can be used to implement an echo server as shown in Example 12-5. The echo protocol simply responds to the client with whatever data the client sent. Like chargen, it's useful for network testing. Also like chargen, echo relies on the client to close the connection. Unlike chargen, however, an echo server must both read and write from the connection.

Example 12-5. Echo server
 import java.nio.*; import java.nio.channels.*; import java.net.*; import java.util.*; import java.io.IOException; public class EchoServer {        public static int DEFAULT_PORT = 7;      public static void main(String[] args) {        int port;     try {       port = Integer.parseInt(args[0]);     }     catch (Exception ex) {       port = DEFAULT_PORT;        }     System.out.println("Listening for connections on port " + port);          ServerSocketChannel serverChannel;     Selector selector;     try {       serverChannel = ServerSocketChannel.open( );       ServerSocket ss = serverChannel.socket( );       InetSocketAddress address = new InetSocketAddress(port);       ss.bind(address);       serverChannel.configureBlocking(false);       selector = Selector.open( );       serverChannel.register(selector, SelectionKey.OP_ACCEPT);     }     catch (IOException ex) {       ex.printStackTrace( );       return;        }          while (true) {              try {         selector.select( );       }       catch (IOException ex) {         ex.printStackTrace( );         break;       }                Set readyKeys = selector.selectedKeys( );       Iterator iterator = readyKeys.iterator( );       while (iterator.hasNext( )) {                  SelectionKey key = (SelectionKey) iterator.next( );         iterator.remove( );         try {           if (key.isAcceptable( )) {             ServerSocketChannel server = (ServerSocketChannel ) key.channel( );             SocketChannel client = server.accept( );             System.out.println("Accepted connection from " + client);             client.configureBlocking(false);             SelectionKey clientKey = client.register(               selector, SelectionKey.OP_WRITE  SelectionKey.OP_READ);             ByteBuffer buffer = ByteBuffer.allocate(100);             clientKey.attach(buffer);           }           if (key.isReadable( )) {             SocketChannel client = (SocketChannel) key.channel( );             ByteBuffer output = (ByteBuffer) key.attachment( );             client.read(output);           }           if (key.isWritable( )) {             SocketChannel client = (SocketChannel) key.channel( );             ByteBuffer output = (ByteBuffer) key.attachment( );             output.flip( );             client.write(output);             output.compact( );           }         }         catch (IOException ex) {           key.cancel( );           try {             key.channel( ).close( );           }           catch (IOException cex) {}         }                }     }      } } 

One thing I noticed while writing and debugging this program: the buffer size makes a big difference, although perhaps not in the way you might think. A big buffer can hide a lot of bugs . If the buffer is large enough to hold complete test cases without being flipped or drained, it's very easy to not notice that the buffer isn't being flipped or compacted at the right times because the test cases never actually need to do that. Before releasing your program, try turning the buffer size down to something significantly lower than the input you're expecting. In this case, I tested with a buffer size of 10. This test degrades performance, so you shouldn't ship with such a ridiculously small buffer, but you absolutely should test your code with small buffers to make sure it behaves properly when the buffer fills up.

12.3.7 Duplicating Buffers

It's often desirable to make a copy of a buffer to deliver the same information to two or more channels. The duplicate() methods in each of the six typed buffer classes do this:

 public abstract ByteBuffer   duplicate( ) public abstract IntBuffer    duplicate( ) public abstract ShortBuffer  duplicate( ) public abstract FloatBuffer  duplicate( ) public abstract CharBuffer   duplicate( ) public abstract DoubleBuffer duplicate( ) 

The return values are not clones . The duplicated buffers share the same data, including the same backing array if the buffer is indirect. Changes to the data in one buffer are reflected in the other buffer. Thus, you should mostly use this method when you're only going to read from the buffers. Otherwise, it can be tricky to keep track of where the data is being modified.

The original and duplicated buffers do have independent marks, limits, and positions even though they share the same data. One buffer can be ahead of or behind the other buffer.

Duplication is useful when you want to transmit the same data over multiple channels, roughly in parallel. You can make duplicates of the main buffer for each channel and allow each channel to run at its own speed. For example, recall the single file HTTP server in Example 10-6. Reimplemented with channels and buffers as shown in Example 12-6, NonblockingSingleFileHTTPServer , the single file to serve is stored in one constant, read-only buffer. Every time a client connects, the program makes a duplicate of this buffer just for that channel, which is stored as the channel's attachment. Without duplicates, one client has to wait till the other finishes so the original buffer can be rewound. Duplicates enable simultaneous buffer reuse.

Example 12-6. A non-blocking HTTP server that chunks out the same file
 import java.io.*; import java.nio.*; import java.nio.channels.*; import java.util.Iterator; import java.net.*; public class NonblockingSingleFileHTTPServer {   private ByteBuffer contentBuffer;   private int port = 80;   public NonblockingSingleFileHTTPServer(    ByteBuffer data, String encoding, String MIMEType, int port)     throws UnsupportedEncodingException {          this.port = port;     String header = "HTTP/1.0 200 OK\r\n"      + "Server: OneFile 2.0\r\n"      + "Content-length: " + data.limit( ) + "\r\n"      + "Content-type: " + MIMEType + "\r\n\r\n";     byte[] headerData = header.getBytes("ASCII");     ByteBuffer buffer = ByteBuffer.allocate(      data.limit( ) + headerData.length);     buffer.put(headerData);     buffer.put(data);     buffer.flip( );     this.contentBuffer = buffer;        }      public void run( ) throws IOException {        ServerSocketChannel serverChannel = ServerSocketChannel.open( );     ServerSocket  serverSocket = serverChannel.socket( );     Selector selector = Selector.open( );     InetSocketAddress localPort = new InetSocketAddress(port);     serverSocket.bind(localPort);     serverChannel.configureBlocking(false);     serverChannel.register(selector, SelectionKey.OP_ACCEPT);     while (true) {            selector.select( );       Iterator keys = selector.selectedKeys( ).iterator( );       while (keys.hasNext( )) {         SelectionKey key = (SelectionKey) keys.next( );         keys.remove( );         try {           if (key.isAcceptable( )) {             ServerSocketChannel server = (ServerSocketChannel) key.channel( );             SocketChannel channel = server.accept( );             channel.configureBlocking(false);             SelectionKey newKey = channel.register(selector,                                                     SelectionKey.OP_READ);           }           else if (key.isWritable( )) {             SocketChannel channel = (SocketChannel) key.channel( );             ByteBuffer buffer = (ByteBuffer) key.attachment( );              if (buffer.hasRemaining( )) {                channel.write(buffer);                }             else {  // we're done                channel.close( );                }           }           else if (key.isReadable( )) {             // Don't bother trying to parse the HTTP header.             // Just read something.             SocketChannel channel = (SocketChannel) key.channel( );             ByteBuffer buffer = ByteBuffer.allocate(4096);              channel.read(buffer);             // switch channel to write-only mode             key.interestOps(SelectionKey.OP_WRITE);             key.attach(contentBuffer.duplicate( ));           }         }         catch (IOException ex) {           key.cancel( );           try {             key.channel( ).close( );           }           catch (IOException cex) {}         }       }     }   }      public static void main(String[] args) {     if (args.length == 0) {       System.out.println(         "Usage: java NonblockingSingleFileHTTPServer file port encoding");        return;     }            try {       String contentType = "text/plain";       if (args[0].endsWith(".html")  args[0].endsWith(".htm")) {         contentType = "text/html";       }              FileInputStream fin = new FileInputStream(args[0]);       FileChannel in = fin.getChannel( );       ByteBuffer input = in.map(FileChannel.MapMode.READ_ONLY, 0, in.size( ));                // set the port to listen on       int port;       try {         port = Integer.parseInt(args[1]);         if (port < 1  port > 65535) port = 80;       }         catch (Exception ex) {         port = 80;       }                String encoding = "ASCII";       if (args.length > 2) encoding = args[2];                NonblockingSingleFileHTTPServer server        = new NonblockingSingleFileHTTPServer(          input, encoding, contentType, port);       server.run( );              }     catch (Exception ex) {       ex.printStackTrace( );       System.err.println(ex);     }      } } 

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 a single ByteBuffer so that they can be blasted to clients very quickly. However, although all clients receive the same content, they may not receive it at the same time. Different parallel clients will be at different locations in the file. This is why we duplicate the buffer, so each channel has its own buffer. The overhead is small because all channels do share the same content. They just have different indexes into that content.

All incoming connections are handled by a single Selector in the run( ) method. The initial setup here is very similar to the earlier chargen server. The run( ) method opens a ServerSocketChannel and binds it to the specified port. Then it creates the Selector and registers it with the ServerSocketChannel . When a SocketChannel is accepted, the same Selector object is registered with it. Initially it's registered for reading because the HTTP protocol requires the client to send a request before the server responds.

The response to a read is simplistic. The program reads as many bytes of input as it can up to 4K. Then it resets the interest operations for the channel to writability. (A more complete server would actually attempt to parse the HTTP header request here and choose the file to send based on that information.) Next, the content buffer is duplicated and attached to the channel.

The next time the program passes through the while loop, this channel should be ready to receive data (or if not the next time, the time after that; the asynchronous nature of the connection means we won't see it until it's ready). At this point, we get the buffer out of the attachment, and write as much of the buffer as we can onto the channel. It's no big deal if we don't write it all this time. We'll just pick up where we left off the next pass through the loop. The buffer keeps track of its own position. Although many incoming clients might result in the creation of many buffer objects, the real overhead is minimal because they'll all share the same underlying data.

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 mapped into the ByteBuffer array input . (To be perfectly honest, this is complete overkill for the small to medium size files you're most likely to be serving here, and probably would be slower than using an InputStream that reads into a byte array, but I wanted to show you file mapping at least once.) 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, port 80 is used. The encoding is read from the third command-line argument if present. Otherwise, ASCII is assumed. Then these values are used to construct a NonblockingSingleFileHTTPServer object and start it running.

12.3.8 Slicing Buffers

Slicing a buffer is a slight variant of duplicating. Slicing also creates a new buffer that shares the same data with the old buffer. However, the slice's initial position is the current position of the original buffer. That is, the slice is like a subsequence of the original buffer that only contains the elements from the current position to the limit. Rewinding the slice only moves it back to the position of the original buffer when the slice was created. The slice can't see anything in the original buffer before that point. Again, there are separate slice( ) methods in each of the six typed buffer classes:

 public abstract ByteBuffer   slice( ) public abstract IntBuffer    slice( ) public abstract ShortBuffer  slice( ) public abstract FloatBuffer  slice( ) public abstract CharBuffer   slice( ) public abstract DoubleBuffer slice( ) 

This is useful when you have a long buffer of data that is easily divided into multiple parts such as a protocol header followed by the data. You can read out the header then slice the buffer and pass the new buffer containing only the data to a separate method or class.

12.3.9 Marking and Resetting

Like input streams, buffers can be marked and reset if you want to reread some data. Unlike input streams, this can be done to all buffers, not just some of them. For a change, the relevant methods are declared once in the Buffer superclass and inherited by all the various subclasses:

 public final Buffer mark( ) public final Buffer reset( ) 

The reset( ) method throws an InvalidMarkException , a runtime exception, if the mark is not set.

The mark is unset if the limit is set to a point below the mark.

12.3.10 Object Methods

The buffer classes all provide the usual equals( ) , hashCode( ) , and toString( ) methods. They also implement Comparable , and therefore provide compareTo( ) methods. However, buffers are not Serializable or Cloneable .

Two buffers are considered to be equal if:

  • They have the same type (e.g., a ByteBuffer is never equal to an IntBuffer but may be equal to another ByteBuffer ).

  • They have the same number of elements remaining in the buffer.

  • The remaining elements at the same relative positions are equal to each other.

Note that equality does not consider the buffers' elements that precede the cursor, the buffers' capacity, limits, or marks. For example, this code fragment would print true even though the first buffer is twice the size of the second:

 CharBuffer buffer1 = CharBuffer.wrap("12345678"); CharBuffer buffer2 = CharBuffer.wrap("5678"); buffer1.get( ); buffer1.get( ); buffer1.get( ); buffer1.get( ); System.out.println(buffer1.equals(buffer2)); 

The hashCode( ) method is implemented in accordance with the contract for equality. That is, two equal buffers will have equal hash codes and two unequal buffers are very unlikely to have equal hash codes. However, because the buffer's hash code changes every time an element is added to or removed from the buffer, buffers do not make good hash table keys.

Comparison is implemented by comparing the remaining elements in each buffer, one by one. If all the corresponding elements are equal, the buffers are equal. Otherwise, the result is the outcome of comparing the first pair of unequal elements. If one buffer runs out of elements before an unequal element is found and the other buffer still has elements, the shorter buffer is considered to be less than the longer buffer.

The toString( ) method returns strings that look something like this:

 java.nio.HeapByteBuffer[pos=0 lim=62 cap=62] 

These are primarily useful for debugging. The notable exception is CharBuffer , which returns a string containing the remaining chars in the buffer.



Java Network Programming
Java Network Programming, Third Edition
ISBN: 0596007213
EAN: 2147483647
Year: 2003
Pages: 164

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net