12.1 An Example Client

     

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 rama.poly.edu on port 19:

 SocketAddress rama = new InetSocketAddress("rama.poly.edu", 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:

 ByteBuffer buffer= ByteBuffer.allocate(74); 

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:

 int bytesRead = client.read(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:

 WritableByteChannel output = Channels.newChannel(System.out); 

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:

 buffer.flip( ); output.write(buffer); 

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:

 buffer.clear( ); 

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.

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

Here's the output from a sample run:

 $  java ChargenClient rama.poly.edu  !"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg !"#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh "#$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi #$%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij $%&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk %&'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl &'( )*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm ... 

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:

 client.configureBlocking(false); 

In non-blocking mode, read() may return 0 because it doesn't read anything. Therefore the loop needs to be a little different:

 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;   } } 

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.



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

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