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:
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)
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( )
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)
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( )
|
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);The cursor is positioned at the beginning of the buffer; that is, the
IntBuffer buffer2 = IntBuffer.allocate(100);
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( );The array( ) method does expose the
int[] data2 = buffer2.array( );
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");Here, the buffer contains a reference to the array, which serves as
ByteBuffer buffer1 = ByteBuffer.wrap(data);
char[] text = "Some text".toCharArray( );
CharBuffer buffer2 = CharBuffer.wrap(text);
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";We can only fill the buffer up to its capacity. If we tried to fill
CharBuffer buffer = CharBuffer.wrap(s);
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 = ";Buffer classes also have
while (buffer.hasRemaining( )) {
result+= buffer.get( );
}
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)These both throw IndexOutOfBoundsException if you
public abstract ByteBuffer put(int index, byte b)
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)These put methods insert the data from the specified array or
public ByteBuffer get(byte[] dst)
public ByteBuffer put(byte[] array, int offset, int length)
public ByteBuffer put(byte[] array)
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 typeint,
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( )In the world of new I/O, these methods do the job performed by
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)
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)) {Suppose instead of a chargen protocol, you want to test the network
buffer.order(ByteOrder.LITLLE_ENDIAN);
}
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 230 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:The client connects to the server.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.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( )For example, consider a client for the Intgen
public abstract CharBuffer asCharBuffer( )
public abstract IntBuffer asIntBuffer( )
public abstract LongBuffer asLongBuffer( )
public abstract FloatBuffer asFloatBuffer( )
public abstract DoubleBuffer asDoubleBuffer( )
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.*;There's one thing to note here. Although you can
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( );
}
}
}
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
ByteBuffers. 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( )(If it weren't for invocation chaining, these six
public abstract IntBuffer compact( )
public abstract ShortBuffer compact( )
public abstract FloatBuffer compact( )
public abstract CharBuffer compact( )
public abstract DoubleBuffer compact( )
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 copyingreading
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.*;One thing I noticed
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) {}
}
}
}
}
}
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( )The return values are not clones. The duplicated buffers share the
public abstract IntBuffer duplicate( )
public abstract ShortBuffer duplicate( )
public abstract FloatBuffer duplicate( )
public abstract CharBuffer duplicate( )
public abstract DoubleBuffer duplicate( )
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.*;The constructors set up the data to be sent along with an HTTP header
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("l") || args[0].endsWith("")) {
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);
}
}
}
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( )This is useful when you have a long buffer of data that is easily
public abstract IntBuffer slice( )
public abstract ShortBuffer slice( )
public abstract FloatBuffer slice( )
public abstract CharBuffer slice( )
public abstract DoubleBuffer slice( )
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( )The reset( ) method throws an
public final Buffer reset( )
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");The hashCode( )
CharBuffer buffer2 = CharBuffer.wrap("5678");
buffer1.get( );
buffer1.get( );
buffer1.get( );
buffer1.get( );
System.out.println(buffer1.equals(buffer2));
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.