Java makes it easy to
define and work with multiple threads of execution within a
program. java.lang.Thread is the
fundamental thread class in the Java API. There are two ways to define a
thread. One is to subclass Thread , override
the run() method, and then instantiate your
Thread subclass. The other is to define a
class that implements the Runnable method
(i.e., define a run() method) and then pass
an instance of this Runnable object to the
Thread() constructor. In either case, the
result is a Thread object, where the
run() method is the body of the thread. When you call the start() method of the
Thread object, the interpreter creates a
new thread to execute the run() method. This new thread continues to run until the
run() method exits, at which point it
ceases to exist. Meanwhile, the original
thread continues running itself, starting with the
statement following the start() method. The
following code demonstrates:
final List list; // Some long unsorted list of objects; initialized elsewhere
/** A Thread class for sorting a List in the background */ class BackgroundSorter extends Thread { List l; public BackgroundSorter(List l) { this.l = l; } // Constructor public void run() { Collections.sort(l); } // Thread body }
// Create a BackgroundSorter thread Thread sorter = new BackgroundSorter(list); // Start it running; the new thread runs the run() method above, while // the original thread continues with whatever statement comes next. sorter.start();
// Here's another way to define a similar thread Thread t = new Thread(new Runnable() { // Create a new thread public void run() { Collections.sort(list); } // to sort the list of objects. }); t.start(); // Start it running
Thread Priorities
Threads can run at different priority levels. A thread at a given
priority level does not typically run unless there are no higher-priority
threads waiting to run. Here is some code you can use
when working with thread priorities:
// Set a thread t to lower-than-normal priority t.setPriority(Thread.NORM_PRIORITY-1);
// Set a thread to lower priority than the current thread t.setPriority(Thread.currentThread().getPriority() - 1);
// Threads that don't pause for I/O should explicitly yield the CPU // to give other threads with the same priority a chance to run. Thread t = new Thread(new Runnable() { public void run() { for(int i = 0; i < data.length; i++) { // Loop through a bunch of data process(data[i]); // Process it if ((i % 10) == 0) // But after every 10 iterations, Thread.yield(); // pause to let other threads run. } } });
Making a Thread Sleep
Often, threads are used to perform some kind of repetitive
task at a fixed interval. This is particularly true when doing
graphical programming that involves animation or similar
effects. The key to doing this is making a thread sleep,
or stop running for a specified amount of time. This is done with
the static Thread.sleep() method:
public class Clock extends Thread { java.text.DateFormat f = // How to format the time for this locale java.text.DateFormat.getTimeInstance(java.text.DateFormat.MEDIUM); volatile boolean keepRunning = true;
public Clock() { // The constructor setDaemon(true); // Daemon thread: interpreter can exit while it runs start(); // This thread starts itself }
public void run() { // The body of the thread while(keepRunning) { // This thread runs until asked to stop String time = f.format(new java.util.Date()); // Current time System.out.println(time); // Print the time try { Thread.sleep(1000); } // Wait 1,000 milliseconds catch (InterruptedException e) {} // Ignore this exception } }
// Ask the thread to stop running public void pleaseStop() { keepRunning = false; } }
Notice the pleaseStop() method in this
example. You can forcefully terminate a thread by calling its
stop() method, but this method has been
deprecated because a thread that is forcefully stopped can leave
objects it is manipulating in an inconsistent state. If you need
a thread that can be stopped, you should define a method such as
pleaseStop() that stops the thread
in a controlled way.
Timers
In Java 1.3, the java.util.Timer and
java.util.TimerTask classes make it even easier
to run repetitive tasks. Here is some code that behaves much like
the previous Clock class:
import java.util.*;
// How to format the time for this locale final java.text.DateFormat timeFmt = java.text.DateFormat.getTimeInstance(java.text.DateFormat.MEDIUM); // Define the time-display task TimerTask displayTime = new TimerTask() { public void run() { System.out.println(timeFmt.format(new Date())); } }; // Create a timer object to run the task (and possibly others) Timer timer = new Timer(); // Now schedule that task to be run every 1,000 milliseconds, starting now Timer.schedule(displayTime, 0, 1000);
// To stop the time-display task displayTime.cancel();
Waiting for a Thread to Finish
Sometimes one thread needs to stop and wait for
another thread to complete. You can accomplish this with the
join() method:
List list; // A long list of objects to be sorted; initialized elsewhere
// Define a thread to sort the list: lower its priority, so it runs only // when the current thread is waiting for I/O, and then start it running. Thread sorter = new BackgroundSorter(list); // Defined earlier sorter.setPriority(Thread.currentThread.getPriority()-1); // Lower priority sorter.start(); // Start sorting
// Meanwhile, in this original thread, read data from a file byte[] data = readData(); // Method defined elsewhere
// Before we can proceed, we need the list to be fully sorted, so // we must wait for the sorter thread to exit, if it hasn't already. try { sorter.join(); } catch(InterruptedException e) {}
Thread Synchronization
When using multiple threads, you must be very careful
if you allow more than one thread to access the same data
structure. Consider what would happen if one thread was trying
to loop through the elements of a List while
another thread was sorting those elements. Preventing this
problem is called thread synchronization and is one of the
central problems of multithreaded computing. The basic technique
for preventing two threads from accessing the same object at the
same time is to require a thread to obtain a lock on the
object before the thread can modify it. While any one thread holds the
lock, another thread that requests the lock has to wait until the
first thread is done and releases the lock. Every Java object has the fundamental ability to provide such
a locking capability.
The easiest way to keep objects thread-safe is to declare all
sensitive methods synchronized . A thread must
obtain a lock on an object before it can execute any of its
synchronized methods, which means that no other
thread can execute any other synchronized
method at the same time. (If a static method is declared
synchronized , the thread must obtain a
lock on the class, and this works in the same manner.)
To do finer-grained locking, you can specify
synchronized blocks of code that hold a lock
on a specified object for a short time:
// This method swaps two array elements in a synchronized block public static void swap(Object[] array, int index1, int index2) { synchronized(array) { Object tmp = array[index1]; array[index1] = array[index2]; array[index2] = tmp; } }
// The Collection, Set, List, and Map implementations in java.util do // not have synchronized methods (except for the legacy implementations // Vector and Hashtable). When working with multiple threads, you can // obtain synchronized wrapper objects. List synclist = Collections.synchronizedList(list); Map syncmap = Collections.synchronizedMap(map);
Deadlock
When you are synchronizing threads, you must be careful to avoid
deadlock, which occurs when two threads end
up waiting for each other to release a lock they need. Since neither can proceed, neither one can release the lock it
holds, and they both stop running:
// When two threads try to lock two objects, deadlock can occur unless // they always request the locks in the same order. final Object resource1 = new Object(); // Here are two objects to lock final Object resource2 = new Object(); Thread t1 = new Thread(new Runnable() { // Locks resource1 then resource2 public void run() { synchronized(resource1) { synchronized(resource2) { compute(); } } } });
Thread t2 = new Thread(new Runnable() { // Locks resource2 then resource1 public void run() { synchronized(resource2) { synchronized(resource1) { compute(); } } } });
t1.start(); // Locks resource1 t2.start(); // Locks resource2 and now neither thread can progress!
Coordinating Threads with wait( ) and notify( )
Sometimes a thread needs to stop running and wait until some kind
of event occurs, at which point it is told to continue
running. This is done with the wait() and
notify() methods. These aren't methods of the
Thread class, however; they are methods of
Object . Just as every Java object has a lock
associated with it, every object can maintain a list of waiting
threads. When a thread calls the wait() method
of an object, any locks the thread holds are temporarily released,
and the thread is added to the list of waiting threads for that
object and stops running. When another thread calls the
notify() method of the same object, the object
wakes up one of the waiting threads and allows it to continue
running:
/** * A queue. One thread calls push() to put an object on the queue. * Another calls pop() to get an object off the queue. If there is no * data, pop() waits until there is some, using wait()/notify(). * wait() and notify() must be used within a synchronized method or * block. */ import java.util.*;
public class Queue { LinkedList q = new LinkedList(); // Where objects are stored public synchronized void push(Object o) { q.add(o); // Append the object to the end of the list this.notify(); // Tell waiting threads that data is ready } public synchronized Object pop() { while(q.size() == 0) { try { this.wait(); } catch (InterruptedException e) { /* Ignore this exception */ } } return q.remove(0); } }
Thread Interruption
In the examples illustrating the sleep() ,
join() , and wait()
methods, you may have noticed that calls to each of these methods
are wrapped in a try statement that catches an
InterruptedException . This is necessary
because the interrupt() method allows one
thread to interrupt the execution of another. Interrupting a
thread is not intended to stop it from executing, but to
wake it up from a blocking state.
If the interrupt() method is called on a thread
that is not blocked, the thread continues running, but
its "interrupt status" is set to indicate that an interrupt has
been requested. A thread can test its own interrupt status by
calling the static Thread.interrupted() method,
which returns true if the thread has been
interrupted and, as a side effect, clears the interrupt status.
One thread can test the interrupt status of another thread with
the instance method isInterrupted() , which
queries the status but does not clear it.
If a thread calls sleep() ,
join() , or wait() while its
interrupt status is set, it does not block but
immediately throws an InterruptedException (the
interrupt status is cleared as a side effect of throwing the
exception). Similarly, if the interrupt()
method is called on a thread that is already blocked in a call to
sleep() , join() , or
wait() , that thread stops blocking by
throwing an InterruptedException .
One of the most common times that threads block is while doing
input/output; a thread often has to pause and
wait for data to become available from the filesystem or from
the network. (The java.io ,
java.net , and java.nio APIs
for performing I/O operations are discussed later in this
chapter.) Unfortunately, the interrupt()
method does not wake up a thread blocked in an I/O method of
the java.io package. This is one of the
shortcomings of java.io that is cured by the
New I/O API in java.nio . If a thread is
interrupted while blocked in an I/O operation on any channel that
implements
java.nio.channels.InterruptibleChannel ,
the channel is closed, the thread's interrupt status is set, and
the thread wakes up by throwing a
java.nio.channels.ClosedByInterruptException .
The same thing happens if a thread tries to call a blocking I/O
method while its interrupt status is set. Similarly, if a thread
is interrupted while it is blocked in the
select() method of a
java.nio.channels.Selector (or if it calls
select() while its interrupt status is set),
select() will stop blocking (or will never
start) and will return immediately. No exception is thrown in this
case; the interrupted thread simply wakes up, and the
select() call returns.
The java.io.File class represents a file or a
directory and defines a number of important methods for
manipulating files and directories. Note, however, that
none of these methods allow you to read the contents of a file;
that is the job of java.io.FileInputStream , which is
just one of the many types of I/O streams used in Java
and discussed in the next section. Here are some things you
can do with File :
import java.io.*; import java.util.*;
// Get the name of the user's home directory and represent it with a File File homedir = new File(System.getProperty("user.home")); // Create a File object to represent a file in that directory File f = new File(homedir, ".configfile");
// Find out how big a file is and when it was last modified long filelength = f.length(); Date lastModified = new java.util.Date(f.lastModified());
// If the file exists, is not a directory, and is readable, // move it into a newly created directory. if (f.exists() && f.isFile() && f.canRead()) { // Check config file File configdir = new File(homedir, ".configdir"); // A new config directory configdir.mkdir(); // Create that directory f.renameTo(new File(configdir, ".config")); // Move the file into it }
// List all files in the home directory String[] allfiles = homedir.list();
// List all files that have a ".java" suffix String[] sourcecode = homedir.list(new FilenameFilter() { public boolean accept(File d, String name) { return name.endsWith(".java"); } });
The File class provides some important additional
functionality as of Java 1.2:
// List all filesystem root directories; on Windows, this gives us // File objects for all drive letters (Java 1.2 and later). File[] rootdirs = File.listRoots();
// Atomically, create a lock file, then delete it (Java 1.2 and later) File lock = new File(configdir, ".lock"); if (lock.createNewFile()) { // We successfully created the file, so do something ... // Then delete the lock file lock.delete(); } else { // We didn't create the file; someone else has a lock System.err.println("Can't create lock file; exiting."); System.exit(1); }
// Create a temporary file to use during processing (Java 1.2 and later) File temp = File.createTempFile("app", ".tmp"); // Filename prefix and suffix
// Make sure file gets deleted when we're done with it (Java 1.2 and later) temp.deleteOnExit();
RandomAccessFile
The
java.io package also defines a
RandomAccessFile class that allows you to read
binary data from arbitrary locations in a file. This can be
a useful thing to do in certain situations,
but most applications read files
sequentially, using the stream classes described in the next
section. Here is a short example of using
Random-AccessFile :
// Open a file for read/write ("rw") access File datafile = new File(configdir, "datafile"); RandomAccessFile f = new RandomAccessFile(datafile, "rw"); f.seek(100); // Move to byte 100 of the file byte[] data = new byte[100]; // Create a buffer to hold data f.read(data); // Read 100 bytes from the file int i = f.readInt(); // Read a 4-byte integer from the file f.seek(100); // Move back to byte 100 f.writeInt(i); // Write the integer first f.write(data); // Then write the 100 bytes f.close(); // Close file when done with it
The java.io package
defines a large number of classes for reading and writing
streaming, or sequential, data. The
InputStream and OutputStream
classes are for reading and writing streams of bytes, while the
Reader and Writer classes
are for reading and writing streams of characters. Streams can be
nested, meaning you might read characters from a
FilterReader object that reads and processes
characters from an underlying Reader stream. This underlying Reader stream might read bytes from
an InputStream and convert them to
characters.
Reading Console Input
There are a number of common operations you can perform with
streams. One is to read lines of input the user types at the
console:
import java.io.*;
BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); System.out.print("What is your name: "); String name = null; try { name = console.readLine(); } catch (IOException e) { name = "<" + e + ">"; } // This should never happen System.out.println("Hello " + name);
Reading Lines from a Text File
Reading lines of text from a file is a similar operation. The
following code reads an entire text file and quits when it reaches
the end:
String filename = System.getProperty("user.home") + File.separator + ".cshrc"; try { BufferedReader in = new BufferedReader(new FileReader(filename)); String line; while((line = in.readLine()) != null) { // Read line, check for end-of-file System.out.println(line); // Print the line } in.close(); // Always close a stream when you are done with it } catch (IOException e) { // Handle FileNotFoundException, etc. here }
Writing Text to a File
Throughout this book, you've seen the use of the
System.out.println() method to display text on
the console. System.out simply refers to an output
stream. You can print text to any output stream using similar
techniques. The following code
shows how to output text to a file:
try { File f = new File(homedir, ".config"); PrintWriter out = new PrintWriter(new FileWriter(f)); out.println("## Automatically generated config file. DO NOT EDIT!"); out.close(); // We're done writing } catch (IOException e) { /* Handle exceptions */ }
Reading a Binary File
Not all files contain text, however. The following lines of code
treat a file as a stream of bytes and read the bytes into a
large array:
try { File f; // File to read; initialized elsewhere int filesize = (int) f.length(); // Figure out the file size byte[] data = new byte[filesize]; // Create an array that is big enough // Create a stream to read the file DataInputStream in = new DataInputStream(new FileInputStream(f)); in.readFully(data); // Read file contents into array in.close(); } catch (IOException e) { /* Handle exceptions */ }
Compressing Data
Various other packages of the Java platform define specialized stream
classes that operate on streaming data in some useful way. The
following code shows how to use stream classes from
java.util.zip to compute a
checksum of data and then compress the data while writing it to a
file:
import java.io.*; import java.util.zip.*;
try { File f; // File to write to; initialized elsewhere byte[] data; // Data to write; initialized elsewhere Checksum check = new Adler32(); // An object to compute a simple checksum
// Create a stream that writes bytes to the file f FileOutputStream fos = new FileOutputStream(f); // Create a stream that compresses bytes and writes them to fos GZIPOutputStream gzos = new GZIPOutputStream(fos); // Create a stream that computes a checksum on the bytes it writes to gzos CheckedOutputStream cos = new CheckedOutputStream(gzos, check);
cos.write(data); // Now write the data to the nested streams cos.close(); // Close down the nested chain of streams long sum = check.getValue(); // Obtain the computed checksum } catch (IOException e) { /* Handle exceptions */ }
Reading ZIP Files
The java.util.zip package also contains a
ZipFile class that gives you random access to
the entries of a ZIP archive and allows you to read those entries
through a stream:
import java.io.*; import java.util.zip.*;
String filename; // File to read; initialized elsewhere String entryname; // Entry to read from the ZIP file; initialized elsewhere ZipFile zipfile = new ZipFile(filename); // Open the ZIP file ZipEntry entry = zipfile.getEntry(entryname); // Get one entry InputStream in = zipfile.getInputStream(entry); // A stream to read the entry BufferedInputStream bis = new BufferedInputStream(in); // Improves efficiency // Now read bytes from bis... // Print out contents of the ZIP file for(java.util.Enumeration e = zipfile.entries(); e.hasMoreElements();) { ZipEntry zipentry = (ZipEntry) e.nextElement(); System.out.println(zipentry.getName()); }
Computing Message Digests
If you need to compute a cryptographic-strength checksum (also
knows as a message digest), use one of the stream classes of the
java.security package. For example:
import java.io.*; import java.security.*; import java.util.*;
File f; // File to read and compute digest on; initialized elsewhere List text = new ArrayList(); // We'll store the lines of text here
// Get an object that can compute an SHA message digest MessageDigest digester = MessageDigest.getInstance("SHA"); // A stream to read bytes from the file f FileInputStream fis = new FileInputStream(f); // A stream that reads bytes from fis and computes an SHA message digest DigestInputStream dis = new DigestInputStream(fis, digester); // A stream that reads bytes from dis and converts them to characters InputStreamReader isr = new InputStreamReader(dis); // A stream that can read a line at a time BufferedReader br = new BufferedReader(isr); // Now read lines from the stream for(String line; (line = br.readLine()) != null; text.add(line)) ; // Close the streams br.close(); // Get the message digest byte[] digest = digester.digest();
Streaming Data to and from Arrays
So far, we've used a variety of stream classes
to manipulate streaming data, but the data itself ultimately comes
from a file or is written to the console. The
java.io package defines other
stream classes that can
read data from and write data to arrays of bytes or strings of
text:
import java.io.*;
// Set up a stream that uses a byte array as its destination ByteArrayOutputStream baos = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(baos); out.writeUTF("hello"); // Write some string data out as bytes out.writeDouble(Math.PI); // Write a floating-point value out as bytes byte[] data = baos.toByteArray(); // Get the array of bytes we've written out.close(); // Close the streams
// Set up a stream to read characters from a string Reader in = new StringReader("Now is the time!"); // Read characters from it until we reach the end int c; while((c = in.read()) != -1) System.out.print((char) c);
Other classes that operate this way include
ByteArrayInputStream , StringWriter ,
CharArrayReader , and CharArrayWriter .
Thread Communication with Pipes
PipedInputStream and
PipedOutputStream and their character-based
counterparts, PipedReader and
PipedWriter , are
another interesting set of streams defined by
java.io . These streams are used in pairs
by two threads that want to communicate. One thread writes bytes to a
PipedOutputStream or characters to a
PipedWriter , and another thread reads bytes or
characters from the corresponding
PipedInputStream or
PipedReader :
// A pair of connected piped I/O streams forms a pipe. One thread writes // bytes to the PipedOutputStream, and another thread reads them from the // corresponding PipedInputStream. Or use PipedWriter/PipedReader for chars. final PipedOutputStream writeEndOfPipe = new PipedOutputStream(); final PipedInputStream readEndOfPipe = new PipedInputStream(writeEndOfPipe);
// This thread reads bytes from the pipe and discards them Thread devnull = new Thread(new Runnable() { public void run() { try { while(readEndOfPipe.read() != -1); } catch (IOException e) {} // ignore it } }); devnull.start();
Serialization
One of the most important features of the
java.io package is the ability to
serialize
objects: to convert an object into a stream of bytes that can later be
deserialized back into a copy of the original object. The following
code shows how to use serialization to save an object to a file and
later read it back:
Object o; // The object we are serializing; it must implement Serializable File f; // The file we are saving it to
try { // Serialize the object ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f)); oos.writeObject(o); oos.close();
// Read the object back in ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f)); Object copy = ois.readObject(); ois.close(); } catch (IOException e) { /* Handle input/output exceptions */ } catch (ClassNotFoundException cnfe) { /* readObject() can throw this */ }
The previous example serializes to a file, but remember, you can write
serialized objects to any type of stream. Thus, you can write an
object to a byte array, then read it back from the byte array,
creating a deep copy of the object. You can write the
object's bytes to a compression stream or even write
the bytes to a stream connected across a network to another
program!
JavaBeans Persistence
Java 1.4 introduces a new serialization mechanism intended for
use with JavaBeans components. java.io
serialization works by saving the state of the internal fields
of an object. java.beans persistence, on the
other hand, works by saving a bean's state as a sequence of calls
to the public methods defined by the class. Since it is based
on the public API rather than on the internal state, the JavaBeans
persistence mechanism allows interoperability between different
implementations of the same API, handles version skew more
robustly, and is suitable for longer-term storage of serialized
objects.
A bean and any descendant beans or other objects
serialized with java.beans.XMLEncoder and
can be deserialized with
java.beans.XMLDecoder . These classes write
to and read from specified streams, but they are not stream
classes themselves. Here is how you might encode a bean:
// Create a JavaBean, and set some properties on it javax.swing.JFrame bean = new javax.swing.JFrame("PersistBean"); bean.setSize(300, 300); // Now save its encoded form to the file bean.xml BufferedOutputStream out = // Create an output stream new BufferedOutputStream(new FileOutputStream("bean.xml")); XMLEncoder encoder = new XMLEncoder(out); // Create encoder for stream encoder.writeObject(bean); // Encode the bean encoder.close(); // Close encoder and stream
Here is the corresponding code to decode the bean from its
serialized form:
BufferedInputStream in = // Create input stream new BufferedInputStream(new FileInputStream("bean.xml")); XMLDecoder decoder = new XMLDecoder(in); // Create decoder for stream Object b = decoder.readObject(); // Decode a bean decoder.close(); // Close decoder and stream bean = (javax.swing.JFrame) b; // Cast bean to proper type bean.setVisible(true); // Start using it
The java.net package defines a number of
classes that make writing networked applications surprisingly
easy. Various examples follow.
Networking with the URL Class
The easiest networking class to use is
URL , which represents a uniform resource
locator. Different Java implementations may support different
sets of URL protocols, but, at a minimum, you can rely on support
for the http:// , ftp:// , and
file:// protocols. In Java 1.4, secure HTTP is
also supported with the https:// protocol.
Here are some ways you can
use the URL class:
import java.net.*; import java.io.*;
// Create some URL objects URL url=null, url2=null, url3=null; try { url = new URL("http://www."); // An absolute URL url2 = new URL(url, "catalog/books/javanut4/"); // A relative URL url3 = new URL("http:", "www.", "index.html"); } catch (MalformedURLException e) { /* Ignore this exception */ }
// Read the content of a URL from an input stream InputStream in = url.openStream();
// For more control over the reading process, get a URLConnection object URLConnection conn = url.openConnection();
// Now get some information about the URL String type = conn.getContentType(); String encoding = conn.getContentEncoding(); java.util.Date lastModified = new java.util.Date(conn.getLastModified()); int len = conn.getContentLength();
// If necessary, read the contents of the URL using this stream InputStream in = conn.getInputStream();
Working with Sockets
Sometimes you need more control over your networked application
than is possible with the URL class. In this
case, you can use a Socket to communicate
directly with a server. For example:
import java.net.*; import java.io.*;
// Here's a simple client program that connects to a web server, // requests a document, and reads the document from the server. String hostname = "java."; // The server to connect to int port = 80; // Standard port for HTTP String filename = "/index.html"; // The file to read from the server Socket s = new Socket(hostname, port); // Connect to the server
// Get I/O streams we can use to talk to the server InputStream sin = s.getInputStream(); BufferedReader fromServer = new BufferedReader(new InputStreamReader(sin)); OutputStream sout = s.getOutputStream(); PrintWriter toServer = new PrintWriter(new OutputStreamWriter(sout));
// Request the file from the server, using the HTTP protocol toServer.print("GET " + filename + " HTTP/1.0\r\n\r\n"); toServer.flush();
// Now read the server's response, assume it is a text file, and print it out for(String l = null; (l = fromServer.readLine()) != null; ) System.out.println(l);
// Close everything down when we're done toServer.close(); fromServer.close(); s.close();
Secure Sockets with SSL
In Java 1.4, the Java Secure Socket Extension, or JSSE, has been
added to the core Java platform in the packages
javax.net and javax.net.ssl .[1]
This API enables encrypted network communication over sockets that
use the SSL (Secure Sockets Layer, also known as TLS) protocol.
SSL is widely used on the Internet: it is the basis for secure web
communication using the https:// protocol. In
Java 1.4 and later, you can use https:// with
the URL class as previously shown to securely
download documents from web servers that support SSL.
[1]
An earlier version of JSSE using different package names is
available as a separate download for use with Java 1.2 and Java
1.3. See http://java./products/jsse/.
Like all Java security APIs, JSSE is highly configurable and
gives low-level control over all details of setting up and
communicating over an SSL socket. The
javax.net and javax.net.ssl
packages are fairly complex, but in practice, there are only a few
classes you need to use to securely communicate with a
server. The following program is a variant on the preceding code that
uses HTTPS instead of HTTP to securely transfer the contents of
the requested URL:
import java.io.*; import java.net.*; import javax.net.ssl.*; import java.security.cert.*;
/** * Get a document from a web server using HTTPS. Usage: * java HttpsDownload <hostname> <filename> **/ public class HttpsDownload { public static void main(String[] args) throws IOException { // Get a SocketFactory object for creating SSL sockets SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
// Use the factory to create a secure socket connected to the // HTTPS port of the specified web server. SSLSocket sslsock=(SSLSocket)factory.createSocket(args[0], // Hostname 443); // HTTPS port
// Get the certificate presented by the web server SSLSession session = sslsock.getSession(); X509Certificate cert; try { cert = (X509Certificate)session.getPeerCertificates()[0]; } catch(SSLPeerUnverifiedException e) { // If no or invalid certificate System.err.println(session.getPeerHost() + " did not present a valid certificate."); return; }
// Display details about the certificate System.out.println(session.getPeerHost() + " has presented a certificate belonging to:"); System.out.println("\t[" + cert.getSubjectDN().getName() + "]"); System.out.println("The certificate bears the valid signature of:"); System.out.println("\t[" + cert.getIssuerDN().getName() + "]");
// If the user does not trust the certificate, abort System.out.print("Do you trust this certificate (y/n)? "); System.out.flush(); BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); if (Character.toLowerCase(console.readLine().charAt(0)) != 'y') return;
// Now use the secure socket just as you would use a regular socket // First, send a regular HTTP request over the SSL socket PrintWriter out = new PrintWriter(sslsock.getOutputStream());
out.print("GET " + args[1] + " HTTP/1.0\r\n\r\n"); out.flush();
// Next, read the server's response and print it to the console BufferedReader in = new BufferedReader(new InputStreamReader(sslsock.getInputStream())); String line; while((line = in.readLine()) != null) System.out.println(line); // Finally, close the socket sslsock.close(); } }
Servers
A client application uses a Socket to
communicate with a server. The server does the same thing: it uses
a Socket object to communicate with each of its
clients. However, the server has an additional task, in that it
must be able to recognize and accept client connection
requests. This is done with the ServerSocket
class. The following code shows how you might use a
ServerSocket . The code implements a simple HTTP
server that responds to all requests by sending back (or
mirroring) the exact contents of the HTTP request. A dummy server
like this is useful when debugging HTTP clients:
import java.io.*; import java.net.*;
public class HttpMirror { public static void main(String[] args) { try { int port = Integer.parseInt(args[0]); // The port to listen on ServerSocket ss = new ServerSocket(port); // Create a socket to listen for(;;) { // Loop forever Socket client = ss.accept(); // Wait for a connection ClientThread t = new ClientThread(client); // A thread to handle it t.start(); // Start the thread running } // Loop again } catch (Exception e) { System.err.println(e.getMessage()); System.err.println("Usage: java HttpMirror <port>;"); } }
static class ClientThread extends Thread { Socket client; ClientThread(Socket client) { this.client = client; } public void run() { try { // Get streams to talk to the client BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream())); PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream())); // Send an HTTP response header to the client out.print("HTTP/1.0 200\r\nContent-Type: text/plain\r\n\r\n"); // Read the HTTP request from the client and send it right back // Stop when we read the blank line from the client that marks // the end of the request and its headers. String line; while((line = in.readLine()) != null) { if (line.length() == 0) break; out.println(line); } out.close(); in.close(); client.close(); } catch (IOException e) { /* Ignore exceptions */ } } } }
This server code could be modified using JSSE to support SSL
connections. Making a server secure is more complex than making a
client secure, however, because a server must have a
certificate it can present to the client. Therefore, server-side
JSSE is not demonstrated here.
Datagrams
Both URL and
Socket perform
networking on top of a stream-based network connection. Setting up and
maintaining a stream across a network takes work at the network level,
however. Sometimes you need a low-level way to speed a packet of data
across a network, but you don't care about maintaining a stream. If, in
addition, you don't need a guarantee that your data will get there or
that the packets of data will arrive in the order you sent them, you
may be interested in the DatagramSocket and
DatagramPacket classes:
import java.net.*;
// Send a message to another computer via a datagram try { String hostname = "host.example.com"; // The computer to send the data to InetAddress address = // Convert the DNS hostname InetAddress.getByName(hostname); // to a lower-level IP address. int port = 1234; // The port to connect to String message = "The eagle has landed."; // The message to send byte[] data = message.getBytes(); // Convert string to bytes DatagramSocket s = new DatagramSocket(); // Socket to send message with DatagramPacket p = // Create the packet to send new DatagramPacket(data, data.length, address, port); s.send(p); // Now send it! s.close(); // Always close sockets when done } catch (UnknownHostException e) {} // Thrown by InetAddress.getByName() catch (SocketException e) {} // Thrown by new DatagramSocket() catch (java.io.IOException e) {} // Thrown by DatagramSocket.send()
// Here's how the other computer can receive the datagram try { byte[] buffer = new byte[4096]; // Buffer to hold data DatagramSocket s = new DatagramSocket(1234); // Socket that receives it // through DatagramPacket p = new DatagramPacket(buffer, buffer.length); // The packet that receives it s.receive(p); // Wait for a packet to arrive String msg = // Convert the bytes from the new String(buffer, 0, p.getLength()); // packet back to a string. s.close(); // Always close the socket } catch (SocketException e) {} // Thrown by new DatagramSocket() catch (java.io.IOException e) {} // Thrown by DatagramSocket.receive()
java.util.Properties is a subclass of
java.util.Hashtable , a legacy collections class
that predates the Collections API of Java 1.2. A
Properties object maintains a mapping between
string keys and string values and defines methods that allow the
mappings to be written to and read from a simply formatted text
file. This makes the Properties class ideal for
configuration and user preference files. The
Properties class is also used for the system
properties returned by System.get- Property() :
import java.util.*; import java.io.*;
// Note: many of these system properties calls throw a security exception if // called from untrusted code such as applets. String homedir = System.getProperty("user.home"); // Get a system property Properties sysprops = System.getProperties(); // Get all system properties
// Print the names of all defined system properties for(Enumeration e = sysprops.propertyNames(); e.hasMoreElements();) System.out.println(e.nextElement());
sysprops.list(System.out); // Here's an even easier way to list the properties
// Read properties from a configuration file Properties options = new Properties(); // Empty properties list File configfile = new File(homedir, ".config"); // The configuration file try { options.load(new FileInputStream(configfile)); // Load props from the file } catch (IOException e) { /* Handle exception here */ }
// Query a property ("color"), specifying a default ("gray") if undefined String color = options.getProperty("color", "gray");
// Set a property named "color" to the value "green" options.setProperty("color", "green");
// Store the contents of the Properties object back into a file try { options.store(new FileOutputStream(configfile), // Output stream "MyApp Config File"); // File header comment text } catch (IOException e) { /* Handle exception */ }
Preferences
Java 1.4 introduces a new Preferences API, which is specifically
tailored for working with user and systemwide preferences and is
more useful than Properties for this purpose. The Preferences API
is defined by the java.util.prefs package. The
key class in that package is Preferences .
You can obtain a Preferences object that contains
user-specific preferences with the static method
Preferences.userNodeForPackage() and obtain a
Preferences object that contains systemwide
preferences with
Preferences.systemNodeForPackage() . Both
methods take a java.lang.Class object as their
sole argument and return a Preferences object
shared by all classes in that package. (This means that
the preference names you use must be unique within the package.)
Once you have a Preferences object, use the
get() method to query the string value of a
named preference, or use other type-specific methods such as
getInt() , getBoolean() , and
getByteArray() . Note that to query
preference values, a default value must be passed for all methods. This
default value is returned if no preference with the specified name
has been registered or if the file or database that holds the
preference data cannot be accessed. A typical use
of Preferences is the following:
package com.davidflanagan.editor; import java.util.prefs.Preferences;
public class TextEditor { // Fields to be initialized from preference values public int width; // Screen width in columns public String dictionary; // Dictionary name for spell checking
public void initPrefs() { // Get Preferences objects for user and system preferences for this package Preferences userprefs = Preferences.userNodeForPackage(TextEditor.class); Preferences sysprefs = Preferences.systemNodeForPackage(TextEditor.class);
// Look up preference values. Note that you always pass a default value. width = userprefs.getInt("width", 80); // Look up a user preference using a system preference as the default dictionary = userprefs.get("dictionary" sysprefs.get("dictionary", "default_dictionary")); } }
In addition to the get() methods for querying
preference values, there are corresponding
put() methods for setting the values of named
preferences:
// User has indicated a new preference, so store it userprefs.putBoolean("autosave", false);
If your application wants to be notified of user or
system preference changes while the application is in progress, it
may register a PreferenceChangeListener with
addPreferenceChangeListener() . A
Preferences object can export the names and
values of its preferences as an XML file and can read
preferences from such an XML file. (See
importPreferences() , exportNode() , and
exportSubtree() in java.util.pref.Preferences in .)
Preferences objects exist in a hierarchy that
typically corresponds to the hierarchy of package names. Methods
for navigating this hierarchy exist but are not typically used by
ordinary applications.
Another new feature of Java 1.4 is the Logging API, defined in the
java.util.logging package. Typically,
the application developer uses a Logger object
with a name that corresponds to the class or package name of the
application to generate log messages at any of seven severity
levels (see the Level class in ). These messages
may report errors and warnings or provide informational
messages about interesting events in the application's life cycle.
They can include debugging information or even trace the execution
of important methods within the program.
The system administrator
or end user of the application is responsible for setting up a
logging configuration file that specifies where log messages are
directed (the console, a file, a network socket, or a combination
of these), how they are formatted (as plain text or XML documents),
and at what severity threshold they are logged (log messages with
a severity below the specified threshold are discarded with very
little overhead and should not significantly impact the
performance of the application). The logging level
severity threshold can be configured independently for each named
Logger . This end-user configurability means
that you can write programs to output diagnostic messages that
are normally discarded but can be logged during program
development or when a problem arises in a deployed application.
Logging is particularly useful for applications such as servers that
run unattended and do not have a graphical user interface.
For most applications, using the Logging API is quite simple.
Obtain a named Logger object whenever necessary
by calling the static Logger.getLogger()
method, passing the class or package name of the application as
the logger name. Then, use one of the many
Logger instance methods to generate log
messages. The easiest methods to use have names that correspond
to severity levels, such as severe() ,
warning() , and info() :
import java.util.logging.*;
// Get a Logger object named after the current package Logger logger = Logger.getLogger("com.davidflanagan.servers.pop"); logger.info("Starting server."); // Log an informational message ServerSocket ss; // Do some stuff try { ss = new ServerSocket(110); } catch(Exception e) { // Log exceptions logger.log(Level.SEVERE, "Can't bind port 110", e); // Complex log message logger.warning("Exiting"); // Simple warning return; } logger.fine("got server socket"); // Low-severity (fine-detail) debug message
|