ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

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

Pages: 1, 2, 3

Next Pagearrow