|
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 bufferusing 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.)
- Create two ServerSocketChannel instances, one for each port.
- Set each channel to be nonblocking.
- Bind each channel to its respective port.
- Create a java.nio.channels.Selector object.
- Have each channel register itself with the Selector, noting the channel's
interest in "accept" operations.
- Call selector.select(), which blocks until either of the channels receives an
accept event.
- Ask the Selector for the events that happened.
- Iterate the returned list, dealing with each event in turn.
- 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
selectorkind 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 callbecause 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.
|