Team LiB
Previous Section Next Section

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();

    Team LiB
    Previous Section Next Section