5.11. I/O and Networking with java.nio
Java 1.4 introduced an entirely new API for
high-performance, nonblocking I/O and networking. This API consists
primarily of three new packages. java.nio defines
Buffer classes that are used to store sequences of
bytes or other primitive values.
java.nio.channels
defines
channels
through which data can be transferred between a buffer and a data
source or sink, such as a file or a network socket. This package also
contains important classes used for nonblocking I/O.
Finally, the
java.nio.charset package contains classes for
efficiently converting buffers of
bytes into buffers of
characters.
The sections that follow contain examples of using all three of these
packages as well as examples of specific I/O tasks with the New I/O
API.
5.11.1. Basic Buffer Operations
The java.nio
package
includes an
abstract Buffer class, which defines generic
operations on buffers. This package also defines type-specific
subclasses such as ByteBuffer,
CharBuffer, and IntBuffer. (See
Buffer and ByteBuffer in the
reference section for details on these classes and their various
methods.) The following code illustrates typical sequences of buffer
operations on a ByteBuffer. The other
type-specific buffer classes have similar methods.
import java.nio.*;
// Buffers don't have public constructors. They are allocated instead.
ByteBuffer b = ByteBuffer.allocate(4096); // Create a buffer for 4,096 bytes
// Or do this to try to get an efficient buffer from the low-level OS
ByteBuffer buf2 = ByteBuffer.allocateDirect(65536);
// Here's another way to get a buffer: by "wrapping" an array
byte[] data; // Assume this array is created and initialized elsewhere
ByteBuffer buf3 = ByteBuffer.wrap(data); // Create buffer that uses the array
// It is also possible to create a "view buffer" to view bytes as other types
buf3.order(ByteOrder.BIG_ENDIAN); // Specify the byte order for the buffer
IntBuffer ib = buf3.asIntBuffer(); // View those bytes as integers
// Now store some data in the buffer
b.put(data); // Copy bytes from array to buffer at current position
b.put((byte)42); // Store another byte at the new current position
b.put(0, (byte)9); // Overwrite first byte in buffer. Don't change position.
b.order(ByteOrder.BIG_ENDIAN); // Set the byte order of the buffer
b.putChar('x'); // Store the two bytes of a Unicode character in buffer
b.putInt(0xcafebabe); // Store four bytes of an int into the buffer
// Here are methods for querying basic numbers about a buffer
int capacity = b.capacity(); // How many bytes can the buffer hold? (4,096)
int position = b.position(); // Where will the next byte be written or read?
// A buffer's limit specifies how many bytes of the buffer can be used.
// When writing into a buffer, this should be the capacity. When reading data
// from a buffer, it should be the number of bytes that were previously
// written.
int limit = b.limit(); // How many should be used?
int remaining = b.remaining(); // How many left? Return limit-position.
boolean more=b.hasRemaining(); // Test if there is still room in the buffer
// The position and limit can also be set with methods of the same name
// Suppose you want to read the bytes you've written into the buffer
b.limit(b.position()); // Set limit to current position
b.position(0); // Set limit to 0; start reading at beginning
// Instead of the two previous calls, you usually use a convenience method
b.flip(); // Set limit to position and position to 0; prepare for reading
b.rewind(); // Set position to 0; don't change limit; prepare for rereading
b.clear(); // Set position to 0 and limit to capacity; prepare for writing
// Assuming you've called flip(), you can start reading bytes from the buffer
buf2.put(b); // Read all bytes from b and put them into buf2
b.rewind(); // Rewind b for rereading from the beginning
byte b0 = b.get(); // Read first byte; increment buffer position
byte b1 = b.get(); // Read second byte; increment buffer position
byte[] fourbytes = new byte[4];
b.get(fourbytes); // Read next four bytes, add 4 to buffer position
byte b9 = b.get(9); // Read 10th byte, without changing current position
int i = b.getInt(); // Read next four bytes as an integer; add 4 to position
// Discard bytes you've already read; shift the remaining ones to the
// beginning of the buffer; set position to new limit and limit to capacity,
// preparing the buffer for writing more bytes into it.
b.compact();
You
may notice that many buffer methods return the object on which they
operate. This is done so that method calls can be
"chained" in code, as follows:
ByteBuffer bb=ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN).putInt(1234);
Many methods throughout java.nio and its
subpackages return the current object to enable this kind of method
chaining. Note that the use of this kind of chaining is a stylistic
choice (which I have avoided in this chapter) and does not have any
significant impact on efficiency.
ByteBuffer is the
most important of the buffer classes. However, another commonly used
class is CharBuffer.
CharBuffer objects can be created by wrapping a
string and can also be converted to strings.
CharBuffer implements the new
java.lang.CharSequence interface, which means that
it can be used like a String or
StringBuffer in certain applications (e.g., for
regular expression pattern matching).
// Create a read-only CharBuffer from a string
CharBuffer cb = CharBuffer.wrap("This string is the data for the CharBuffer");
String s = cb.toString(); // Convert to a String with toString() method
System.out.println(cb); // or rely on an implicit call to toString().
char c = cb.charAt(0); // Use CharSequence methods to get characters
char d = cb.get(1); // or use a CharBuffer absolute read.
// A relative read that reads the char and increments the current position
// Note that only the characters between the position and limit are used when
// a CharBuffer is converted to a String or used as a CharSequence.
char e = cb.get();
Bytes in a ByteBuffer are commonly converted to
characters in a CharBuffer and vice versa.
We'll see how to do this when we consider the
java.nio.charset package.
5.11.2. Basic Channel Operations
Buffers are not all that useful on
their ownthere isn't much point in storing
bytes into a buffer only to read them out again. Instead, buffers are
typically used with channels: your program stores bytes into a
buffer, then passes the buffer to a channel, which reads the bytes
out of the buffer and writes them to a file, network socket, or some
other destination. Or, in the reverse, your program passes a buffer
to a channel, which reads bytes from a file, socket, or other source
and stores those bytes into the buffer, where they can then be
retrieved by your program. The java.nio.channels
package defines several channel classes that represent files,
sockets, datagrams, and pipes. (We'll see examples
of these concrete classes later in this chapter.) The following code,
however, is based on the capabilities of the various channel
interfaces defined by java.nio.channels and should
work with any Channel object:
Channel c; // Object that implements Channel interface; initialized elsewhere
if (c.isOpen()) c.close(); // These are the only methods defined by Channel
// The read() and write() methods are defined by the
// ReadableByteChannel and WritableByteChannel interfaces.
ReadableByteChannel source; // Initialized elsewhere
WritableByteChannel destination; // Initialized elsewhere
ByteBuffer buffer = ByteBuffer.allocateDirect(16384); // Low-level 16 KB buffer
// Here is the basic loop to use when reading bytes from a source channel and
// writing them to a destination channel until there are no more bytes to read
// from the source and no more buffered bytes to write to the destination.
while(source.read(buffer) != -1 || buffer.position() > 0) {
// Flip buffer: set limit to position and position to 0. This prepares
// the buffer for reading (which is done by a channel *write* operation).
buffer.flip();
// Write some or all of the bytes in the buffer to the destination
destination.write(buffer);
// Discard the bytes that were written, copying the remaining ones to
// the start of the buffer. Set position to limit and limit to capacity,
// preparing the buffer for writing (done by a channel *read* operation).
buffer.compact();
}
// Don't forget to close the channels
source.close();
destination.close();
In addition to the
ReadableByteChannel
and
WritableByteChannel interfaces illustrated in the
preceding code, java.nio.channels defines several
other channel interfaces. ByteChannel simply
extends the readable and writable interfaces without adding any new
methods. It is a useful shorthand for channels that support both
reading and writing. GatheringByteChannel is an
extension of WritableByteChannel that defines
write() methods that gather
bytes from more than one buffer and write them out. Similarly,
ScatteringByteChannel is an extension of
ReadableByteChannel that defines
read() methods that read bytes from the channel
and scatter or distribute them into more than
one buffer. The gathering and scattering write( )
and read() methods can be useful when working with
network protocols that use fixed-size headers that you want to store
in a buffer separate from the rest of the transferred data.
One confusing point to be aware of is that a
channel read operation involves writing (or putting) bytes into a
buffer, and a channel write operation involves reading (or getting)
bytes from a buffer. Thus, when I say that the flip(
) method prepares a buffer for reading, I mean that it
prepares a buffer for use in a channel write( )
operation! The reverse is true for the buffer's
compact() method.
5.11.3. Encoding and Decoding Text with Charsets
A
java.nio.charset.Charset object represents a
character set plus
an encoding for that character set. Charset and
its associated classes,
CharsetEncoder and
CharsetDecoder,
define methods for encoding strings of characters into sequences of
bytes and decoding sequences of bytes into strings
of characters. Since these classes are part of the New I/O API, they
use the ByteBuffer and
CharBuffer
classes:
// The simplest case. Use Charset convenience routines to convert.
Charset charset = Charset.forName("ISO-8859-1"); // Get Latin-1 Charset
CharBuffer cb = CharBuffer.wrap("Hello World"); // Characters to encode
// Encode the characters and store the bytes in a newly allocated ByteBuffer
ByteBuffer bb = charset.encode(cb);
// Decode these bytes into a newly allocated CharBuffer and print them out
System.out.println(charset.decode(bb));
Note
the use of the ISO-8859-1 (a.k.a. Latin-1) charset in
this example. This 8-bit charset is suitable for most Western
European languages, including English. Programmers who work only with
English may also use the 7-bit US-ASCII charset. The
Charset class does not do encoding and decoding
itself, and the previous convenience routines create
CharsetEncoder and
CharsetDecoder classes internally. If you plan to
encode or decode multiple times, it is more efficient to create these
objects yourself:
Charset charset = Charset.forName("US-ASCII"); // Get the charset
CharsetEncoder encoder = charset.newEncoder(); // Create an encoder from it
CharBuffer cb = CharBuffer.wrap("Hello World!"); // Get a CharBuffer
WritableByteChannel destination; // Initialized elsewhere
destination.write(encoder.encode(cb)); // Encode chars and write
The preceding
CharsetEncoder.encode( ) method must allocate a
new ByteBuffer each time it is called. For maximum
efficiency, you can call lower-level methods to do the encoding and
decoding into an existing buffer:
ReadableByteChannel source; // Initialized elsewhere
Charset charset = Charset.forName("ISO-8859-1"); // Get the charset
CharsetDecoder decoder = charset.newDecoder(); // Create a decoder from it
ByteBuffer bb = ByteBuffer.allocateDirect(2048); // Buffer to hold bytes
CharBuffer cb = CharBuffer.allocate(2048); // Buffer to hold characters
while(source.read(bb) != -1) { // Read bytes from the channel until EOF
bb.flip(); // Flip byte buffer to prepare for decoding
decoder.decode(bb, cb, true); // Decode bytes into characters
cb.flip(); // Flip char buffer to prepare for printing
System.out.print(cb); // Print the characters
cb.clear(); // Clear char buffer to prepare for decoding
bb.clear(); // Prepare byte buffer for next channel read
}
source.close(); // Done with the channel, so close it
System.out.flush(); // Make sure all output characters appear
The preceding code relies on the fact
that ISO-8859-1 is an 8-bit encoding charset and
that there is one-to-one mapping between characters and bytes. For
more complex charsets, such as the UTF-8 encoding of Unicode or the
EUC-JP charset used with Japanese text; however, this does not
hold, and more than one byte is required for some (or all)
characters. When this is the case, there is no guarantee that all
bytes in a buffer can be decoded at once (the end of the buffer may
contain a partial character). Also, since a single character may
encode to more than one byte, it can be tricky to know how many bytes
a given string will encode into. The following code shows a loop you
can use to decode bytes in a more general way:
ReadableByteChannel source; // Initialized elsewhere
Charset charset = Charset.forName("UTF-8"); // A Unicode encoding
CharsetDecoder decoder = charset.newDecoder(); // Create a decoder from it
ByteBuffer bb = ByteBuffer.allocateDirect(2048); // Buffer to hold bytes
CharBuffer cb = CharBuffer.allocate(2048); // Buffer to hold characters
// Tell the decoder to ignore errors that might result from bad bytes
decoder.onMalformedInput(CodingErrorAction.IGNORE);
decoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
decoder.reset(); // Reset decoder if it has been used before
while(source.read(bb) != -1) { // Read bytes from the channel until EOF
bb.flip(); // Flip byte buffer to prepare for decoding
decoder.decode(bb, cb, false); // Decode bytes into characters
cb.flip(); // Flip char buffer to prepare for printing
System.out.print(cb); // Print the characters
cb.clear(); // Clear the character buffer
bb.compact(); // Discard already decoded bytes
}
source.close(); // Done with the channel, so close it
// At this point, there may still be some bytes in the buffer to decode
bb.flip(); // Prepare for decoding
decoder.decode(bb, cb, true); // Pass true to indicate this is the last call
decoder.flush(cb); // Output any final characters
cb.flip(); // Flip char buffer
System.out.print(cb); // Print the final characters
5.11.4. Working with Files
FileChannel
is
a concrete Channel class that performs file I/O
and implements
ReadableByteChannel
and
WritableByteChannel (although its
read() method works
only if the underlying file is open for reading, and its
write( ) method works
only if the file is open for writing). Obtain a
FileChannel object by using the
java.io package to create a
FileInputStream, a
FileOutputStream, or a
RandomAccessFile and then call the
getChannel() method (added
in Java 1.4) of that object. As an example, you can use two
FileChannel objects to copy a file:
String filename = "test"; // The name of the file to copy
// Create streams to read the original and write the copy
FileInputStream fin = new FileInputStream(filename);
FileOutputStream fout = new FileOutputStream(filename + ".copy");
// Use the streams to create corresponding channel objects
FileChannel in = fin.getChannel();
FileChannel out = fout.getChannel();
// Allocate a low-level 8KB buffer for the copy
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
while(in.read(buffer) != -1 || buffer.position() > 0) {
buffer.flip(); // Prepare to read from the buffer and write to the file
out.write(buffer); // Write some or all buffer contents
buffer.compact(); // Discard all bytes that were written and prepare to
} // read more from the file and store them in the buffer.
in.close(); // Always close channels and streams when done with them
out.close();
fin.close(); // Note that closing a FileChannel does not
fout.close(); // automatically close the underlying stream.
FileChannel has
special transferTo( ) and
transferFrom() methods that make it particularly
easy (and on many operating systems, particularly efficient) to
transfer a specified number of bytes from a
FileChannel to some other specified channel, or
from some other channel to a FileChannel. These
methods allow us to simplify the preceding file-copying code to the
following:
FileChannel in, out; // Assume these are initialized as in the
// preceding example.
long numbytes = in.size(); // Number of bytes in original file
in.transferTo(0, numbytes, out); // Transfer that amount to output channel
This code could be equally well-written using transferFrom(
) instead of TRansferTo() (note that
these two methods expect their arguments in different orders):
long numbytes = in.size();
out.transferFrom(in, 0, numbytes);
FileChannel has other
capabilities that are not shared by other channel classes. One of the
most important is the ability to "memory
map" a file or a portion of a file, i.e., to obtain
a MappedByteBuffer (a subclass of
ByteBuffer) that represents the contents of the
file and allows you to read (and optionally write) file contents
simply by reading from and writing to the buffer. Memory mapping a
file is a somewhat expensive operation, so this technique is usually
efficient only when you are working with a large file to which you
need repeated access. Memory mapping offers you yet another way to
perform the same file-copy operation shown previously:
long filesize = in.size();
ByteBuffer bb = in.map(FileChannel.MapMode.READ_ONLY, 0, filesize);
while(bb.hasRemaining()) out.write(bb);
The channel interfaces defined by
java.nio.channels include
ByteChannel but not
CharChannel. The channel API is low-level and
provides methods for reading bytes only. All of the previous examples
have treated files as binary files. It is possible to use the
CharsetEncoder and
CharsetDecoder classes introduced earlier to
convert between bytes and characters, but when you want to work with
text files, the Reader and
Writer classes of the java.io
package are usually much easier to use than
CharBuffer. Fortunately, the
Channels class defines convenience methods that
bridge between the old and new APIs. Here is code that wraps a
Reader and a Writer object
around input and output channels, reads lines of
Latin-1 text from the input channel, and
writes them back out to the output channel, with the encoding changed
to UTF-8:
ReadableByteChannel in; // Assume these are initialized elsewhere
WritableByteChannel out;
// Create a Reader and Writer from a FileChannel and charset name
BufferedReader reader=new BufferedReader(Channels.newReader(in, "ISO-8859-1"));
PrintWriter writer = new PrintWriter(Channels.newWriter(out, "UTF-8"));
String line;
while((line = reader.readLine()) != null) writer.println(line);
reader.close();
writer.close();
Unlike the FileInputStream
and FileOutputStream classes, the
FileChannel class allows random access to the
contents of the file. The zero-argument position()
method returns the file pointer (the position
of the next byte to be read), and the one-argument position(
) method allows you to set this pointer to any value you
want. This allows you to skip around in a file in the way that the
java.io.RandomAccessFile does. Here is an example:
// Suppose you have a file that has data records scattered throughout, and the
// last 1,024 bytes of the file are an index that provides the position of
// those records. Here is code that reads the index of the file, looks up the
// position of the first record within the file and then reads that record.
FileChannel in = new FileInputStream("test.data").getChannel(); // The channel
ByteBuffer index = ByteBuffer.allocate(1024); // A buffer to hold the index
long size = in.size(); // The size of the file
in.position(size - 1024); // Position at start of index
in.read(index); // Read the index
int record0Position = index.getInt(0); // Get first index entry
in.position(record0Position); // Position file at that point
ByteBuffer record0 = ByteBuffer.allocate(128); // Get buffer to hold data
in.read(record0); // Finally, read the record
The final feature of
FileChannel that we'll consider
here is its ability to lock a file or a portion of a file against all
concurrent access (an exclusive lock) or against concurrent writes (a
shared lock). (Note that some operating systems strictly enforce all
locks while others provide only an advisory locking facility that
requires programs to cooperate and to attempt to acquire a lock
before reading or writing portions of a shared file.) In the previous
random-access example, suppose we wanted to ensure that no other
program was modifying the record data while we read it. We could
acquire a shared lock on that portion of the file with the following
code:
FileLock lock = in.lock(record0Position, // Start of locked region
128, // Length of locked region
true); // Shared lock: prevent concurrent updates
// but allow concurrent reads.
in.position(record0Position); // Move to start of index
in.read(record0); // Read the index data
lock.release(); // You're done with the lock, so release it
5.11.5. Client-Side Networking
The
New I/O API includes networking
capabilities as well as file-access capabilities. To communicate over
the network, you can use the
SocketChannel
class.
Create a SocketChannel with the static
open() method,
then read and write bytes from and to it as you would with any other
channel object. The following code uses
SocketChannel to send an HTTP request to a web
server and saves the server's response (including
all of the HTTP headers) to a file.
Note the use of
java.net.InetSocketAddress, a subclass of
java.net.SocketAddress, to tell the
SocketChannel what to connect to. These classes
were also introduced as part of the New I/O API.
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
// Create a SocketChannel connected to the web server at www.oreilly.com
SocketChannel socket =
SocketChannel.open(new InetSocketAddress("www.oreilly.com",80));
// A charset for encoding the HTTP request
Charset charset = Charset.forName("ISO-8859-1");
// Send an HTTP request to the server. Start with a string, wrap it to
// a CharBuffer, encode it to a ByteBuffer, then write it to the socket.
socket.write(charset.encode(CharBuffer.wrap("GET / HTTP/1.0\r\n\r\n")));
// Create a FileChannel to save the server's response to
FileOutputStream out = new FileOutputStream("oreilly.html");
FileChannel file = out.getChannel();
// Get a buffer for holding bytes while transferring from socket to file
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
// Now loop until all bytes are read from the socket and written to the file
while(socket.read(buffer) != -1 || buffer.position() > 0) { // Are we done?
buffer.flip(); // Prepare to read bytes from buffer and write to file
file.write(buffer); // Write some or all bytes to the file
buffer.compact(); // Discard those that were written
}
socket.close(); // Close the socket channel
file.close(); // Close the file channel
out.close(); // Close the underlying file
Another way to create a
SocketChannel is with the no-argument version of
open(), which creates an unconnected channel. This
allows you to call the socket() method to obtain
the underlying socket, configure the socket as desired, and connect
to the desired host with the connect method. For example:
SocketChannel sc = SocketChannel.open(); // Open an unconnected socket channel
Socket s = sc.socket(); // Get underlying java.net.Socket
s.setSoTimeout(3000); // Time out after three seconds
// Now connect the socket channel to the desired host and port
sc.connect(new InetSocketAddress("www.davidflanagan.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(8192); // Create a buffer
try { sc.read(buffer); } // Try to read from socket
catch(SocketTimeoutException e) { // Catch timeouts here
System.out.println("The remote computer is not responding.");
sc.close();
}
In addition to the
SocketChannel class, the
java.nio.channels package defines a
DatagramChannel for networking with datagrams
instead of sockets. DatagramChannel is not
demonstrated here, but you can read about it in the reference
section.
One of the most powerful features of the New I/O API is that channels
such as SocketChannel and
DatagramChannel can be used in nonblocking mode.
We'll see examples of this in later sections.
5.11.6. Server-Side Networking
The
java.net package
defines a
Socket class for
communication between a client and a server and defines a
ServerSocket class used by the server to listen
for and accept connections from clients. The
java.nio.channels
package is analogous: it defines a
SocketChannel class for data transfer and a
ServerSocketChannel
class for accepting connections.
ServerSocketChannel is an unusual channel because
it does not implement ReadableByteChannel or
WritableByteChannel. Instead of
read() and write() methods, it
has an accept() method for
accepting client connections and obtaining a
SocketChannel through which it communicates with
the client. Here is the code for a simple, single-threaded server
that listens for connections on port 8000 and reports the current
time to any client that connects:
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
public class DateServer {
public static void main(String[] args) throws java.io.IOException {
// Get a CharsetEncoder for encoding the text sent to the client
CharsetEncoder encoder = Charset.forName("US-ASCII").newEncoder();
// Create a new ServerSocketChannel and bind it to port 8000
// Note that this must be done using the underlying ServerSocket
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new java.net.InetSocketAddress(8000));
for(;;) { // This server runs forever
// Wait for a client to connect
SocketChannel client = server.accept();
// Get the current date and time as a string
String response = new java.util.Date().toString() + "\r\n";
// Wrap, encode, and send the string to the client
client.write(encoder.encode(CharBuffer.wrap(response)));
// Disconnect from the client
client.close();
}
}
}
5.11.7. Nonblocking I/O
The
preceding
DateServer class is a simple network server.
Because it does not maintain a connection with any client, it never
needs to communicate with more than one at a time, and there is never
more than one SocketChannel in use. More realistic
servers must be able to communicate with more than one client at a
time. The java.io and java.net
APIs allow only blocking I/O, so servers written using these APIs
must use a separate thread for each client. For large-scale servers
with many clients, this approach does not scale well. To solve this
problem, the New I/O API allows most channels (but not
FileChannel) to be used in nonblocking mode and
allows a single thread to manage all pending connections. This is
done with a Selector object, which keeps track of
a set of registered channels and can block until one or more of those
channels is ready for I/O, as the following code illustrates. This is
a longer example than most in this chapter, but it is a complete
working server class that manages a
ServerSocketChannel and any number of
SocketChannel connections to clients through a
single Selector object.
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*; // For Set and Iterator
public class NonBlockingServer {
public static void main(String[] args) throws IOException {
// Get the character encoders and decoders you'll need
Charset charset = Charset.forName("ISO-8859-1");
CharsetEncoder encoder = charset.newEncoder();
CharsetDecoder decoder = charset.newDecoder();
// Allocate a buffer for communicating with clients
ByteBuffer buffer = ByteBuffer.allocate(512);
// All of the channels in this code will be in nonblocking mode.
// So create a Selector object that will block while monitoring
// all of the channels and stop blocking only when one or more
// of the channels is ready for I/O of some sort.
Selector selector = Selector.open();
// Create a new ServerSocketChannel and bind it to port 8000
// Note that this must be done using the underlying ServerSocket
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new java.net.InetSocketAddress(8000));
// Put the ServerSocketChannel into nonblocking mode
server.configureBlocking(false);
// Now register it with the Selector (note that register() is called
// on the channel, not on the selector object, however).
// The SelectionKey represents the registration of this channel with
// this Selector.
SelectionKey serverkey = server.register(selector,
SelectionKey.OP_ACCEPT);
for(;;) { // The main server loop. The server runs forever.
// This call blocks until there is activity on one of the
// registered channels. This is the key method in nonblocking
// I/O.
selector.select();
// Get a java.util.Set containing the SelectionKey objects for
// all channels that are ready for I/O.
Set keys = selector.selectedKeys();
// Use a java.util.Iterator to loop through the selected keys
for(Iterator i = keys.iterator(); i.hasNext(); ) {
// Get the next SelectionKey in the set and remove it
// from the set. It must be removed explicitly, or it will
// be returned again by the next call to select().
SelectionKey key = (SelectionKey) i.next();
i.remove();
// Check whether this key is the SelectionKey obtained when
// you registered the ServerSocketChannel.
if (key == serverkey) {
// Activity on the ServerSocketChannel means a client
// is trying to connect to the server.
if (key.isAcceptable()) {
// Accept the client connection and obtain a
// SocketChannel to communicate with the client.
SocketChannel client = server.accept();
// Put the client channel in nonblocking mode
client.configureBlocking(false);
// Now register it with the Selector object,
// telling it that you'd like to know when
// there is data to be read from this channel.
SelectionKey clientkey =
client.register(selector, SelectionKey.OP_READ);
// Attach some client state to the key. You'll
// use this state when you talk to the client.
clientkey.attach(new Integer(0));
}
}
else {
// If the key obtained from the Set of keys is not the
// ServerSocketChannel key, then it must be a key
// representing one of the client connections.
// Get the channel from the key.
SocketChannel client = (SocketChannel) key.channel();
// If you are here, there should be data to read from
// the channel, but double-check.
if (!key.isReadable()) continue;
// Now read bytes from the client. Assume that all the
// client's bytes are in one read operation.
int bytesread = client.read(buffer);
// If read() returns -1, it indicates end-of-stream,
// which means the client has disconnected, so
// deregister the selection key and close the channel.
if (bytesread == -1) {
key.cancel();
client.close();
continue;
}
// Otherwise, decode the bytes to a request string
buffer.flip();
String request = decoder.decode(buffer).toString();
buffer.clear();
// Now reply to the client based on the request string
if (request.trim().equals("quit")) {
// If the request was "quit", send a final message
// Close the channel and deregister the
// SelectionKey
client.write(encoder.encode(CharBuffer.wrap("Bye.")));
key.cancel();
client.close();
}
else {
// Otherwise, send a response string comprised of
// the sequence number of this request plus an
// uppercase version of the request string. Note
// that you keep track of the sequence number by
// "attaching" an Integer object to the
// SelectionKey and incrementing it each time.
// Get sequence number from SelectionKey
int num = ((Integer)key.attachment()).intValue();
// For response string
String response = num + ": " +
request.toUpperCase();
// Wrap, encode, and write the response string
client.write(encoder.encode(CharBuffer.wrap(response)));
// Attach an incremented sequence nubmer to the key
key.attach(new Integer(num+1));
}
}
}
}
}
}
Nonblocking I/O is most useful for
writing network servers. It is also useful in clients that have more
than one network connection pending at the same time. For example,
consider a web browser downloading a web page and the images
referenced by that page at the same time. One other interesting use
of nonblocking I/O is to perform nonblocking socket connection
operations. The idea is that you can ask a
SocketChannel to establish a connection to a
remote host and then go do other stuff (such as build a GUI, for
example) while the underlying OS is setting up the connection across
the network. Later, you do a select() call to
block until the connection has been established, if it
hasn't been already. The code for a nonblocking
connect looks like this:
// Create a new, unconnected SocketChannel. Put it in nonblocking
// mode, register it with a new Selector, and then tell it to connect.
// The connect call will return instead of waiting for the network
// connect to be fully established.
Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_CONNECT);
channel.connect(new InetSocketAddress(hostname, port));
// Now go do other stuff while the connection is set up
// For example, you can create a GUI here
// Now block if necessary until the SocketChannel is ready to connect.
// Since you've registered only one channel with this selector, you
// don't need to examine the key set; you know which channel is ready.
while(selector.select() == 0) /* empty loop */;
// This call is necessary to finish the nonblocking connections
channel.finishConnect();
// Finally, close the selector, which deregisters the channel from it
selector.close();
|