The Java Platform
Pages: 1, 2, 3
Threads
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.
Files and Directories
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
Input and Output Streams
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
Networking
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.oreilly.com"); // An absolute URL
url2 = new URL(url, "catalog/books/javanut4/"); // A relative URL
url3 = new URL("http:", "www.oreilly.com", "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.oreilly.com"; // 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.sun.com/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()
Properties and Preferences
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.
Logging
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