J2EE

J2SE for the Enterprise Developer, Part 4: SocketChannel and ServerSocketChannel
By Jason Hunter

Part 4 of our series about new capabilities in Java 2 Platform, Standard Edition (J2SE) 1.4 for enterprise developers

In previous installments of this series covering the most important Java 2 Standard Edition (J2SE) 1.4 features for enterprise programmers (see the series index here), I introduced the notion of channels and buffers and offered a thorough overview of the FileChannel class. In this installment, I'll show you the SocketChannel and ServerSocketChannel classes and demonstrate how they enable servers to scale to thousands of clients using the new Selector class and nonblocking I/O.

For network communication, J2SE 1.4 provides two new classes: SocketChannel and ServerSocketChannel. Going beyond the original java.net package and its Socket and ServerSocket classes, these new classes in java.nio.channels have several new features. They allow input and output sides of a socket to be independently shut down without closing the channel; they support asynchronous shutdown; and, most important, they can be used with a Selector for nonblocking I/O, as we'll see later.

SocketChannel and ServerSocketChannel

You can obtain a SocketChannel instance in two ways:

  • On a client, by using the static method SocketChannel.open(InetSocketAddress) to establish a new connection.
  • On a server, by calling the instance method serverSocketChannel.accept() that returns a SocketChannel for a new connection initiated by a client.

The java.net.InetSocketAddress class passed to open() is a new J2SE 1.4 class used to specify a socket address. It's an immutable type holding an IP socket address (such as a hostname and port pair). The following code uses SocketChannel and InetSocketAddress to connect to a remote server and prints the first thing in the server's reply.

  InetSocketAddress addr = new InetSocketAddress(hostname, port);
  SocketChannel ch = SocketChannel.open(addr);

  ByteBuffer buf = ByteBuffer.allocate(1024);
  ch.read(buf);

  buf.flip();

  CharBuffer cbuf = charset.decode(buf);
  System.out.println(cbuf);

The first code line constructs an InetSocketAddress for a given remote hostname and port. The second line uses the static open() method to establish a SocketChannel to this remote host. The third line creates a 1K byte buffer, and the fourth line reads from the channel into the buffer—using the channel/buffer interactions explained in previous articles. After the read, the buffer is flipped to prepare the contents for reading. In the last two lines, the byte buffer contents are converted to a CharBuffer and printed to standard out. We'll cover charsets and byte-to-string decoding in the next article in this series.

Jumping to the server side, the following code demonstrates a simple date/time server using the ServerSocketChannel class. Note that using New I/O (NIO) on one half of a socket does not require NIO on the other side. NIO speaks the same TCP/IP protocol as all other network programs. Here's the code:


  InetSocketAddress addr = new InetSocketAddress(port);
  ServerSocketChannel sch = ServerSocketChannel.open();
  sch.socket().bind(addr);

  SocketChannel ch = sch.accept(); // blocking
  try {
    String now = new Date().toString();
    CharBuffer cbuf = CharBuffer.wrap(now);
    ByteBuffer buf = charset.encode(cbuf);
    ch.write(buf);
    System.out.println("Wrote to " +
      ch.socket().getInetAddress());
  }
  finally { ch.close(); }

This example prints the current date and time to any client that connects. The first line creates an InetSocketAddress for the port on which the server will listen. It constructs the address without a hostname, so we'll listen on the default network interface for the server. The second line creates a ServerSocketChannel instance using the static open() factory method, and the third line binds the channel to the specified address.

Instead of having a bind() method directly on ServerSocketChannel, the class provides a socket() method that exposes the underlying Socket. Accessing and manipulating socket features such as bind(), setReceiveBufferSize(), setSendBufferSize(), setSoLinger(), setSoTimeout(), and so on are always done using socket(). This was deemed better than duplicating the methods directly on the ServerSocketChannel itself.

