ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Introducing Nonblocking Sockets

by Giuseppe Naccarato
09/04/2002

Nonblocking sockets, introduced in Java 2 Standard Edition 1.4, allow net communication between applications without blocking the processes using the sockets. In this article, I will show in detail what a nonblocking socket is, how it works, and in which contexts it can be useful.

As of Java 1.4, a programmer can use a brand-new set of APIs for I/O operations. This is the result of JSR 51, which started in January 2000 and has been available to programmers since Java 1.4 beta. Some of the most important new features in Java 1.4 deal with subjects such as high performance read/write operations on both files and sockets, regular expressions, decoding/encoding character sets, in-memory mapping, and locking files. In this article, I will discuss one particular new concept -- the new I/O API: nonblocking sockets.

A nonblocking socket allows input/output operation on a channel without blocking the processes using it. I'm talking about asynchronous high-performance read/write operations that, as you will see, turn upside-down the techniques for designing and developing socked-based applications.

Java developers might ask: why introducing a new technology to handle sockets? What's wrong with the Java 1.3.x sockets? Suppose you would like to implement a server accepting diverse client connections. Suppose, as well, that you would like the server to be able to process multiple requests simultaneously. Using Java 1.3.x, you have two choices to develop such a server:

Both solutions work, but adopting the first one -- the whole thread-management solution, with related concurrency and conflict troubles -- has to be developed by programmer. The second solution may cost money, and it makes the application dependent on a non-JDK external module. By means of the nonblocking socket, you can implement a nonblocking server without directly managing threads or resorting to external modules.

Buffer

Related Reading

Java NIO
By Ron Hitchens

Before we can face the nonblocking socket issue, we have to spend some words about a new Java 1.4 class: java.nio.Buffer. A Buffer instance is merely a limited container of primitive data. It is limited because it can contain a limited number of bytes; in other words, it is not a container, like a Vector or an ArrayList, which theoretically have no limit. In addition, a Buffer instance can only contain items belonging to a Java base type, such as int, char, double, boolean, and so on.

The Buffer class is an abstract class having seven known implementations, one for each base type:

In nonblocking sockets programming, and in general in all contexts where the new I/O system works, it's very important to figure out how Buffer objects work. This is because the new socket channels use Buffer objects to transfer data across the net.

You can create a new Buffer instance by using one of its static methods: allocate, allocateDirect, or wrap. In the following code sample, three Buffer objects will be instantiated in three different ways:

ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
ByteBuffer buffer3 = ByteBuffer.wrap(new String("hello").getBytes());

The first two lines of this code fragment create two ByteBuffer objects, which can each contain 1024 bytes. The allocate and allocateDirect methods work in the same way; the only difference is that the second one directly uses the operating system to allocate the buffer; thus it should provide faster access. Unfortunately, not all virtual machines support the direct-allocation method. The third line uses the wrap method. It creates a ByteBuffer object, which contains the bytes composing the string "hello."

Working with Buffer objects is more or less the same as working with streams. The "current position" concept is an important thing to figure out if you want to deal with the Buffer objects properly. At any time, a buffer has a current position indicating an item. Automatically, after each read or write operation, the current position indicates the next item in the buffer.

To write something to the buffer, you can use the put method:

// Writing on a buffer
IntBuffer buffer = IntBuffer.allocate(10);
  for (int i=0; i < buffer.capacity(); i++) {
  buffer.put(i);
}

The code fragment creates a buffer that can contain 10 integer values. Then, the numbers from 0 to 9 are put in the buffer. As you can see, I use the capacity method to get the capacity of the buffer.

To read the contents of a buffer, you can proceed in the following way:

// Reading from a buffer
buffer.position(0);
while (buffer.hasRemaining()) {
  int i = buffer.get();
  System.out.println("i="+i);
}

Invoking the position method, you can set the current position to 0; that is, at the beginning of the buffer. The hasRemaining method returns true, whether or not there are still elements between the current position and the limit of the buffer. The code inside of the while instruction reads the items by calling the get method and shows them on the console.

It is very important to understand the difference between the limit and the capacity of the buffer. The capacity is the maximum number of items the buffer can contain; the limit is a value, somewhere between 0 and the capacity, representing an arbitrary limitation for the buffer set by using the limit or the flip methods. Take a look at the following example:

// Sample of using flip
buffer.position(5);
buffer.flip();
while (buffer.hasRemaining()) {
  int i = buffer.get();
  System.out.println("i="+i);
}

The current position is set to 5 by the position method. The flip method works like this: it sets the limit to the current position, which is 5; then, it sets the current position again to 0. Therefore, the while loop will scan only the first five elements, because the flip method has set the new limit to 5. Therefore, the shown numbers will be 0, 1, 2, 3, and 4.

Another important method of the Buffer class is clear. It sets the current position to 0 and the limit to the capacity of the buffer. Basically, the clear method annuls the effects provided by previous flip (or limit) invocations. Consider this example:

// Sample of using clear
buffer.clear();
while (buffer.hasRemaining()) {
  int i = buffer.get();
  System.out.println("i="+i);
}

The code above will show the numbers from 0 to 9, independently of the current position and the limit of the buffer.

Nonblocking System Architecture

In this section I explain, in a theoretical way, the architecture of a nonblocking system and how it works. The characters of this comedy (or drama, if you prefer) are the following:

Figure 1 illustrates the architecture of a system using nonblocking sockets.


Figure 1. Nonblocking socket architecture

As you may notice, client applications simultaneously perform requests to the server. The selector collects them, creates the keys, and sends them to the server. This may seem like a blocking system, because the requests are processed one at a time; actually, it is not like that. In fact, each key doesn't represent the entire information stream a client sends to a server, but just a part. We don't have to forget the selector divides the client-data in sub-requests identified by the keys. Consequently, if more clients continuously send data to the server, the selector will create more keys, which will be processed according to a time-sharing policy. To emphasize that, in Figure 1 the keys have the same color of their related clients.

Server Nonblocking

The entities I introduced in the previous section have correspondent Java entities. Client and server are two Java applications. The socket channels are instances of the new SocketChannel class, which allow transferring data through the net. They can be seen as the new sockets for the Java programmer. The SocketChannel class is defined in the java.nio.channel package.

The selector is a Selector class object. Each instance of Selector can monitor more socket channels, and thus more connections. When something interesting happens on the channel (for example, a client attempting a connection or a read/write operation), the selector informs the application to process the request. The selector does that by creating the keys, which are instances of the SelectionKey class. Each key holds information about the application making the request and the type of the request. The type can be one of the following:

A general algorithm to write a nonblocking server might be this:

create SocketChannel;
create Selector
associate the SocketChannel to the Selector
for(;;) {
  waiting events from the Selector;
  event arrived; create keys;
  for each key created by Selector {
    check the type of request;
    isAcceptable:
      get the client SocketChannel;
      associate that SocketChannel  to the Selector;
      record it for read/write operations
      continue;
   isReadable:
      get the client SocketChannel;
      read from the socket;
      continue;
   isWriteable:
      get the client SocketChannel;
      write on the socket;
      continue;
  } 
}

Basically, the server implementation consists of an infinite loop in which the selector waits for events and creates the keys. According to the key-types, an opportune operation is performed. There are four possible types for a key:

Usually an acceptable key is created on the server side. In fact, this kind of key simply informs the server that a client required a connection. In this circumstance, as you can see by the algorithm, the server individuates the socket channel and associates this to the selector for read/write operations. From this moment, when the accepted client reads or writes something, the selector will create readable or writeable keys for that client. Hence, the server will intercept those keys and perform the right actions.

Now you are ready to write the server in Java, following the proposed algorithm. The creation of the socket channel, the selector, and the socket-selector registration can be made in this way:

// Create the server socket channel
ServerSocketChannel server = ServerSocketChannel.open();
// nonblocking I/O
server.configureBlocking(false);
// host-port 8000
server.socket().bind(new java.net.InetSocketAddress(host,8000));
System.out.println("Server attivo porta 8000");
// Create the selector
Selector selector = Selector.open();
// Recording server to selector (type OP_ACCEPT)
server.register(selector,SelectionKey.OP_ACCEPT);

The open static method creates an instance of SocketChannel. The configureBlocking(false) invocation sets the channel as nonblocking. The connection to the server is made by the bind method. The "host" string represents the IP address of the server, and 8000 is the communication port. To create the selector, you can invoke the open static method of the Selector class. Finally, the register method associates the selector to the socket channel.

The second parameter represents the type of the registration. In this case, we use OP_ACCEPT, which means the selector merely reports that a client attempts a connection to the server. Other possible options are: OP_CONNECT, which will be used by the client; OP_READ; and OP_WRITE.

The Java code for the infinite loop is the following:

// Infinite server loop
for(;;) {
  // Waiting for events
  selector.select();
  // Get keys
  Set keys = selector.selectedKeys();
  Iterator i = keys.iterator();

  // For each keys...
  while(i.hasNext()) {
    SelectionKey key = (SelectionKey) i.next();

    // Remove the current key
    i.remove();

    // if isAccetable = true
    // then a client required a connection
    if (key.isAcceptable()) {
      // get client socket channel
      SocketChannel client = server.accept();
      // Non Blocking I/O
      client.configureBlocking(false);
      // recording to the selector (reading)
      client.register(selector, SelectionKey.OP_READ);
      continue;
    }

    // if isReadable = true
    // then the server is ready to read 
    if (key.isReadable()) {

      SocketChannel client = (SocketChannel) key.channel();

      // Read byte coming from the client
      int BUFFER_SIZE = 32;
      ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
      try {
        client.read(buffer);
      }
      catch (Exception e) {
        // client is no longer active
        e.printStackTrace();
        continue;
      }

      // Show bytes on the console
      buffer.flip();
      Charset charset=Charset.forName("ISO-8859-1");
      CharsetDecoder decoder = charset.newDecoder();
      CharBuffer charBuffer = decoder.decode(buffer);
      System.out.print(charBuffer.toString());
      continue;
    }
  }
}

