Java 1.4 introduces an entirely new API for high-performance, nonblocking I/O and networking. This API consists
primarily of three new packages. java.nio
defines Buffer classes that are used to store
sequences of bytes or other primitive values.
java.nio.channels defines
channels through which data can be transferred
between a buffer and a data source or sink, such as a file or
a network socket. This package also contains important classes
used for nonblocking I/O. Finally, the
java.nio.charset package contains classes for
efficiently converting buffers of bytes into buffers of
characters. The subsections that follow contain examples of
using all three of these packages, as well as examples of specific
I/O tasks with the New I/O API.
Basic Buffer Operations
The java.nio package includes an abstract
Buffer class, which defines generic operations
on buffers. This package also defines type-specific subclasses such as
ByteBuffer , CharBuffer , and
IntBuffer .
(See Buffer and ByteBuffer
in for details on these classes and their
various methods.) The following code illustrates typical sequences
of buffer operations on a ByteBuffer .
The other type-specific buffer classes have similar methods.
import java.nio.*;
// Buffers don't have public constructors. They are allocated instead. ByteBuffer b = ByteBuffer.allocate(4096); // Create a buffer for 4,096 bytes // Or do this to try to get an efficient buffer from the low-level OS ByteBuffer buf2 = ByteBuffer.allocateDirect(65536); // Here's another way to get a buffer: by "wrapping" an array byte[] data; // Assume this array is created and initialized elsewhere ByteBuffer buf3 = ByteBuffer.wrap(data); // Create buffer that uses the array // It is also possible to create a "view buffer" to view bytes as other types buf3.order(ByteOrder.BIG_ENDIAN); // Specify the byte order for the buffer IntBuffer ib = buf3.asIntBuffer(); // View those bytes as integers
// Now store some data in the buffer b.put(data); // Copy bytes from array to buffer at current position b.put((byte)42); // Store another byte at the new current position b.put(0, (byte)9); // Overwrite first byte in buffer. Don't change position. b.order(ByteOrder.BIG_ENDIAN); // Set the byte order of the buffer b.putChar('x'); // Store the two bytes of a Unicode character in buffer b.putInt(0xcafebabe); // Store four bytes of an int into the buffer
// Here are methods for querying basic numbers about a buffer int capacity = b.capacity(); // How many bytes can the buffer hold? (4,096) int position = b.position(); // Where will the next byte be written or read? // A buffer's limit specifies how many bytes of the buffer can be used. // When writing into a buffer, this should be the capacity. When reading data // from a buffer, it should be the number of bytes that were previously // written. int limit = b.limit(); // How many should be used? int remaining = b.remaining(); // How many left? Return limit-position. boolean more=b.hasRemaining(); // Test if there is still room in the buffer
// The position and limit can also be set with methods of the same name // Suppose you want to read the bytes you've written into the buffer b.limit(b.position()); // Set limit to current position b.position(0); // Set limit to 0; start reading at beginning
// Instead of the two previous calls, you usually use a convenience method b.flip(); // Set limit to position and position to 0; prepare for reading b.rewind(); // Set position to 0; don't change limit; prepare for rereading b.clear(); // Set position to 0 and limit to capacity; prepare for writing
// Assuming you've called flip(), you can start reading bytes from the buffer buf2.put(b); // Read all bytes from b and put them into buf2 b.rewind(); // Rewind b for rereading from the beginning byte b0 = b.get(); // Read first byte; increment buffer position byte b1 = b.get(); // Read second byte; increment buffer position byte[] fourbytes = new byte[4]; b.get(fourbytes); // Read next four bytes, add 4 to buffer position byte b9 = b.get(9); // Read 10th byte, without changing current position int i = b.getInt(); // Read next four bytes as an integer; add 4 to position
// Discard bytes you've already read; shift the remaining ones to the beginning // of the buffer; set position to new limit and limit to capacity, preparing // the buffer for writing more bytes into it. b.compact();
You may notice that many buffer methods return the object on which they
operate. This is done so that method calls can be "chained"
in code, as follows:
ByteBuffer bb=ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN).putInt(1234);
Many methods throughout
java.nio and its subpackages return the current object to
enable this kind of method chaining. Note that the use of this
kind of chaining is a stylistic choice (which I have avoided in
this chapter) and does not have any significant impact on
efficiency.
ByteBuffer is the most important of the
buffer classes. However, another commonly used class is
CharBuffer . CharBuffer
objects can be created by wrapping a string and can also be
converted to strings. CharBuffer
implements the new java.lang.CharSequence
interface, which means that it can be used like a
String or StringBuffer in
certain applications, (e.g., for regular expression pattern
matching.
// Create a read-only CharBuffer from a string CharBuffer cb = CharBuffer.wrap("This string is the data for the CharBuffer"); String s = cb.toString(); // Convert to a String with toString() method System.out.println(cb); // or rely on an implicit call to toString(). char c = cb.charAt(0); // Use CharSequence methods to get characters char d = cb.get(1); // or use a CharBuffer absolute read. // A relative read that reads the char and increments the current position // Note that only the characters between the position and limit are used when // a CharBuffer is converted to a String or used as a CharSequence. char e = cb.get();
Bytes in a ByteBuffer are commonly converted to characters in a
CharBuffer and vice versa. We'll see how to do this when
we consider the java.nio.charset package.
Basic Channel Operations
Buffers are not all that useful on their own -- there isn't
much point in storing bytes into a buffer only to read them out
again. Instead, buffers are typically used with channels:
your program stores bytes into a buffer, then passes the buffer
to a channel, which reads the bytes out of the buffer and writes
them to a file, network socket, or some other destination. Or,
in the reverse, your program passes a buffer to a channel, which
reads bytes from a file, socket, or other source, and stores
those bytes into the buffer, where they can then be retrieved by
your program. The java.nio.channels package
defines several channel classes that represent files, sockets,
datagrams, and pipes. (We'll see examples of these
concrete classes later in this chapter.) The following code, however,
is based on the capabilities of the various channel interfaces
defined by java.nio.channels and should work
with any Channel object:
Channel c; // Object that implements Channel interface; initialized elsewhere if (c.isOpen()) c.close(); // These are the only methods defined by Channel
// The read() and write() methods are defined by the // ReadableByteChannel and WritableByteChannel interfaces. ReadableByteChannel source; // Initialized elsewhere WritableByteChannel destination; // Initialized elsewhere ByteBuffer buffer = ByteBuffer.allocateDirect(16384); // Low-level 16 KB buffer
// Here is the basic loop to use when reading bytes from a source channel // and writing them to a destination channel until there are no more bytes to // read from the source and no more buffered bytes to write to the destination. while(source.read(buffer) != -1 || buffer.position() > 0) { // Flip buffer: set limit to position and position to 0. This prepares // the buffer for reading (which is done by a channel *write* operation). buffer.flip(); // Write some or all of the bytes in the buffer to the destination destination.write(buffer); // Discard the bytes that were written, copying the remaining ones to // the start of the buffer. Set position to limit and limit to capacity, // preparing the buffer for writing (done by a channel *read* operation). buffer.compact(); }
// Don't forget to close the channels source.close(); destination.close();
In addition to the ReadableByteChannel and
WritableByteChannel interfaces illustrated in
the preceding code, java.nio.channels
defines several other channel interfaces.
ByteChannel simply extends the readable and
writable interfaces without adding any new methods. It is a
useful shorthand for channels that support both reading and
writing. GatheringByteChannel is an
extension of WritableByteChannel that defines
write() methods that gather bytes from more than
one buffer and write them out. Similarly,
ScatteringByteChannel is an extension of
ReadableByteChannel that defines
read() methods that read bytes from the
channel and scatter or distribute them into more than one
buffer. The gathering and scattering write()
and read() methods can be useful when working
with network protocols that use fixed-size headers that you want
to store in a buffer separate from the rest of the transferred
data.
One confusing point to be aware of is that a channel read
operation involves writing (or putting) bytes into a buffer, and
a channel write operation involves reading (or getting) bytes
from a buffer. Thus, when I say that the
flip() method prepares a buffer for reading,
I mean that it prepares a buffer for use in a channel
write() operation! The reverse is true for
the buffer's compact() method.
Encoding and Decoding Text with Charsets
A java.nio.charset.Charset object represents
a character set plus an encoding for that character set.
Charset and its associated classes,
CharsetEncoder and
CharsetDecoder , define methods for encoding
strings of characters into sequences of bytes and decoding
sequences of bytes into strings of characters. Since these
classes are part of the New I/O API, they use the
ByteBuffer and CharBuffer
classes:
// The simplest case. Use Charset convenience routines to convert. Charset charset = Charset.forName("ISO-8859-1"); // Get Latin-1 Charset CharBuffer cb = CharBuffer.wrap("Hello World"); // Characters to encode // Encode the characters and store the bytes in a newly allocated ByteBuffer ByteBuffer bb = charset.encode(cb); // Decode these bytes into a newly allocated CharBuffer and print them out System.out.println(charset.decode(bb));
Note the use of the ISO-8859-1 (a.k.a. "Latin-1") charset in this
example. This 8-bit charset is suitable for most Western
European languages, including English. Programmers who work only with English may also use the 7-bit "US-ASCII"
charset. The Charset class does not do
encoding and decoding itself, and the previous convenience routines create CharsetEncoder and
CharsetDecoder classes internally. If you
plan to encode or decode multiple times, it is more
efficient to create these objects yourself:
Charset charset = Charset.forName("US-ASCII"); // Get the charset CharsetEncoder encoder = charset.newEncoder(); // Create an encoder from it CharBuffer cb = CharBuffer.wrap("Hello World!"); // Get a CharBuffer WritableByteChannel destination; // Initialized elsewhere destination.write(encoder.encode(cb)); // Encode chars and write
The preceding CharsetEncoder.encode() method must allocate a new ByteBuffer each
time it is called. For maximum efficiency, there are
lower-level methods you can call to do the encoding and decoding
into an existing buffer:
ReadableByteChannel source; // Initialized elsewhere Charset charset = Charset.forName("ISO-8859-1"); // Get the charset CharsetDecoder decoder = charset.newDecoder(); // Create a decoder from it ByteBuffer bb = ByteBuffer.allocateDirect(2048); // Buffer to hold bytes CharBuffer cb = CharBuffer.allocate(2048); // Buffer to hold characters
while(source.read(bb) != -1) { // Read bytes from the channel until EOF bb.flip(); // Flip byte buffer to prepare for decoding decoder.decode(bb, cb, true); // Decode bytes into characters cb.flip(); // Flip char buffer to prepare for printing System.out.print(cb); // Print the characters cb.clear(); // Clear char buffer to prepare for decoding bb.clear(); // Prepare byte buffer for next channel read } source.close(); // Done with the channel, so close it System.out.flush(); // Make sure all output characters appear
The preceding code relies on the fact that ISO-8895-1 is
an 8-bit encoding charset and that there is one-to-one mapping between
characters and bytes. For more complex charsets, such as the
UTF-8 encoding of Unicode or the EUC-JP charset used with
Japanese text, however, this does not hold, and more than one
byte is required for some (or all) characters. When this is the
case, there is no guarantee that all bytes in a buffer can be
decoded at once (the end of the buffer may contain a partial
character). Also, since a single character may encode to more
than one byte, it can be tricky to know how many bytes a given
string will encode into. The following code shows a loop you can
use to decode bytes in a more general way:
ReadableByteChannel source; // Initialized elsewhere Charset charset = Charset.forName("UTF-8"); // A Unicode encoding CharsetDecoder decoder = charset.newDecoder(); // Create a decoder from it ByteBuffer bb = ByteBuffer.allocateDirect(2048); // Buffer to hold bytes CharBuffer cb = CharBuffer.allocate(2048); // Buffer to hold characters
// Tell the decoder to ignore errors that might result from bad bytes decoder.onMalformedInput(CodingErrorAction.IGNORE); decoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
decoder.reset(); // Reset decoder if it has been used before while(source.read(bb) != -1) { // Read bytes from the channel until EOF bb.flip(); // Flip byte buffer to prepare for decoding decoder.decode(bb, cb, false); // Decode bytes into characters cb.flip(); // Flip char buffer to prepare for printing System.out.print(cb); // Print the characters cb.clear(); // Clear the character buffer bb.compact(); // Discard already decoded bytes } source.close(); // Done with the channel, so close it
// At this point, there may still be some bytes in the buffer to decode bb.flip(); // Prepare for decoding decoder.decode(bb, cb, true); // Pass true to indicate this is the last call decoder.flush(cb); // Output any final characters cb.flip(); // Flip char buffer System.out.print(cb); // Print the final characters
Working with Files
FileChannel is a concrete
Channel class that performs file I/O and
implements ReadableByteChannel and
WritableByteChannel (although its
read() method works only if the underlying
file is open for reading, and its write()
method works only if the file is open for writing). Obtain a
FileChannel object by using the
java.io package to create a
FileInputStream , a
FileOutputStream , or a
RandomAccessFile , and then call the getChannel() method (new in Java 1.4) of that
object. As an example, you can use two
FileChannel objects to copy a file with code
such as the following:
String filename = "test"; // The name of the file to copy // Create streams to read the original and write the copy FileInputStream fin = new FileInputStream(filename); FileOutputStream fout = new FileOutputStream(filename + ".copy"); // Use the streams to create corresponding channel objects FileChannel in = fin.getChannel(); FileChannel out = fout.getChannel(); // Allocate a low-level 8KB buffer for the copy ByteBuffer buffer = ByteBuffer.allocateDirect(8192); while(in.read(buffer) != -1 || buffer.position() > 0) { buffer.flip(); // Prepare to read from the buffer and write to the file out.write(buffer); // Write some or all buffer contents buffer.compact(); // Discard all bytes that were written and prepare to } // read more from the file and store them in the buffer. in.close(); // Always close channels and streams when done with them out.close(); fin.close(); // Note that closing a FileChannel does not automatically fout.close(); // close the underlying stream.
FileChannel has special
transferTo() and
transferFrom() methods that make it
particularly easy (and on many operating systems, particularly
efficient) to transfer a specified number of bytes from a
FileChannel to some other specified channel,
or from some other channel to a FileChannel .
These methods allow us to simplify the preceding file-copying code
to the following:
FileChannel in, out; // Assume these are initialized as in the // preceding example. long numbytes = in.size(); // Number of bytes in original file in.transferTo(0, numbytes, out); // Transfer that amount to output channel
This code could be equally well-written using
transferFrom() instead of
transferTo() (note that these two methods
expect their arguments in different orders):
long numbytes = in.size(); out.transferFrom(in, 0, numbytes);
FileChannel also has other capabilities that
are not shared by other channel classes. One of the most important
is the ability to "memory map" a file or a portion of a
file, i.e., to obtain a MappedByteBuffer (a
subclass of ByteBuffer ) that represents the
contents of the file and allows you to read (and optionally
write) file contents simply by reading from and writing to the
buffer. Memory mapping a file is a somewhat expensive
operation, so this technique is usually efficient only when
you are working with a large file to which you need repeated access. Memory mapping offers you yet another way to perform the
same file-copy operation shown previously:
long filesize = in.size(); ByteBuffer bb = in.map(FileChannel.MapMode.READ_ONLY, 0, filesize); while(bb.hasRemaining()) out.write(bb);
The channel interfaces defined by
java.nio.channels include
ByteChannel but not
CharChannel . The channel API is low-level
and provides methods for reading bytes only. All of the
previous examples have treated files as binary files. It is
possible to use the CharsetEncoder and
CharsetDecoder classes introduced earlier to
convert between bytes and characters, but when you want to work
with text files, the Reader and
Writer classes of the
java.io package are usually much easier to
use than CharBuffer . Fortunately, the
Channels class defines convenience methods
that bridge between the new and old APIs. Here is code that
wraps a Reader and a
Writer object around input and output
channels, reads lines of Latin-1 text from the input channel, and
writes them back out to the output channel, with the encoding
changed to UTF-8:
ReadableByteChannel in; // Assume these are initialized elsewhere WritableByteChannel out; // Create a Reader and Writer from a FileChannel and charset name BufferedReader reader=new BufferedReader(Channels.newReader(in, "ISO-8859-1")); PrintWriter writer = new PrintWriter(Channels.newWriter(out, "UTF-8")); String line; while((line = reader.readLine()) != null) writer.println(line); reader.close(); writer.close();
Unlike the FileInputStream and
FileOutputStream classes, the
FileChannel class allows random access to the
contents of the file. The zero-argument
position() method returns the file pointer (the position of the next byte to be read), and the one-argument
position() method allows you to set this
pointer to any value you want. This allows you to skip around
in a file in the way that the
java.io.RandomAccessFile does. Here is an
example:
// Suppose you have a file that has data records scattered throughout, and the // last 1,024 bytes of the file are an index that provides the position of // those records. Here is code that reads the index of the file, looks up the // position of the first record within the file, and then reads that record. FileChannel in = new FileInputStream("test.data").getChannel(); // The channel ByteBuffer index = ByteBuffer.allocate(1024); // A buffer to hold the index long size = in.size(); // The size of the file in.position(size - 1024); // Position at start of index in.read(index); // Read the index int record0Position = index.getInt(0); // Get first index entry in.position(record0Position); // Position file at that point ByteBuffer record0 = ByteBuffer.allocate(128); // Get buffer to hold data in.read(record0); // Finally, read the record
The final feature of FileChannel that we'll
consider here is its ability to lock a file or a portion of
a file against all concurrent access (an exclusive lock) or
against concurrent writes (a shared lock). (Note that some
operating systems strictly enforce all locks, while others only
provide an advisory locking facility that requires programs to
cooperate and to attempt to acquire a lock before reading or
writing portions of a shared file.) In the previous random-access
example, suppose we wanted to ensure that no other program
was modifying the record data while we read it. We could acquire
a shared lock on that portion of the file with the following code:
FileLock lock = in.lock(record0Position, // Start of locked region 128, // Length of locked region true); // Shared lock: prevent concurrent updates // but allow concurrent reads. in.position(record0Position); // Move to start of index in.read(record0); // Read the index data lock.release(); // You're done with the lock, so release it
Client-Side Networking
The New I/O API includes networking capabilities as well as
file-access capabilities. To communicate over the network, you
can use the SocketChannel class. Create a
SocketChannel with the static
open() method, then read and write bytes
from and to it as you would with any other channel object. The following
code uses SocketChannel to send an HTTP
request to a web server and saves the server's response
(including all of the HTTP headers) to a file. Note the use of
java.net.InetSocketAddress , a subclass of
java.net.SocketAddress , to tell the
SocketChannel what to connect to. These
classes are also new in Java 1.4 and were introduced as part of
the New I/O API.
import java.io.*; import java.net.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*;
// Create a SocketChannel connected to the web server at www. SocketChannel socket = SocketChannel.open(new InetSocketAddress("www.",80)); // A charset for encoding the HTTP request Charset charset = Charset.forName("ISO-8859-1"); // Send an HTTP request to the server. Start with a string, wrap it to // a CharBuffer, encode it to a ByteBuffer, then write it to the socket. socket.write(charset.encode(CharBuffer.wrap("GET / HTTP/1.0\r\n\r\n"))); // Create a FileChannel to save the server's response to FileOutputStream out = new FileOutputStream("oreilly.html"); FileChannel file = out.getChannel(); // Get a buffer for holding bytes while transferring from socket to file ByteBuffer buffer = ByteBuffer.allocateDirect(8192); // Now loop until all bytes are read from the socket and written to the file while(socket.read(buffer) != -1 || buffer.position() > 0) { // Are we done? buffer.flip(); // Prepare to read bytes from buffer and write to file file.write(buffer); // Write some or all bytes to the file buffer.compact(); // Discard those that were written } socket.close(); // Close the socket channel file.close(); // Close the file channel out.close(); // Close the underlying file
Another way to create a SocketChannel is with
the no-argument version of open() , which
creates an unconnected channel. This allows you to call the
socket() method to obtain the underlying
socket, configure the socket as desired, and connect to the
desired host with the connect method. For example:
SocketChannel sc = SocketChannel.open(); // Open an unconnected socket channel Socket s = sc.socket(); // Get underlying java.net.Socket s.setSOTimeout(3000); // Time out after three seconds // Now connect the socket channel to the desired host and port sc.connect(new InetSocketAddress("www.davidflanagan.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(8192); // Create a buffer try { sc.read(buffer); } // Try to read from socket catch(SocketTimeoutException e) { // Catch timeouts here System.out.println("The remote computer is not responding."); sc.close(); }
In addition to the SocketChannel class, the
java.nio.channels package defines a
DatagramChannel for networking with datagrams
instead of sockets. DatagramChannel is not
demonstrated here, but you can read about it in .
One of the most powerful features of the New I/O API is that
channels such as SocketChannel and
DatagramChannel can be used in nonblocking
mode. We'll see examples of this in later sections.
Server-Side Networking
The java.net package defines a
Socket class for communication between a
client and a server and defines a ServerSocket
class used by the server to listen for and accept connections
from clients. The java.nio.channels package
is analogous: it defines a SocketChannel
class for data transfer and a
ServerSocketChannel class for accepting
connections. ServerSocketChannel is an
unusual channel because it does not implement
ReadableByteChannel or
WritableByteChannel . Instead of
read() and write()
methods, it has an accept() method for
accepting client connections and obtaining a
SocketChannel through which it communicates
with the client. Here is the code for a simple, single-threaded
server that listens for connections on port 8000 and reports the
current time to any client that connects:
import java.nio.*; import java.nio.channels.*; import java.nio.charset.*;
public class DateServer { public static void main(String[] args) throws java.io.IOException { // Get a CharsetEncoder for encoding the text sent to the client CharsetEncoder encoder = Charset.forName("US-ASCII").newEncoder();
// Create a new ServerSocketChannel and bind it to port 8000 // Note that this must be done using the underlying ServerSocket ServerSocketChannel server = ServerSocketChannel.open(); server.socket().bind(new java.net.InetSocketAddress(8000));
for(;;) { // This server runs forever // Wait for a client to connect SocketChannel client = server.accept(); // Get the current date and time as a string String response = new java.util.Date().toString() + "\r\n"; // Wrap, encode, and send the string to the client client.write(encoder.encode(CharBuffer.wrap(response))); // Disconnect from the client client.close(); } } }
Nonblocking I/O
The preceding DateServer class is a
simple network server. Because it does not maintain a
connection with any client, it never needs to communicate with
more than one at a time, and there is never more than one
SocketChannel in use. More realistic servers
must be able to communicate with more than one client at a time.
The java.io and java.net
APIs allow only blocking I/O, so servers written using these
APIs must use a separate thread for each client. For large-scale
servers with many clients, this approach does not scale well.
To solve this problem, the New I/O API allows most channels (but
not FileChannel ) to be used in nonblocking
mode and allows a single thread to manage all pending
connections. This is done with a Selector
object, which keeps track of a set of registered channels and can
block until one or more of those channels is ready for I/O, as the
following code illustrates. This is a longer example than most in
this chapter, but it is a complete working server class that
manages a ServerSocketChannel and any number
of SocketChannel connections to clients
through a single Selector object.
import java.io.*; import java.net.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; import java.util.*; // For Set and Iterator
public class NonBlockingServer { public static void main(String[] args) throws IOException { // Get the character encoders and decoders you'll need Charset charset = Charset.forName("ISO-8859-1"); CharsetEncoder encoder = charset.newEncoder(); CharsetDecoder decoder = charset.newDecoder();
// Allocate a buffer for communicating with clients ByteBuffer buffer = ByteBuffer.allocate(512);
// All of the channels in this code will be in nonblocking mode. // So create a Selector object that will block while monitoring // all of the channels and stop blocking only when one or more // of the channels is ready for I/O of some sort. Selector selector = Selector.open();
// Create a new ServerSocketChannel and bind it to port 8000 // Note that this must be done using the underlying ServerSocket ServerSocketChannel server = ServerSocketChannel.open(); server.socket().bind(new java.net.InetSocketAddress(8000)); // Put the ServerSocketChannel into nonblocking mode server.configureBlocking(false); // Now register it with the Selector (note that register() is called // on the channel, not on the selector object, however). // The SelectionKey represents the registration of this channel with // this Selector. SelectionKey serverkey = server.register(selector, SelectionKey.OP_ACCEPT);
for(;;) { // The main server loop. The server runs forever. // This call blocks until there is activity on one of the // registered channels. This is the key method in nonblocking // I/O. selector.select();
// Get a java.util.Set containing the SelectionKey objects for // all channels that are ready for I/O. Set keys = selector.selectedKeys();
// Use a java.util.Iterator to loop through the selected keys for(Iterator i = keys.iterator(); i.hasNext(); ) { // Get the next SelectionKey in the set, and remove it // from the set. It must be removed explicitly, or it will // be returned again by the next call to select(). SelectionKey key = (SelectionKey) i.next(); i.remove();
// Check whether this key is the SelectionKey obtained when // you registered the ServerSocketChannel. if (key == serverkey) { // Activity on the ServerSocketChannel means a client // is trying to connect to the server. if (key.isAcceptable()) { // Accept the client connection and obtain a // SocketChannel to communicate with the client. SocketChannel client = server.accept(); // Put the client channel in nonblocking mode client.configureBlocking(false); // Now register it with the Selector object, // telling it that you'd like to know when // there is data to be read from this channel. SelectionKey clientkey = client.register(selector, SelectionKey.OP_READ); // Attach some client state to the key. You'll // use this state when you talk to the client. clientkey.attach(new Integer(0)); } } else { // If the key obtained from the Set of keys is not the // ServerSocketChannel key, then it must be a key // representing one of the client connections. // Get the channel from the key. SocketChannel client = (SocketChannel) key.channel();
// If you are here, there should be data to read from // the channel, but double-check. if (!key.isReadable()) continue;
// Now read bytes from the client. Assume that all the // client's bytes are in one read operation. int bytesread = client.read(buffer);
// If read() returns -1, it indicates end-of-stream, // which means the client has disconnected, so // deregister the selection key and close the channel. if (bytesread == -1) { key.cancel(); client.close(); continue; }
// Otherwise, decode the bytes to a request string buffer.flip(); String request = decoder.decode(buffer).toString(); buffer.clear(); // Now reply to the client based on the request string if (request.trim().equals("quit")) { // If the request was "quit", send a final message // Close the channel and deregister the // SelectionKey client.write(encoder.encode(CharBuffer. wrap("Bye."))); key.cancel(); client.close(); } else { // Otherwise, send a response string comprised of // the sequence number of this request plus an // uppercase version of the request string. Note // that you keep track of the sequence number by // "attaching" an Integer object to the // SelectionKey and incrementing it each time.
// Get sequence number from SelectionKey int num = ((Integer)key.attachment()).intValue(); // For response string String response = num + ": " + request.toUpperCase(); // Wrap, encode, and write the response string client.write(encoder.encode(CharBuffer. wrap(response))); // Attach an incremented sequence nubmer to the key key.attach(new Integer(num+1)); } } } } } }
Nonblocking I/O is most useful for writing network servers.
It is also useful in clients that have more than one network
connection pending at the same time. For example, consider a web browser downloading a web page and the images
referenced by that page at the same time. One other interesting
use of nonblocking I/O is to perform nonblocking socket
connection operations. The idea is that you can ask a
SocketChannel to establish a connection to a
remote host and then go do other stuff (such as build a GUI, for
example) while the underlying OS is setting up the connection
across the network. Later, you do a select()
call to block until the connection has been established, if it
hasn't been already. The code for a nonblocking connect looks
like this:
// Create a new, unconnected SocketChannel. Put it in nonblocking // mode, register it with a new Selector, and then tell it to connect. // The connect call will return instead of waiting for the network // connect to be fully established. Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_CONNECT); channel.connect(new InetSocketAddress(hostname, port));
// Now go do other stuff while the connection is set up // For example, you can create a GUI here
// Now block if necessary until the SocketChannel is ready to connect. // Since you've registered only one channel with this selector, you // don't need to examine the key set; you know which channel is ready. while(selector.select() == 0) /* empty loop */;
// This call is necessary to finish the nonblocking connections channel.finishConnect();
// Finally, close the selector, which deregisters the channel from it selector.close();
JAXP, the Java API for XML Processing, was originally defined as
an optional extension to the Java platform and was available as a
separate download. In Java 1.4, however, JAXP has been made part
of the core platform. It consists of the following packages (and
their subpackages):
javax.xml.parsers -
This package provides high-level interfaces for instantiating SAX and DOM
parsers; it is a "pluggability layer" that allows the end user
or system administrator to choose or even replace the default
parser implementation with another.
javax.xml.transform -
This package and its subpackages define a Java API for
transforming XML document content and representation using
the XSLT standard. This package also provides a
pluggability layer that allows new XSLT engines to be
"plugged in" and used in place of the default
implementation.
org.xml.sax -
This package and its two subpackages define the de facto
standard SAX (SAX stands for Simple API for XML) API. SAX
is an event-driven, XML-parsing API: a SAX parser invokes
methods of a specified ContentHandler
object (as well as some other related handler objects) as it parses an XML document. The structure and
content of the document are fully described by the method
calls. This is a streaming API that does not build any
permanent representation of the document. It is up to the
ContentHandler
implementation to store any state or perform any actions that are
appropriate. This package includes classes for the SAX 2 API and
deprecated classes for SAX 1.
org.w3c.dom -
This package defines interfaces that represent an XML
document in tree form. The Document Object Model (DOM) is
a recommendation (essentially a standard) of the World Wide
Web Consortium (W3C). A DOM parser reads an XML document
and converts it into a tree of nodes that represent the full
content of the document. Once the tree representation of
the document is created, a program can examine and
manipulate it however it wants.
Examples of each of these packages are presented in the following
subsections.
Parsing XML with SAX
The first step in parsing an XML document with SAX is to obtain
a SAX parser. If you have a SAX parser implementation of your
own, you can simply instantiate the appropriate parser class.
It is usually simpler, however, to use the
javax.xml.parsers package to instantiate
whatever SAX parser is provided by the Java implementation. The
code looks like this:
import javax.xml.parsers.*;
// Obtain a factory object for creating SAX parsers SAXParserFactory parserFactory = SAXParserFactory.newInstance();
// Configure the factory object to specify attributes of the parsers it creates parserFactory.setValidating(true); parserFactory.setNamespaceAware(true);
// Now create a SAXParser object SAXParser parser = parserFactory.newSAXParser(); //May throw exceptions
The SAXParser class is a simple wrapper
around the org.xml.sax.XMLReader class. Once
you have obtained one, as shown in the previous code, you can parse a document
by simply calling one of the various parse()
methods. Some of these methods use the deprecated SAX 1
HandlerBase class, and others use the current
SAX 2 org.xml.sax.helpers.DefaultHandler
class. The DefaultHandler class
provides an empty implementation of all the methods of the ContentHandler ,
ErrorHandler , DTDHandler ,
and EntityResolver interfaces. These are all the
methods that the SAX parser can call while parsing an XML
document. By subclassing DefaultHandler and
defining the methods you care about, you can perform whatever
actions are necessary in response to the method calls generated
by the parser. The following code shows a method that uses SAX
to parse an XML file and determine the number of XML elements
that appear in a document as well as the number of characters of plain
text (possibly excluding "ignorable whitespace") that appear
within those elements:
import java.io.*; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.*;
public class SAXCount { public static void main(String[] args) throws SAXException,IOException, ParserConfigurationException { // Create a parser factory and use it to create a parser SAXParserFactory parserFactory = SAXParserFactory.newInstance(); SAXParser parser = parserFactory.newSAXParser(); // This is the name of the file you're parsing String filename = args[0]; // Instantiate a DefaultHandler subclass to do your counting for you CountHandler handler = new CountHandler(); // Start the parser. It reads the file and calls methods of the // handler. parser.parse(new File(filename), handler); // When you're done, report the results stored by your handler object System.out.println(filename + " contains " + handler.numElements + " elements and " + handler.numChars + " other characters "); }
// This inner class extends DefaultHandler to count elements and text in // the XML file and saves the results in public fields. There are many // other DefaultHandler methods you could override, but you need only // these. public static class CountHandler extends DefaultHandler { public int numElements = 0, numChars = 0; // Save counts here // This method is invoked when the parser encounters the opening tag // of any XML element. Ignore the arguments but count the element. public void startElement(String uri, String localname, String qname, Attributes attributes) { numElements++; }
// This method is called for any plain text within an element // Simply count the number of characters in that text
public void characters(char[] text, int start, int length) { numChars += length; } } }
Parsing XML with DOM
The DOM API is much different from the SAX API. While SAX is an
efficient way to scan an XML document, it is not well-suited for
programs that want to modify documents. Instead of converting an XML
document into a series of method calls, a DOM parser converts the
document into an org.w3c.dom.Document object, which is a tree
of org.w3c.dom.Node objects. The conversion
of the complete XML document to tree form allows random access
to the entire document but can consume substantial amounts of
memory.
In the DOM API, each node in the document tree implements the
Node interface and a
type-specific subinterface. (The most common types of node in a DOM document are
Element and Text nodes.)
When the parser is done parsing the document, your program can
examine and manipulate that tree using the various methods
of Node and its subinterfaces. The
following code uses JAXP to obtain a DOM parser (which, in JAXP
parlance, is called a DocumentBuilder ). It
then parses an XML file and builds a document tree from it.
Next, it examines the Document tree to search
for <sect1> elements and prints the
contents of the <title> of each.
import java.io.*; import javax.xml.parsers.*; import org.w3c.dom.*;
public class GetSectionTitles { public static void main(String[] args) throws IOException, ParserConfigurationException, org.xml.sax.SAXException { // Create a factory object for creating DOM parsers and configure it DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setIgnoringComments(true); // We want to ignore comments factory.setCoalescing(true); // Convert CDATA to Text nodes factory.setNamespaceAware(false); // No namespaces: this is default factory.setValidating(false); // Don't validate DTD: also default
// Now use the factory to create a DOM parser, a.k.a. DocumentBuilder DocumentBuilder parser = factory.newDocumentBuilder();
// Parse the file and build a Document tree to represent its content Document document = parser.parse(new File(args[0]));
// Ask the document for a list of all <sect1> elements it contains NodeList sections = document.getElementsByTagName("sect1"); // Loop through those <sect1> elements one at a time int numSections = sections.getLength(); for(int i = 0; i < numSections; i++) { Element section = (Element)sections.item(i); // A <sect1> // The first Element child of each <sect1> should be a <title> // element, but there may be some whitespace Text nodes first, so // loop through the children until you find the first element // child. Node title = section.getFirstChild(); while(title != null && title.getNodeType() != Node.ELEMENT_NODE) title = title.getNextSibling(); // Print the text contained in the Text node child of this element if (title != null) System.out.println(title.getFirstChild().getNodeValue()); } } }
Transforming XML Documents
The javax.xml.transform package defines a
TransformerFactory class for creating
Transformer objects. A
Transformer can transform a document from its
Source representation into a new
Result representation and optionally
apply an XSLT transformation to the document content in the
process. Three subpackages define concrete implementations of
the Source and Result
interfaces, which allow documents to be transformed among three
representations:
javax.xml.transform.stream -
Represents
documents as streams of XML text
javax.xml.transform.dom -
Represents
documents as DOM Document trees
javax.xml.transform.sax -
Represents
documents as sequences of SAX method calls
The following code shows one use of these packages to transform
the representation of a document from a DOM
Document tree into a stream of XML text. An
interesting feature of this code is that it does not create the
Document tree by parsing a file; instead, it
builds it up from scratch.
import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; import javax.xml.parsers.*; import org.w3c.dom.*;
public class DOMToStream { public static void main(String[] args) throws ParserConfigurationException, TransformerConfigurationException, TransformerException { // Create a DocumentBuilderFactory and a DocumentBuilder DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); // Instead of parsing an XML document, however, just create an empty // document that you can build up yourself. Document document = db.newDocument();
// Now build a document tree using DOM methods Element book = document.createElement("book"); // Create new element book.setAttribute("id", "javanut4"); // Give it an attribute document.appendChild(book); // Add to the document for(int i = 1; i <= 3; i++) { // Add more elements Element chapter = document.createElement("chapter"); Element title = document.createElement("title"); title.appendChild(document.createTextNode("Chapter " + i)); chapter.appendChild(title); chapter.appendChild(document.createElement("para")); book.appendChild(chapter); }
// Now create a TransformerFactory and use it to create a Transformer // object to transform our DOM document into a stream of XML text. // No arguments to newTransformer() means no XSLT stylesheet TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer();
// Create the Source and Result objects for the transformation DOMSource source = new DOMSource(document); // DOM document StreamResult result = new StreamResult(System.out); // to XML text
// Finally, do the transformation transformer.transform(source, result); } }
The most interesting uses of
javax.xml.transform involve XSLT stylesheets.
XSLT is a complex but powerful XML grammar that describes
how XML document content should be converted to another form
(e.g., XML, HTML, or plain text). A tutorial on
XSLT stylesheets is beyond the scope of this book, but the
following code (which contains only six key lines) shows
how you can apply such a stylesheet (which is an XML document
itself) to another XML document and write the resulting document
to a stream:
import java.io.*; import javax.xml.transform.*; import javax.xml.transform.stream.*; import javax.xml.parsers.*; import org.w3c.dom.*;
public class Transform { public static void main(String[] args) throws TransformerConfigurationException, TransformerException { // Get Source and Result objects for input, stylesheet, and output StreamSource input = new StreamSource(new File(args[0])); StreamSource stylesheet = new StreamSource(new File(args[1])); StreamResult output = new StreamResult(new File(args[2]));
// Create a transformer and perform the transformation TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(stylesheet); transformer.transform(input, output); } }
Earlier in the chapter, we saw how easy it is to create and
manipulate multiple threads of execution running within the same
Java interpreter. Java also has a java.lang.Process
class that represents a program running externally to the
interpreter. A Java program can communicate with an external
process using streams in the same way that it might communicate
with a server running on some other computer on the network. Using a Process is always platform-dependent
and is rarely portable, but it is sometimes a useful thing to do:
// Maximize portability by looking up the name of the command to execute // in a configuration file. java.util.Properties config; String cmd = config.getProperty("sysloadcmd"); if (cmd != null) { // Execute the command; Process p represents the running command Process p = Runtime.getRuntime().exec(cmd); // Start the command InputStream pin = p.getInputStream(); // Read bytes from it InputStreamReader cin = new InputStreamReader(pin); // Convert them to chars BufferedReader in = new BufferedReader(cin); // Read lines of chars String load = in.readLine(); // Get the command output in.close(); // Close the stream }
The java.security package defines quite a few
classes related to the Java access-control architecture, which is
discussed in more detail in . These classes allow Java
programs to run untrusted code in
a restricted environment from which it can do no harm. While
these are important classes, you rarely need to use them.
The more interesting classes are the ones used for
authentication; examples of their use are shown below.
Message Digests
A message digest is a value, also known as
cryptographic checksum or secure hash,
that is computed over a sequence of
bytes. The length of the digest is typically much smaller than
the length of the data for which it is computed, but any change,
no matter how small, in the input bytes, produces a change in the
digest. When transmitting data (a message), you can
transmit a message digest along with it. Then, the recipient
of the message can recompute the message digest on the received
data and, by comparing the computed digest to the received digest,
determine whether the message or the digest was
corrupted or tampered with during transmission. We saw a way to
compute a message digest earlier in the chapter when we discussed
streams. A similar technique can be used to compute a
message digest for
nonstreaming binary data:
import java.security.*;
// Obtain an object to compute message digests using the "Secure Hash // Algorithm"; this method can throw a NoSuchAlgorithmException. MessageDigest md = MessageDigest.getInstance("SHA");
byte[] data, data1, data2, secret; // Some byte arrays initialized elsewhere
// Create a digest for a single array of bytes byte[] digest = md.digest(data);
// Create a digest for several chunks of data md.reset(); // Optional: automatically called by digest() md.update(data1); // Process the first chunk of data md.update(data2); // Process the second chunk of data digest = md.digest(); // Compute the digest
// Create a keyed digest that can be verified if you know the secret bytes md.update(data); // The data to be transmitted with the digest digest = md.digest(secret); // Add the secret bytes and compute the digest
// Verify a digest like this byte[] receivedData, receivedDigest; // The data and the digest we received byte[] verifyDigest = md.digest(receivedData); // Digest the received data // Compare computed digest to the received digest boolean verified = java.util.Arrays.equals(receivedDigest, verifyDigest);
Digital Signatures
A digital signature combines a message-digest
algorithm with public-key cryptography. The sender of a message,
Alice, can compute a digest for a message and then encrypt that
digest with her private key. She then sends the message and the
encrypted digest to a recipient, Bob. Bob knows Alice's
public key (it is public, after all), so he can use it to decrypt
the digest and verify that the message has not been tampered
with. In performing this verification, Bob also learns that
the digest was encrypted with Alice's private key, since he
was able to decrypt the digest successfully using Alice's
public key. As Alice
is the only one who knows her private key, the message must have
come from Alice. A digital signature is called such because, like a pen-and-paper signature, it serves to
authenticate the origin of a document or message. Unlike a
pen-and-paper signature, however, a digital signature is very
difficult, if not impossible, to forge, and it cannot simply be
cut and pasted onto another document.
Java makes creating digital signatures easy. In order to create a
digital signature, however, you need a
java.security.PrivateKey object. Assuming that
a keystore exists on your system (see the
keytool documentation in ), you can get one with code like the
following:
// Here is some basic data we need File homedir = new File(System.getProperty("user.home")); File keyfile = new File(homedir, ".keystore"); // Or read from config file String filepass = "KeyStore password" // Password for entire file String signer = "david"; // Read from config file String password = "No one can guess this!"; // Better to prompt for this PrivateKey key; // This is the key we want to look up from the keystore
try { // Obtain a KeyStore object and then load data into it KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(new BufferedInputStream(new FileInputStream(keyfile)), filepass.toCharArray()); // Now ask for the desired key key = (PrivateKey) keystore.getKey(signer, password.toCharArray()); } catch (Exception e) { /* Handle various exception types here */ }
Once you have a PrivateKey object, you create a
digital signature with a
java.security.Signature object:
PrivateKey key; // Initialized as shown previously byte[] data; // The data to be signed Signature s = // Obtain object to create and verify signatures Signature.getInstance("SHA1withDSA"); // Can throw a // NoSuchAlgorithmException s.initSign(key); // Initialize it; can throw an InvalidKeyException s.update(data); // Data to sign; can throw a SignatureException /* s.update(data2); */ // Call multiple times to specify all data byte[] signature = s.sign(); // Compute signature
A Signature object can verify
a digital signature:
byte[] data; // The signed data; initialized elsewhere byte[] signature; // The signature to be verified; initialized elsewhere String signername; // Who created the signature; initialized elsewhere KeyStore keystore; // Where certificates stored; initialize as shown earlier
// Look for a public-key certificate for the signer java.security.cert.Certificate cert = keystore.getCertificate(signername); PublicKey publickey = cert.getPublicKey(); // Get the public key from it
Signature s = Signature.getInstance("SHA1withDSA"); // Or some other algorithm s.initVerify(publickey); // Setup for verification s.update(data); // Specify signed data boolean verified = s.verify(signature); // Verify signature data
Signed Objects
The java.security.SignedObject class is a
convenient utility for wrapping a digital signature around an
object. The SignedObject can then be
serialized and transmitted to a recipient, who can deserialize it
and use the verify() method to verify the
signature:
Serializable o; // The object to be signed; must be Serializable PrivateKey k; // The key to sign with; initialized elsewhere Signature s = Signature.getInstance("SHA1withDSA"); // Signature "engine" SignedObject so = new SignedObject(o, k, s); // Create the SignedObject
// The SignedObject encapsulates the object o; it can now be serialized // and transmitted to a recipient.
// Here's how the recipient verifies the SignedObject SignedObject so; // The deserialized SignedObject Object o; // The original object to extract from it PublicKey pk; // The key to verify with Signature s = Signature.getInstance("SHA1withDSA"); // Verification "engine"
if (so.verify(pk,s)) // If the signature is valid, o = so.getObject(); // retrieve the encapsulated object.
The java.security package includes
cryptography-based classes, but it does not contain classes
for actual encryption and decryption. That is the job of the
javax.crypto package. This package supports
symmetric-key cryptography, in which the same key is used for both
encryption and decryption and must be known by both the sender
and the receiver of encrypted data.
Secret Keys
The
SecretKey interface represents an encryption
key; the first step of any cryptographic operation is to
obtain an appropriate SecretKey . Unfortunately, the keytool program supplied
with the Java SDK cannot generate and store secret keys, so
a program must handle these tasks itself. Here is some code
that shows various ways to work with SecretKey
objects:
import javax.crypto.*; import javax.crypto.spec.*;
// Generate encryption keys with a KeyGenerator object KeyGenerator desGen = KeyGenerator.getInstance("DES"); // DES algorithm SecretKey desKey = desGen.generateKey(); // Generate a key KeyGenerator desEdeGen = KeyGenerator.getInstance("DESede"); // Triple DES SecretKey desEdeKey = desEdeGen.generateKey(); // Generate a key
// SecretKey is an opaque representation of a key. Use SecretKeyFactory to // convert to a transparent representation that can be manipulated: saved // to a file, securely transmitted to a receiving party, etc. SecretKeyFactory desFactory = SecretKeyFactory.getInstance("DES"); DESKeySpec desSpec = (DESKeySpec) desFactory.getKeySpec(desKey, javax.crypto.spec.DESKeySpec.class); byte[] rawDesKey = desSpec.getKey(); // Do the same for a DESede key SecretKeyFactory desEdeFactory = SecretKeyFactory.getInstance("DESede"); DESedeKeySpec desEdeSpec = (DESedeKeySpec) desEdeFactory.getKeySpec(desEdeKey, javax.crypto.spec.DESedeKeySpec.class); byte[] rawDesEdeKey = desEdeSpec.getKey();
// Convert the raw bytes of a key back to a SecretKey object DESedeKeySpec keyspec = new DESedeKeySpec(rawDesEdeKey); SecretKey k = desEdeFactory.generateSecret(keyspec);
// For DES and DESede keys, there is an even easier way to create keys // SecretKeySpec implements SecretKey, so use it to represent these keys byte[] desKeyData = new byte[8]; // Read 8 bytes of data from a file byte[] tripleDesKeyData = new byte[24]; // Read 24 bytes of data from a file SecretKey myDesKey = new SecretKeySpec(desKeyData, "DES"); SecretKey myTripleDesKey = new SecretKeySpec(tripleDesKeyData, "DESede");
Encryption and Decryption with Cipher
Once you have obtained an appropriate SecretKey
object, the central class for
encryption and decryption is Cipher . Use it
like this:
SecretKey key; // Obtain a SecretKey as shown earlier byte[] plaintext; // The data to encrypt; initialized elsewhere
// Obtain an object to perform encryption or decryption Cipher cipher = Cipher.getInstance("DESede"); // Triple-DES encryption // Initialize the cipher object for encryption cipher.init(Cipher.ENCRYPT_MODE, key); // Now encrypt data byte[] ciphertext = cipher.doFinal(plaintext);
// If we had multiple chunks of data to encrypt, we can do this cipher.update(message1); cipher.update(message2); byte[] ciphertext = cipher.doFinal();
// We simply reverse things to decrypt cipher.init(Cipher.DECRYPT_MODE, key); byte[] decryptedMessage = cipher.doFinal(ciphertext);
// To decrypt multiple chunks of data byte[] decrypted1 = cipher.update(ciphertext1); byte[] decrypted2 = cipher.update(ciphertext2); byte[] decrypted3 = cipher.doFinal(ciphertext3);
Encrypting and Decrypting Streams
The Cipher class can also be used with
CipherInputStream or
CipherOutputStream to encrypt or decrypt while
reading or writing streaming data:
byte[] data; // The data to encrypt SecretKey key; // Initialize as shown earlier Cipher c = Cipher.getInstance("DESede"); // The object to perform encryption c.init(Cipher.ENCRYPT_MODE, key); // Initialize it
// Create a stream to write bytes to a file FileOutputStream fos = new FileOutputStream("encrypted.data");
// Create a stream that encrypts bytes before sending them to that stream // See also CipherInputStream to encrypt or decrypt while reading bytes CipherOutputStream cos = new CipherOutputStream(fos, c);
cos.write(data); // Encrypt and write the data to the file cos.close(); // Always remember to close streams java.util.Arrays.fill(data, (byte)0); // Erase the unencrypted data
Encrypted Objects
Finally, the
javax.crypto.SealedObject class provides
an especially easy way to perform encryption. This class
serializes a specified object and encrypts the resulting stream of
bytes. The SealedObject can then be serialized
itself and transmitted to a recipient. The recipient is only
able to retrieve the original object if she knows the required
SecretKey :
Serializable o; // The object to be encrypted; must be Serializable SecretKey key; // The key to encrypt it with Cipher c = Cipher.getInstance("Blowfish"); // Object to perform encryption c.init(Cipher.ENCRYPT_MODE, key); // Initialize it with the key SealedObject so = new SealedObject(o, c); // Create the sealed object
// Object so is a wrapper around an encrypted form of the original object o; // it can now be serialized and transmitted to another party. // Here's how the recipient decrypts the original object Object original = so.getObject(key); // Must use the same SecretKey
View catalog information for Java in a Nutshell, 4th Edition.
|