The accept() call blocks waiting for a client to connect. As soon as a request comes in over the network, accept() returns a SocketChannel. That channel gets used in the try block to construct and write a date, also printing to the console the address of the client that connected. Again, the socket() method exposes the underlying Socket in order to provide socket-related features.

The SocketChannel class presents this interface (simplified for space):

  public class SocketChannel ... {
    SelectableChannel configureBlocking(boolean block);
    SocketChannel open(SocketAddress remote);
 
    read(ByteBuffer dst);
    read(ByteBuffer[] dsts);
    write(ByteBuffer src);
    write(ByteBuffer[] srcs);
 
    Socket socket();
    close();
  }

Most of the methods should look familiar. open() establishes a connection, read() and write() perform network communication, and close() shuts down the connection. The socket() call returns the underlying socket, as discussed above, to manipulate or access properties of the underlying socket. Interestingly, you can get a Socket from a channel but you can't create a SocketChannel from an existing Socket. Contrast this to the FileChannel class, where you can get a channel from a stream but can't get a stream from a channel.

The configureBlocking() method lets you select blocking or nonblocking I/O, a topic covered in the next section. The ServerSocketChannel class presents this interface (again simplified):

  public class ServerSocketChannel ... {
    SelectableChannel configureBlocking(boolean block);
    ServerSocketChannel open();
    ServerSocket socket();
    SocketChannel accept();
    void close()
  }

These methods too should look familiar. You can open() and close() the server socket, accept() a new connection, use socket() to manipulate and access the underlying Socket, and configureBlocking() too.

Nonblocking I/O

Selectors are a mechanism whereby nonblocking channels can register for notification when events happen. Formerly all I/O in Java was blocking and required a thread to sit on the socket waiting for action. C and C++ programmers laughed at Java's waste while they used select() and WaitForSingleEvent() to write scalable servers often using a single thread. Java programmers now can stand proud with the introduction of the Selector.

The Selector class uses the "reactor" model to decouple event arrival from event handling. A Selector collects specified events and makes them available for handling later upon request. The Java docs call the Selector class "a rendezvous point for I/O," and it's an apt description. Think of a bartender with five waiters taking drink orders. Formerly Java would have needed five bartenders, one for each waiter. Selectors let Java use just one bartender.

Here's a nine-step recipe for creating a single-threaded server that can listen on two ports. (Yes, for people who learned to program using Java, such a thing is quite possible.)

  1. Create two ServerSocketChannel instances, one for each port.
  2. Set each channel to be nonblocking.
  3. Bind each channel to its respective port.
  4. Create a java.nio.channels.Selector object.
  5. Have each channel register itself with the Selector, noting the channel's interest in "accept" operations.
  6. Call selector.select(), which blocks until either of the channels receives an accept event.
  7. Ask the Selector for the events that happened.
  8. Iterate the returned list, dealing with each event in turn.
  9. Remove each handled event from the active list.

We'll take the code in steps. Here's the first step, covering the first three recipe items:

  InetSocketAddress addr1 = new InetSocketAddress(5001);
  ServerSocketChannel sc1 = ServerSocketChannel.open();
  sch1.configureBlocking(false);
  sch1.socket().bind(addr1);

  InetSocketAddress addr2 = new InetSocketAddress(5002);
  ServerSocketChannel sc2 = ServerSocketChannel.open();
  sch2.configureBlocking(false);
  sch2.socket().bind(addr2);

This code creates a pair of server sockets on ports 5001 and 5002. The configureBlocking(false) call makes sure that later, when we call accept(), the call won't block. This is what lets a single thread manage both server socket channels. Now for the next two items in the recipe:

  Selector selector = Selector.open();
  sch1.register(selector, SelectionKey.OP_ACCEPT);
  sch2.register(selector, SelectionKey.OP_ACCEPT);

Here we create a new Selector (just one) and register each channel into the Selector. After this, all channel events will get reported to the Selector so our single thread can ask the Selector what's happened, effectively listening on two ports.