The first line of the loop calls the select method, which blocks the execution and waits for events recorded on the selector. In the code fragment, the socket channel is represented by the server variable. Actually, server is not a SocketChannel object, but a ServerSocketChannel object. This, as well as SocketChannel, is a generalization of SelectableChannel, which is generally used by the server applications.

The event the selector waits for is a client attempting a connection. When this happens, the server application gets the keys created by the selector and for each key, it checks the type. As you may notice, when a key is processed, it has to be removed from the set keys by invoking the remove method. If the type of the key is acceptable (isAcceptable()=true), the server locates the client socket channel by invoking the accept method, sets it as nonblocking, and records it to the selector using the OP_READ option. We could also use the OP_WRITE or OP_READ | OP_WRITE options, but for simplicity, I implemented the server such that it can only read from a channel and cannot write.

The client socket channel is now recorded to the selector for reading operations. Consequently, when the client writes something on the socket channel, the selector will inform the server application that there is something to read. That happens by the creation of a readable key, thus isReadable()=true. At this point, the application reads the data from the socket channel by using a 32-byte ByteBuffer, decodes the bytes using the ISO-8859-1 encoding, and shows them on the server console.

Client Nonblocking

To check if the developed server works properly in a nonblocking way, I will implement a client that continuously writes on the socket channel the string "Client XXX," where "XXX" in a number passed by the command line. For example, while this client is running with the number 89, the server console shows "Client 89 Client 89 Client 89 Client 89 ..." What will happen if another client starts with the number 92? If the server were blocking, nothing would happen; the server application would keep on showing a sequence of "Client 89" strings. Since our server uses nonblocking sockets, the console would show something like this: "Client 89 Client 89 Client 92 Client 89 Client 92 Client 92 Client 89 Client 89 ..." That means the read/write operations on the socket channel don't block the server application execution.

Here is a fragment of the client application code:

// Create client SocketChannel
SocketChannel client = SocketChannel.open();

// nonblocking I/O
client.configureBlocking(false);

// Connection to host port 8000
client.connect(new java.net.InetSocketAddress(host,8000));

// Create selector
Selector selector = Selector.open();

// Record to selector (OP_CONNECT type)
SelectionKey clientKey = client.register(selector, SelectionKey.OP_CONNECT);

// Waiting for the connection
while (selector.select(500)> 0) {

  // Get keys
  Set keys = selector.selectedKeys();
  Iterator i = keys.iterator();

  // For each key...
  while (i.hasNext()) {
    SelectionKey key = (SelectionKey)i.next();

    // Remove the current key
    i.remove();

    // Get the socket channel held by the key
    SocketChannel channel = (SocketChannel)key.channel();

    // Attempt a connection
    if (key.isConnectable()) {

      // Connection OK
      System.out.println("Server Found");

      // Close pendent connections
      if (channel.isConnectionPending())
        channel.finishConnect();

      // Write continuously on the buffer
      ByteBuffer buffer = null;
      for (;;) {
        buffer = 
          ByteBuffer.wrap(
            new String(" Client " + id + " ").getBytes());
        channel.write(buffer);
        buffer.clear();
      }

    }
  }
}

As you will note, the structure of the client application recalls the one you have seen for the server. However, there are some differences. The socket channel is associated to the selector by the option OP_CONNECT, which means the selector will have to inform the client when the server accepts the connection. For the client application, the loop is not infinite. The while condition is:

while (selector.select(500)> 0)

This means that the client attempts the connection for 500 milliseconds; if there's no answer, the select method will return 0 because the server is not active on the channel. Inside of the loop, the client checks if the key is connectable. In this case, the client closes the pending connections, if there are any, and then writes the string "Client" followed by the value of the id variable retrieved by the command line parameters.

Conclusion

The new Java 1.4 I/O system is a big step forward towards carrying out fast, flexible, and scalable Java applications. As shown in this article, by means of the nonblocking socket you can write a nonblocking socket-based application without dealing with threads manually.

Resources

D. Flanagan, "Top Ten Cool New Features of Java 2SE 1.4," O'Reilly Network, March 2002.

J. Zukowski, "New I/O Functionality for Java 2 Standard Edition 1.4," java.sun.com, December 2001.

T. Burns, "Nonblocking Socket I/O in JDK 1.4," Owl Mountain Software, December 2001.

A. Saltarin, "New I/O API in JDK 1.4," MokaByte N59, January 2002.

Giuseppe Naccarato has a degree in computer science and works as software developer for an IT company based in Glasgow (UK). His main interests are J2EE- and .Net-related technologies. Contact Giuseppe at http://www.giuseppe-naccarato.com.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.