Although the new I/O APIs aren't specifically designed for clients, they do work for them. I'm going to begin with a client program using the new I/O APIs because it's a little simpler. In particular, many clients can be implemented with one connection at a time, so I can introduce channels and buffers before talking about selectors and non-blocking I/O.
To demonstrate the basics, I'll implement a simple client for the character generator protocol defined in RFC 864. This protocol is designed for testing clients. The server listens for connections on port 19. When a client connects, the server sends a continuous sequence of characters until the client disconnects. Any input from the client is ignored. The RFC does not specify which character sequence to send, but recommends that the server use a recognizable pattern. One common pattern is rotating, 72-character carriage return/linefeed delimited lines of the 95 ASCII printing characters, like this:
!"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh "#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi #$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij $%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk %&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl &'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
I picked this protocol for the examples in this chapter because both the protocol for transmitting the data and the algorithm to generate the data are simple enough that they won't obscure the I/O. However, chargen can transmit a lot of data over a relatively few connections and quickly saturate a network connection. It's thus a good candidate for the new I/O APIs.
When
implementing a client that takes advantage of Java
1.4's new I/O APIs, begin by invoking the static
factory method SocketChannel.open() to create a new
java.nio.channels.SocketChannel object. The
argument to this method is a
java.net.SocketAddress object indicating the host
and port to connect to. For example, this fragment connects the
channel to SocketAddress rama = new InetSocketAddress(", 19);
SocketChannel client = SocketChannel .open(rama); The channel is opened in blocking mode, so the next line of code
won't execute until the connection is established.
If the connection can't be established, an
IOException is thrown. If this were a traditional client, you'd now ask for
the socket's input and/or output streams. However,
it's not. With a
channel you write directly to the channel
itself. Rather than writing byte arrays, you write
ByteBuffer objects.
We've got a pretty good idea that the lines of text
are 74 ASCII characters long (72 printable characters followed by a
carriage return/linefeed pair) so we'll create a
ByteBuffer that has a 74-byte capacity using the
static allocate( ) method: Pass this ByteBuffer object to the
channel's read( ) method. The
channel fills this buffer with the data it reads from the socket. It
returns the number of bytes it successfully read and stored in the
buffer: By default, this will read at least one byte or return -1 to indicate
the end of the data, exactly as an InputStream
does. It will often read more bytes if more bytes are available to be
read. Shortly you'll see how to put this client in
non-blocking mode where it will return 0 immediately if no bytes are
available, but for the moment this code blocks just like an
InputStream. As you could probably guess, this
method can also throw an IOException if anything
goes wrong with the read. Assuming there is some data in the bufferthat is, n >
0this data can be copied to System.out.
There are ways to extract a byte array from a
ByteBuffer that can then be written on a
traditional OutputStream such as
System.out. However, it's more
informative to stick with a pure, channel-based solution. Such a
solution requires wrapping the OutputStream
System.out in a channel using the
Channels utility class, specifically,
its newChannel( ) method: You can then write the data that was read onto this output channel
connected to System.out. However, before you do
that you have to flip the
buffer
so that the output channel starts from the beginning of the data that
was read rather than the end: You don't have to tell the output channel how many
bytes to write. Buffers keep track of how many bytes they contain.
However, in general, the output channel is not guaranteed to write
all the bytes in the buffer. In this specific case, though,
it's a blocking channel and it will either do so or
throw an IOException. You shouldn't create a new buffer for each read and
write. That would kill the performance. Instead, reuse the existing
buffer.
You'll need to clear the buffer before reading into
it again: This is a little different than flipping. Flipping leaves the data in
the buffer intact, but prepares it for writing rather than reading.
Clearing resets the buffer to a pristine state.[2] [2] Actually that's a tad simplistic. The old data
is still present. It's not overwritten, but it will
be overwritten with new data read from the source as soon as
possible.
Example 12-1 puts this together into a complete
client. Because chargen is by design an endless protocol,
you'll need to kill the program using Ctrl-C.
Here's the output from a sample run: So far, this is just an alternate vision of a program that could have
easily been written using streams. The really new feature comes if
you want the client to do something besides copying all input to
output. You can run this connection in either blocking or
non-blocking mode in which read( ) returns
immediately even if no data is available. This allows the program to
do something else before it attempts to read. It
doesn't have to wait for a slow network connection.
To change the blocking mode, pass true (block) or
false (don't block) to the
configureBlocking( ) method.
Let's make this connection non-blocking: In non-blocking mode, read() may return 0 because it doesn't read
anything. Therefore the loop needs to be a little different: There's not a lot of call for this in a
one-connection client like this one. Perhaps you could check to see
if the user has done something to cancel input, for example. However,
as you'll see in the next section, when a program is
processing multiple connections, this enables code to run very
quickly on the fast connections and more slowly on the slow ones.
Each connection gets to run at its own speed without being held up
behind the slowest driver on the one-lane road.
ByteBuffer buffer= ByteBuffer.allocate(74);
int bytesRead = client.read(buffer);
WritableByteChannel output = Channels.newChannel(System.out);
buffer.flip( );
output.write(buffer);
buffer.clear( );
Example 12-1. A channel-based chargen client
import java.nio.*;
import java.nio.channels.*;
import java.net.*;
import java.io.IOException;
public class ChargenClient {
public static int DEFAULT_PORT = 19;
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: java ChargenClient 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(74);
WritableByteChannel out = Channels.newChannel(System.out);
while (client.read(buffer) != -1) {
buffer.flip( );
out.write(buffer);
buffer.clear( );
}
}
catch (IOException ex) {
ex.printStackTrace( );
}
}
}$ java ChargenClient
!"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg
!"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
...
client.configureBlocking(false);
while (true) {
// Put whatever code here you want to run every pass through the loop
// whether anything is read or not
int n = client.read(buffer);
if (n > 0) {
buffer.flip( );
out.write(buffer);
buffer.clear( );
}
else if (n == -1) {
// This shouldn't happen unless the server is misbehaving.
break;
}
}