The first parameter to the register() call is the selector instance. The second is a bitmask for which operations should be noted. Supported operations are OP_ACCEPT, OP_CONNECT, OP_READ, and OP_WRITE. In this code, we use OP_ACCEPT because we want to watch accept operations. OP_READ and OP_WRITE would let us know when a socket is ready for reading or writing operations. Take a second and see if you can figure out why one might use OP_CONNECT.

Answer: It's to help in establishing a new client socket connection. Imagine you have a client that needs to send a message to 1,000 different machines. It takes about a second to establish a connection on the internet. In a single threaded blocking I/O environment, it will take 1,000 seconds to accomplish this task. Using nonblocking I/O, the connections can be established concurrently with a Selector observing the OP_CONNECT events. This executes much more efficiently.

The register() method accepts an optional third argument, an Object, which can be anything but is held along with the registration. It's referred to as an "attribute." Later, during event handling, this attribute can be retrieved and used. For example, with our messaging client, the object could be the message to send. It'll be readily available after the connection gets established.

It may look odd to have the register() method on the channel instead of on the selector—kind of like having the put() method on an object instead of on the Hashtable. It's been supposed that this design better supports the service provider interface (SPI) and makes it easier to plug in new channel and selector implementations. In truth, it's this way simply because it had to be one way or the other and the specification lead just thought this way seemed more natural. Another mystery solved.

Now to end the recipe:

  while (selector.select() > 0) {
    Set keys = selector.selectedKeys();
    Iterator i = keys.iterator();
    while (i.hasNext()) {
      SelectionKey key = (SelectionKey)i.next();
      ServerSocketChannel sch =
        (ServerSocketChannel)key.channel();
      SocketChannel ch = sch.accept();
      handleClient(ch);
      i.remove();
    }
  }

The first line calls selector.select(). This method blocks until a registered event happens, at which point it returns and gives as a return value the number of events that occurred. In our loop we first ask the selector for the "selected keys"—the list of "hot" events as a Set of SelectionKey objects. For each SelectionKey, we fetch the underlying ServerSocketChannel and then call accept() on the channel. We know the accept() will return immediately because the key was in the "selected keys" list, and in this case the Selector was only configured for OP_ACCEPT operations. Once getting a SocketChannel to communicate with the client, we handle the communication. Here we show that with a call to handleClient(ch), our own theoretical method that's responsible for communication. A method like handleConnection() should either return quickly or dispatch to another thread so that processing for the next connection isn't held up.

Lastly, we remove the SelectionKey from the selected keys set. This indicates to the Selector that we've fully handled the event. We leave any event that's not appropriate for us to handle in the selected keys set for some other code to deal with on a later call to selectedKeys().

Now, another stumper question: Why do we check if select() returns greater than 0? We'll explain that in the next section.

The Selector and SelectionKey Classes

The Selector class acts as a multiplexer of selectable channel objects. Its interface looks like this (simplified):

  public class Selector {
    Selector open();
    void close();
    boolean isOpen();

    Set keys();          // all registered keys
    Set selectedKeys();  // selected keys

    int select();        // block indefinitely
    int select(long timeout);
    int selectNow();     // non-blocking

    Selector wakeup();   // wakes up first blocked
  }

We see the standard open() and close() methods and an isOpen() method to determine if the Selector has been closed already. The keys() call returns all registered keys. The selectedKeys() method returns only those keys that are selected or "hot." The keys() call is immutable, but selectedKeys() has to be mutable in order to mark an event as being handled.

The class provides three styles of select(). The simple no-argument form blocks indefinitely. We used this in the last example. The select(long) form blocks for a specified number of milliseconds, after which it will return even if no events occurred. This form allows a single thread to switch back and forth between two selectors, for example. The selectNow() method is always nonblocking and returns immediately, the same as select(0). If nothing happened since the last select call, it just returns 0 indicating no events.

