Networking and asynchronous I/O
Networking and asynchronous I/O overview
Networking is an excellent foundation for learning about asynchronous I/O, which is of course essential knowledge for anyone doing input/output procedures in the Java language. Networking in NIO isn't much different from any other operation in NIO -- it relies on channels and buffers, and you acquire the channels from the usual InputStream s and OutputStream s.
In this section we'll start with the fundamentals of asynchronous I/O -- what it is and what it is not -- and then move on to a more hands-on, procedural example.
Asynchronous I/O is a method for reading and writing data without blocking.
Normally, when your code makes a read() call, the code blocks until there is data to be read. Likewise, a write() call will block until the data can be written.
Asynchronous I/O calls, on the other hand, do not block. Instead, you register your interest in a particular I/O event -- the arrival of readable data, a new socket connection, and so on -- and the system tells you when such an event occurs.
One of the advantages of asynchronous I/O is that it lets you do I/O from a great many inputs and outputs at the same time. Synchronous programs often have to resort to polling, or to the creation of many, many threads, to deal with lots of connections. With asynchronous I/O, you can listen for I/O events on an arbitrary number of channels, without polling and without extra threads.
We'll see asynchronous I/O in action by examining an example program called MultiPortEcho.java. This program is like the traditional echo server, which takes network connections and echoes back to them any data they might send. However, it has the added feature that it can listen on multiple ports at the same time, and deal with connections from all of those ports. And it does it all in a single thread.
The explanation in this section corresponds to the implementation of the go() method in the source code for MultiPortEcho,
so take a look at the source for a fuller picture of what is going on.
The central object in asynchronous I/O is called the Selector.
A Selector is where you register your interest in various I/O events, and it is the object that tells you when those events occur.
So, the first thing we need to do is create a Selector:
Selector selector = Selector.open(); |
Later on, we will call the register() method on various channel objects, in order to register our interest in I/O events happening inside those objects. The first argument to register() is always the Selector.
In order to receive connections, we need a ServerSocketChannel.
In fact, we need one for each of the ports on which we are going to listen. For each of the ports, we open a ServerSocketChannel,
as shown here:
ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking( false ); ServerSocket ss = ssc.socket(); InetSocketAddress address = new InetSocketAddress( ports[i] ); ss.bind( address ); |
The first line creates a new ServerSocketChannel and the last three lines bind it to the given port. The second line sets the ServerSocketChannel to be non-blocking.
We must call this method on every socket channel that we're using; otherwise asynchronous I/O won't work.
Our next step is to register the newly opened ServerSocketChannels with our Selector.
We do this using the ServerSocketChannel.register() method, as shown below:
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); |
The first argument to register() is always the Selector.
The second argument, OP_ACCEPT,
here specifies that we want to listen for accept events -- that is, the events that occur when a new connection is made. This is the only kind of event that is appropriate for a ServerSocketChannel.
Note the return value of the call to register().
A SelectionKey represents this registration of this channel with this Selector.
When a Selector notifies you of an incoming event, it does this by supplying the SelectionKey that corresponds to that event. The SelectionKey can also be used to de-register the channel.
Now that we have registered our interest in some I/O events, we enter the main loop. Just about every program that uses Selectors uses an inner loop much like this one:
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}
|
First, we call the select() method of our Selector.
This method blocks until at least one of the registered events occurs. When one or more events occur, the select() method returns the number of events that occurred.
Next, we call the Selector 's selectedKeys() method, which returns a Set of the SelectionKey objects for which events have occurred.
We process the events by iterating through the SelectionKeys and dealing with each one in turn. For each SelectionKey,
you must determine what I/O event has happened and which I/O objects have been impacted by that event.
At this point in the execution of our program, we've only registered ServerSocketChannel s, and we have only registered them for "accept" events. To confirm this, we call the readyOps() method on our SelectionKey and check to see what kind of event has occurred:
if ((key.readyOps() & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT) {
// Accept the new connection
// ...
}
|
Sure enough, the readOps() method tells us that the event is a new connection.
Because we know there is an incoming connection waiting on this server socket, we can safely accept it; that is, without fear that the accept() operation will block:
ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); SocketChannel sc = ssc.accept(); |
Our next step is to configure the newly-connected SocketChannel to be non-blocking. And because the purpose of accepting this connection is to read data from the socket, we must also register the SocketChannel with our Selector,
as shown below:
sc.configureBlocking( false ); SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ ); |
Note that we've registered the SocketChannel for reading rather than accepting new connections, using the OP_READ argument to register().
Removing the processed SelectionKey
Having processed the SelectionKey,
we're almost ready to return to the main loop. But first we must remove the processed SelectionKey from the set of selected keys. If we do not remove the processed key, it will still be present as an activated key in the main set, which would lead us to attempt to process it again. We call the iterator's remove() method to remove the processed SelectionKey:
it.remove(); |
Now we're set to return to the main loop and receive incoming data (or an incoming I/O event) on one of our sockets.
When data arrives from one of the sockets, it triggers an I/O event. This causes the call to Selector.select(),
in our main loop, to return with an I/O event or events. This time, the SelectionKey will be marked as an OP_READ event, as shown below:
} else if ((key.readyOps() & SelectionKey.OP_READ)
== SelectionKey.OP_READ) {
// Read the data
SocketChannel sc = (SocketChannel)key.channel();
// ...
}
|
As before, we get the channel in which the I/O event occurred and process it. In this case, because this is an echo server, we just want to read the data from the socket and send it right back. See the source code (MultiPortEcho.java) in Resources for details on this process.
Each time we return to the main loop we call the select() method on our Selector,
and we get a set of SelectionKey s. Each key represents an I/O event. We process the events, remove the SelectionKey s from the selected set, and go back to the top of the main loop.
This program is a bit simplistic, since it aims only to demonstrate the techniques involved in asynchronous I/O. In a real application, you would need to deal with closed channels by removing them from the Selector.
And you would probably want to use more than one thread. This program can get away with a single thread because it's only a demo, but in a real-world scenario it might make more sense to create a pool of threads for taking care of the time-consuming portions of I/O event processing.

