|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| Use JSSE and NIO for a quick way to implement non-blocking
communications
Kenneth
Ballard (mailto:kenneth.ballard@ptk.org?cc=&subject=The
easy way to non-blocked sockets) Computer Science undergraduate,
Peru State College 22 October 2003
Although SSL blocking operations -- in
which the socket is blocked from access while data is being read from or
written to -- provide better I/O-error notification than the
non-blocking counterpart, non-blocking operations allow the calling
thread to continue. In this article, the author will cover both the
client and server side as he describes how to create non-blocking secure
connections using the Java Secure Socket Extensions (JSSE) and the Java
NIO (new I/O) library, and he will explain the traditional approach to
creating a non-blocking socket, as well as an alternative (and
necessary) method if you want to use JSSE with
NIO.
To block, or not to block? That is the question. Whether 'tis nobler in
the minds of programmers.... While not Shakespeare, this article brings up
an important point that any programmer should consider when writing an
Internet client. Should the communication operation be of the blocking or
non-blocking variety?
Many programmers don't consider this question when writing an Internet
client using the Java language, primarily because originally there was
only one option -- blocking communication. But now a choice is available
to Java programmers, so perhaps it should be considered for every client
we write.
Non-blocking communication was introduced to the Java language with
version 1.4 of the Java 2 SDK. If you've programmed using this version,
you've probably come across the new I/O (NIO) library. Prior to its
introduction, non-blocking communication could only be used if you
implemented a third-party library, which often had the effect of
introducing bugs into your application.
The NIO library includes non-blocking abilities for files, pipes, and
client and server sockets. A feature the library is missing is secure
non-blocking socket connections. There isn't a secure channel class built
into the NIO or JSSE libraries, but that doesn't mean that you cannot have
secure non-blocking communication. It just involves a little overhead.
To fully appreciate this article, you should be familiar with:
- Java socket communication concepts. You should also have some
practice writing applications. And not just the simple applications that
open a connection, read a line, then quit; programs such as a client or
a communication library that implements a protocol such as POP3 or
HTTP.
- SSL in general and such concepts as cryptography. Basically, know
how to set up a secure connection (but don't worry about JSSE -- this
will be a "crash course" on it).
- The NIO library.
- The Java 2 SDK 1.4 or later installed on the platform of your
choice. (I'm working with version 1.4.1_01 on Windows 98.)
If you need instruction with any of these technologies, see the Resources
section.
So what exactly is blocking and non-blocking communication?
Blocking and non-blocking
communication Blocking communication means that the
communication method is blocking access to the socket while either trying
to access the socket or read or write data. Prior to JDK 1.4, the way to
get around these blocking limitations was to liberally use threads, but
this often created immense thread overhead, which had an impact on the
performance and the scalability of a system. The java.nio package changed
the landscape, allowing servers to effectively use its I/O streams,
handling several client requests in a reasonable amount of time.
With non-blocking communication, the process acts on what I like to
call the "do what you can" concept. Basically, the process will send or
read what it can. If nothing is available to be read, it aborts the read
and the process can do something else until data is available. When
sending data, the process will attempt to send all of the data, but will
return what is actually sent. It can be all, some, or none of the data.
Blocking does have some advantages over non-blocking, especially when
it comes to error control. In blocking socket communication, if any error
results, the method automatically returns with a code identifying the
error. The error could be due to a network timeout, a closed socket, or
any type of I/O error. In non-blocking socket communication, the only
error not processed by the method is a network timeout. To detect timeouts
using non-blocking communication, a little more code must be written to
determine how long it has been since data was last received.
Which type is better depends on the application. If you are using
synchronous communication, blocking communication is better if part of the
data does not have to be processed prior to everything being read while
non-blocking communication will give the opportunity to process any data
already read. Asynchronous communication, such as with IRC and chat
clients, however, will require non-blocking communication to avoid tying
up the socket.
Building a traditional,
non-blocking, client socket The Java NIO library uses
channels instead of streams. The channels can use both blocking and
non-blocking communication, but default to the non-blocking version when
created. But all non-blocking communication goes through a class with
Channel in its name. In the case of socket communication, the
class SocketChannel is used, and the procedure for creating
an object of this class is different from one used in a typical socket, as
shown in Listing 1: Listing 1. Creating and
connecting a SocketChannel object
SocketChannel sc = SocketChannel.open();
sc.connect("www.ibm.com",80);
sc.finishConnect();
|
A pointer of the type SocketChannel must be declared, but
you cannot use the new operator to create the object.
Instead, a static method of the SocketChannel class must be
called to open the channel. After the channel is opened, it can be
connected by calling the connect() method. However, when this
method returns, the socket is not necessarily connected. To be certain
that it is, a follow-up call to finishConnect() must be made.
When the socket is connected, non-blocking communication can commence
using the read() and write() methods of the
SocketChannel class. Or you can cast the object into separate
ReadableByteChannel and WritableByteChannel
objects. Either way, you will be using Buffer objects for the
data. Because use of the NIO library goes beyond the scope of this
article, we won't go any further into this subject.
When the socket is no longer needed, it can be closed using the
close() method:
This will close both the socket connection and the underlying
communication channels.
Building an alternative,
non-blocking, client socket The previous method is slightly
more complicated than the traditional route of creating a socket
connection. However, the traditional route can be used to create a
non-blocking socket, with a few added steps to enable non-blocking
communication.
The underlying communication in a SocketChannel object
involves two Channel classes:
ReadableByteChannel and WritableByteChannel .
These two classes can be created from existing blocking streams,
InputStream and OutputStream respectively, using
the newChannel() method in the Channels class,
as shown in Listing 2: Listing 2. Deriving channels
from streams
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
|
The Channels class is also used to convert channels into
streams or readers and writers. This might appear to switch the
communication over to blocking mode, but it's not the case. If you were to
attempt to read from a stream derived from a channel, the read method
would throw an IllegalBlockingModeException .
The same applies in the opposite direction. You cannot use the
Channels class to convert a stream to a channel and expect
non-blocking communication. If you were to attempt a read from a channel
derived from a stream, it would still be a blocking read. But like so many
things in programming, there are exceptions to this rule.
The exception applies to classes that implement the
SelectableChannel abstract class.
SelectableChannel and its derivatives have the ability to
select either blocking or non-blocking mode. SocketChannel is
one such derivative.
However, in order to be able to switch back and forth between the two,
the interface must be implemented as a SelectableChannel .
With sockets, SocketChannel would need to be used instead of
Socket to enable this ability.
Getting back on track, to create the socket, first build the socket as
you normally would using an object of the Socket class. After
the socket is connected, the streams are converted to channels using the
two lines of code in Listing 2.
Listing 3. Alternate method for creating a
socket
Socket s = new Socket("www.ibm.com", 80);
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
|
As I just explained, this doesn't implement non-blocking socket
communication -- all communication will still be in blocking mode.
Non-blocking communication must be emulated in this kind of setup. Not
much more coding is required for this emulation layer. Let's take a look.
Reading data from the emulation
layer The emulation layer checks the availability of data
prior to attempting a read operation. If data is available, it is read. If
the data is not available, possibly because the socket was closed, it
returns a code signifying such. Notice in Listing 4 that the
ReadableByteChannel is still used for reading, though
InputStream is perfectly capable of performing this action.
The reason? To provide the illusion that NIO is performing the
communication instead of the emulation layer. Plus it makes it much easier
to have both the emulation layer and other channels together, for example,
to write the data to a file channel. Listing 4.
Emulating a non-blocking read operation
/* The checkConnection method returns the character read when
determining if a connection is open.
*/
y = checkConnection();
if(y <= 0) return y;
buffer.putChar((char ) y);
return rbc.read(buffer);
|
Writing data to the emulation
layer With non-blocking communication, write operations will
write only what can be written. The size of the send buffer has a lot to
do with how much data can be sent in one write operation. The buffer's
size can be determined by calling the getSendBufferSize()
method of the Socket object. The size must be taken into
account when attempting a non-blocking write operation. If you try to
write data larger than that block size in length, it must be broken into
multiple write operations. A single, too-large write will be blocked.
Listing 5. Emulating a non-blocking write
operation
int x, y = s.getSendBufferSize(), z = 0;
int expectedWrite;
byte [] p = buffer.array();
ByteBuffer buf = ByteBuffer.allocateDirect(y);
/* If there isn't any data to write, return, otherwise flush the stream */
if(buffer.remaining() == 0) return 0;
os.flush()
for(x = 0; x < p.length; x += y)
{
if(p.length - x < y)
{
buf.put(p, x, p.length - x);
expectedWrite = p.length - x;
}
else
{
buf.put(p, x, y);
expectedWrite = y;
}
/* Check the status of the socket to make sure it's still open */
if(!s.isConnected()) break;
/* Write the data to the stream, flushing immediately afterward */
buf.flip();
z = wbc.write(buf); os.flush();
if(z < expectedWrite) break;
buf.clear();
}
if(x > p.length) return p.length;
else if(x == 0) return -1;
else return x + z;
|
Just like a read operation, first the socket needs to be checked to see
if it is still connected. But if the data is written to a
WritableByteBuffer object, such as in Listing 5, the object
will automatically check this and throw the necessary exception if it is
not connected. Following this action, and before the data is written, the
stream should be immediately flushed to ensure there is room in the send
buffer for sending data. The same applies for any write operation. The
data is sent in blocks equal in length to the send buffer. Doing this
flush will ensure that the send buffer doesn't get overrun, causing the
write operation to block.
Because the write operation is supposed to write only what it can, the
process must also check the socket to make sure it is still open after
each block of data is written. If the socket ever closes while attempting
to write the data, then the write operation must abort and return the
amount of data it was able to send before the socket closed.
BufferedOutputReader cannot be used when emulating
non-blocking writes. If you are attempting to write data that is more than
twice the length of the buffer, all data up to a multiple of the buffer
length is written directly (with the leftover data being buffered). For
example, if the buffer length is 256 bytes and you're attempting to write
529 bytes, the object will flush the current buffer, send 512 bytes, then
store the remaining 17.
For non-blocking writes, this is not what we want. Instead we want the
data to be written in separate writes of the same block size with the
ultimate goal of all data eventually being written. If the data is sent in
one large block with a leftover amount being buffered, the write operation
will block while all data is being sent.
Template of the emulation layer
class The entire emulation layer can be placed inside a
class to allow for easy integration into an application. If this is to be
done, I recommend that the class be derived from ByteChannel .
The class can be cast from ByteChannel into separate
ReadableByteChannel and WritableByteChannel
objects.
Listing 6 illustrates an example class template for the emulation
layer, derived from ByteChannel . This class will be used
throughout the rest of this article to represent non-blocking operations
across blocking connections. Listing 6. Template
of a class for the emulation layer
public class nbChannel implements ByteChannel
{
Socket s;
InputStream is; OutputStream os;
ReadableByteChannel rbc;
WritableByteChannel wbc;
public nbChannel(Socket socket);
public int read(ByteBuffer dest);
public int write(ByteBuffer src);
public void close();
protected int checkConnection();
}
|
Creating a socket using the
emulation layer Using the new emulation layer to create a
socket is very straightforward. Simply create the Socket
object as you normally would, then create the nbChannel
object, as shown in Listing 7: Listing 7. Using the
emulation layer
Socket s = new Socket("www.ibm.com", 80);
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
|
Building a traditional,
non-blocking, server socket Non-blocking sockets on the
server side aren't much different from ones on the client side. There's
just a little more overhead into setting up the socket to accept incoming
connections. The socket must be bound in blocking mode by deriving a
blocking server socket from the server socket channel. The code segment in
Listing 8 outlines how to do it. Listing 8.
Creating a non-blocking server socket (SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(port));
SocketChannel sc = ssc.accept();
|
As with a client socket channel, the server socket channel must be
opened instead of using the new operator and a constructor.
After it is opened, a server socket object must be derived in order to
bind the socket channel to a port. Once the socket is bound, the server
socket object can be discarded.
The channel accepts incoming connections using the
accept() method and routes them to socket channels. Once an
incoming connection is accepted and routed to a socket channel object,
communication can commence through the read() and
write() methods.
Building an alternative non-blocking
server socket Actually, this really isn't an alternative.
Because the server socket channel must be bound using a server socket
object, why not avoid the server socket channel completely and just use a
server socket object? But instead of using SocketChannel for
communication, it'll instead use the emulation layer
nbChannel . Listing 9. Alternate for
setting up a server socket
ServerSocket ss = new ServerSocket(port);
Socket s = ss.accept();
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
|
Creating an SSL
connection In creating an SSL connection, we need to look at
it from the client side and from the server side.
From the client
side The traditional method of creating SSL connections
involves the use of socket factories and a few other things. I won't go
into too much detail on how to create an SSL connection, but there is a
great tutorial entitled, "Secure your sockets with JSSE" (see Resources) that
you can review for more information.
The default method of creating an SSL socket is simple and involves
only a few short steps:
- Create the socket factory.
- Create the connected socket.
- Start the handshake.
- Derive the streams.
- Communicate.
Listing 10 illustrates these steps: Listing 10.
Creating a secure client socket
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
ssl.startHandshake();
InputStream is = ssl.getInputStream();
OutputStream os = ssl.getOutputStream();
|
The default method does not involve client authentication, custom
certificates, and other things that might be necessary for certain
connections.
From the server
side The traditional method of setting up SSL server
connections requires a little overhead, plus a lot of type casting.
Because this goes beyond the scope of this article, I won't go into great
detail, but will instead talk about the default method of enabling SSL
server connections.
Creating a default SSL server socket also involves a few short
steps:
- Create the server socket factory.
- Create and bind the server socket.
- Accept an incoming connection.
- Start the handshake.
- Derive the streams.
- Communicate.
Although this sounds suspiciously similar to the steps on the client
side, note that this removes a lot of security options, such as client
authentication.
Listing 11 illustrates these steps: Listing 11.
Creating a secure server socket
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
ssls.startHandshake();
InputStream is = ssls.getInputStream();
OutputStream os = ssls.getOutputStream();
|
Creating a secure, non-blocking
connection We also need to look at both the client and the
server side when we craft a secure, non-blocking connection.
From the client
side Setting up a secure non-blocking connection on the
client side is straightforward:
- Create and connect the
Socket object.
- Attach the
Socket object to the emulation layer.
- Communicate through the emulation layer.
Listing 12 illustrates these steps: Listing 12.
Creating a secure client connection
/* Create the factory, then the secure socket */
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
/* Start the handshake. Should be done before deriving channels */
ssl.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssl);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
|
Using the emulation layer
class outlined previously, non-blocking secure communication becomes
possible. Because a secure socket channel cannot be opened using the
SocketChannel class and a class for this does not exist in
the Java API, an emulating class was created. The emulating class will
then allow for non-blocking communication when used with either a secure
or non-secure socket connection.
The steps outlined involve a default setup for the security. For more
advanced security, such as custom certificates and client authentication,
the Resources
section offers articles outlining how it can be done.
From the server
side Setting up a socket on the server side requires only a
little more overhead for default security. But once the socket is accepted
and routed, the setup becomes exactly the same as on the client side, as
shown in Listing 13: Listing 13. Creating a
secure, non-blocking server socket
/* Create the factory, then the socket, and put it into listening mode */
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
/* Start the handshake on the new socket */
ssls.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssls);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
|
Again, remember these steps use the default security setup.
Integrating secure and non-secure
client connections Most Internet client applications,
whether written using the Java language or another one, need to provide
for both secure and non-secure connections. The Java Secure Socket
Extensions library makes this easy; it is a method I employed recently in
writing an HTTP client library.
The SSLSocket class is derived from Socket .
You can probably see where I'm going with this. All that is necessary is
just one Socket pointer for the object. If the socket
connection will not use SSL, the socket can be created as you would
normally. If it will use SSL, then there's slightly more overhead, but
after that, the code is straightforward. Listing 14 shows an example:
Listing 14. Integrating secure and non-secure
client connections
Socket s;
ReadableByteChannel rbc;
WritableByteChannel wbc;
nbChannel socketChannel;
if(!useSSL) s = new Socket(host, port);
else
{
SSLSocketFactory sslsf = SSLSocketFactory.getDefault();
SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port);
ssls.startHandshake();
s = ssls;
}
socketChannel = new nbChannel(s);
rbc = (ReadableByteChannel)socketChannel;
wbc = (WritableByteChannel)socketChannel;
...
s.close();
|
Following the creation of the channels, the communication will be
secure if the socket uses SSL or plain if it doesn't. Closing the socket
will cause a terminating handshake if SSL is used.
One possibility for this setup is to have two separate classes. One
class should handle all communication through the socket along with
connecting a non-secure socket. A separate class should handle creating a
secure connection including all necessary setup for the secure connection,
whether default or not. The secure class should plug directly into the
communication class and should only be called if a secure connection is to
be used.
The simplest path to
integration The methods outlined in this article represent
the simplest methods that I know of for integrating both JSSE and NIO into
the same code to provide non-blocking secure communication. While other
methods do exist, they require a lot of time and effort on the part of the
programmer attempting to implement the procedure.
One such option is to implement your own SSL layer atop NIO using the
Java Cryptography Extensions. Another option would be to modify an
existing custom SSL layer called EspreSSL (formerly known as jSSL) to
change it to the NIO library. I would recommend either of these two
options only if you have ample time available.
The downloadable zip file in the Resources
section provides sample code to help you get started practicing the
techniques described in this article, including:
- nbChannel, source code to the emulation layer introduced with
Listing 7
- A simple HTTPS client that connects to Verisign's Web site and
downloads the home page
- A simple, non-blocking, secure server (Secure Daytime Server)
- An integrated secure and non-secure client
Resources
- Download the source code
for this article.
- "Introduction to
Cryptography" serves up the basics of cryptography as an
introduction to SSL (PDF format).
- "Secure Your
Sockets with JSSE" is an introductory guide to employing
JSSE.
- "Merlin brings
non-blocking I/O to the Java platform" (developerWorks,
March 2002) introduces the non-blocking features of Merlin's NIO package
and offers a socket-programming example.
- "The ins and outs
of Merlin's new I/O buffers" (developerWorks,
March 2003) demonstrates how to manipulate data buffers for such tasks
as reading/writing primitives and working with memory-mapped
files.
- "Custom SSL for
advanced JSSE developers" (developerWorks,
September 2002) demonstrates how to program your way around some of the
restrictions of SSL.
- The "Getting started
with new I/O (NIO)" tutorial (developerWorks,
July 2003) covers the NIO library in great detail, from the high-level
concepts to under-the-hood programming detail.
- This IBMJSSE
Overview provides a general journey through the Java Secure Socket
Extension.
- The JSSE reference
guide is an exhaustive resource of the technology.
- Explore the new I/O capabilities of version 1.4 in detail with Java
NIO by Ron Hitchens (O'Reilly & Associates, 2002).
- Java NIO also
includes a support site, JavaNIO.info,
that offers slide presentations of code development, GUI demo tools, and
other supplements to the book.
- Find hundreds of articles and other Java technology-related
resources on the developerWorks
Java technology zone.
About the
author Kenneth Ballard is currently a junior studying
computer science at Peru State College in Peru, Nebraska. He is also
a staff writer for the school's student newspaper, the Peru State
Times. He has an Associate of Science degree in Computer
Programming from Southwestern Community College in Creston, Iowa,
where he worked in a work-study program as a PC technician. His
studies include Java technology, C++, COBOL, Visual Basic, and
networking. Contact Kenneth at kenneth.ballard@ptk.org |
|