The wakeup() method wakes up the first blocked selector and causes it to return immediately. This call becomes very important when changing a Selector's configuration. Changes made during a select call don't take effect until the next select call—because at the lowest level Java's select() method passes through the call to the operating system's underlying select function, which of course doesn't care one whit about changes happening in Java. Thus, you must wakeup() a selector after making changes so the operating system call can be reissued. The wakeup() method wakes up just the "first" blocked selector, so if two threads block on the same selector the call will affect only one at a time.

The potential for a wakeup() is the answer to our earlier question and why our while loop in the past example checked the select() return value even though it used the indefinitely blocking form. Should the select be awakened before any event happens, it will return 0 and we can efficiently skip the loop. Instead the loop will go right back into a select() with the new configuration.

The SelectionKey class presents this interface:

  public class SelectionKey {
    static final int OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE;
    int interestOps();
    SelectionKey interestOps(int ops);
    int readyOps();
    boolean isAcceptable();
    boolean isConnectable();
    boolean isReadable();
    boolean isWritable();

    SelectableChannel cancel();   // cancels reg
    Object attachment();  // retrieves attachment

    SelectableChannel channel();  // gets handle
    Selector selector();          // gets handle
  }

At the top of this code we see the four operation types that can be registered. The interestOps() getter method returns the bitmask for the operations currently registered. The setter method (the form that takes an int argument) allows you to assign a new set. For example:

  key.interestOps(OP_READ | OP_WRITE);

The readyOps() method returns the bitmask of which operations are currently "hot." The following code example would check if a channel is ready for both reading and writing:

  int mask = key.readyOps();
  boolean both = mask & key.OP_READ && mask & key.OP_WRITE;

Because this code isn't so readable, the four "is" methods let you query more easily:

  boolean both = key.isReadable() && key.isWritable();

The cancel() method cancels the key's registration in a Selector. Remember, this won't take effect until any currently blocked select() calls return. The attachment() method returns any third-argument attachment passed during the key registration, like the message to send once the connection was established. The channel() and selector() methods at the bottom are simple accessors to the channel and selector joined by this key.

Putting the SelectionKey class to work, the following code iterates over the registered keys, removes any read ops, and then cancels the registration if there are no remaining ops:

  Set allKeys = selector.keys(); // immutable
  Iterator i = allKeys.iterator();
  while (i.hasNext()) {
    SelectionKey key = (SelectionKey)i.next();
    key.interestOps(
      key.interestOps() & ˜SelectionKey.OP_READ);
    if (key.interestOps() == 0) {
      key.cancel();
    }
  }

The selector.keys() call returns all the registered keys, not just the "hot" ones. It's an immutable set. For each key in that set, we set its interestOps() to be the former interestOps() minus the OP_READ bit. We use bitwise math to accomplish this. Then, we check if the interestOps() mask returns 0 (no registrations), and if so we cancel() the key. Because the keys set is immutable, you use cancel() to remove a key from the set. Just remember, after cancel() any currently blocked select() calls won't directly notice the cancellation.

The last class to look at is SelectableChannel. You may have noticed this class type returned by the configureBlocking() method. It's a supertype of both SocketChannel and ServerSocketChannel and provides these classes with their "selectablity." Any class that extends SelectableChannel can be used for nonblocking I/O. By looking at the class hierarchy you can tell that FileChannel does not support nonblocking I/O while the socket channels do, as do DatagramChannel, Pipe.SinkChannel, and Pipe.SourceChannel. By extending SelectableChannel and overriding its methods properly, you can write your own nonblocking channels.

The SelectableChannel class contains these methods:

  public class SelectableChannel {
    SelectableChannel configureBlocking(boolean block);

    SelectionKey register(Selector sel, int ops);
    SelectionKey register(Selector sel, int ops, Object att);
    SelectionKey keyFor(Selector sel);

    int validOps();  // ones you can set
    boolean isRegistered();
  }

You saw the configureBlocking() and register() methods earlier with SocketChannel and ServerSocketChannel. Although I showed them as part of those classes, the methods were actually inherited from SelectableChannel.

The keyFor() method lets you query a Selector to find the SelectionKey for the channel. Thus, even though the register() methods return the SelectionKey instance, it's easy to ignore that and just ask for the key later when needed.

The validOps() method returns a mask for the operations appropriate for this channel. For ServerSocketChannel the only valid operation is OP_ACCEPT. The last isRegistered() method returns whether the channel has currently been assigned to any selectors.

Putting Nonblocking I/O to Use

Now that we've seen how J2SE 1.4 and NIO provide support for nonblocking I/O, we must ask ourselves where we can apply this new ability.

The obvious server-side program to leverage nonblocking I/O is a chat server. Chat servers are usually responsible for maintaining thousands of open connections to different clients and efficiently noticing new messages to be exchanged. Java servers in the past needed one thread per connected client in order to watch for new communications from that client. Perhaps you remember the VolanoMark server JVM benchmark put out by the makers of VolanoChat? A good score required massive thread scalability, and the benchmark pushed JVM vendors (and OS vendors) to scale threads to a level never seen before. With nonblocking I/O, such gargantuan thread piles aren't necessary, and even more clients can be handled with fewer server demands.

Most of us aren't building chat servers, however. We're writing J2EE applications. What benefit is NIO in this situation? At this point it's actually less than you might think. J2EE 1.4 and earlier were designed and developed before NIO. (More accurately, they were done before J2SE 1.4 would have been acceptable as a required base.) Thus, the J2EE APIs don't expose the channel metaphor directly. Servlets still communicate with their clients using streams, not channels. There's no response.getChannel() method. But NIO still has its place, and it's on the implementation side.

First, NIO supports better server-side component code. Memory mapped files and file locking alone are a boon to server-side, non-EJB programs. Add in nonblocking I/O and you have a new ability for back-end code to more effectively communicate with other back-end code.

Second, NIO supports better J2EE server implementations. Even though channels can't be exposed to servlets, that doesn't mean a servlet container can't leverage NIO and nonblocking I/O to optimize its performance. Imagine you're writing a servlet engine: Where would nonblocking I/O provide benefit? Somewhat surprisingly, it wouldn't help you increase the number of new client connections you could establish. Even without NIO, an HTTP server can listen on port 80 using a single thread, dispatching quickly after connection to a different request handling thread.

The place where NIO provides benefit is in managing already established connections. Keep-Alive connections. Most HTTP servers, in order to improve performance, keep connections open with their clients for a short length of time (i.e., 30 seconds) in case the client makes another request for images or pages. In fact, with HTTP/1.1 this is the default unless the client requests otherwise. Assuming 50 new clients per second and a 30-second Keep-Alive timeout setting, the server will need to listen concurrently to 1,500 "kept alive" socket connections. Add in one more for port 80 or 8080, and it's quite a load. Now, using NIO, instead of 1,501 threads required you need just one. The same thread listening on port 80 can track all the 1,500 "kept alive" sockets using a Selector.

Conclusion

The SocketChannel and ServerSocketChannel classes provide a new metaphor for network computing, one that provides support for nonblocking I/O. Java has now satisfied the ancient proverb, "A server with a thousand clients can run with a single thread."


Jason Hunter (jasonhunter@servlets.com) is a consultant and publisher of Servlets.com. He is the author of Java Servlet Programming and coauthor of Java Enterprise Best Practices (both from O'Reilly).

Coming up next time in this J2SE 1.4 series:
Java's new library for regular expressions and the new Charset class for advanced and pluggable internationalization.



Please rate this document:

Excellent Good Average Below Average Poor


Send us your comments

E-mail this page
Printer View Printer View
Oracle Is The Information Company About Oracle | Oracle RSS Feeds | Careers | Contact Us | Site Maps | Legal Notices | Terms of Use | Privacy