BUY THIS BOOK
Add to Cart

Print Book $54.99


Safari Books Online

What is this?

Add to UK Cart

Print Book £39.50

What is this?

Looking to Reprint this content?


Java RMI
Java RMI By William Grosso
October 2001
Pages: 572

Cover | Table of Contents | Colophon


Table of Contents

Chapter 1: Streams
This chapter discusses Java's stream classes, which are defined in the java.io.* package. While streams are not really part of RMI, a working knowledge of the stream classes is an important part of an RMI programmer's skillset. In particular, this chapter provides essential background information for understanding two related areas: sockets and object serialization.
A stream is an ordered sequence of bytes. However, it's helpful to also think of a stream as a data structure that allows client code to either store or retrieve information. Storage and retrieval are done sequentially—typically, you write data to a stream one byte at a time or read information from the stream one byte at a time. However, in most stream classes, you cannot "go back"—once you've read a piece of data, you must move on. Likewise, once you've written a piece of data, it's written.
You may think that a stream sounds like an impoverished data structure. Certainly, for most programming tasks, a HashMap or an ArrayList storing objects is preferable to a read-once sequence of bytes. However, streams have one nice feature: they are a simple and correct model for almost any external device connected to a computer. Why correct? Well, when you think about it, the code-level mechanics of writing data to a printer are not all that different from sending data over a modem; the information is sent sequentially, and, once it's sent, it can not be retrieved or "un-sent." Hence, streams are an abstraction that allow client code to access an external resource without worrying too much about the specific resource.
Using the streams library is a two-step process. First, device-specific code that creates the stream objects is executed; this is often called "opening" the stream. Then, information is either read from or written to the stream. This second step is device-independent; it relies only on the stream interfaces. Let's start by looking at the stream classes offered with Java:
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Core Classes
A stream is an ordered sequence of bytes. However, it's helpful to also think of a stream as a data structure that allows client code to either store or retrieve information. Storage and retrieval are done sequentially—typically, you write data to a stream one byte at a time or read information from the stream one byte at a time. However, in most stream classes, you cannot "go back"—once you've read a piece of data, you must move on. Likewise, once you've written a piece of data, it's written.
You may think that a stream sounds like an impoverished data structure. Certainly, for most programming tasks, a HashMap or an ArrayList storing objects is preferable to a read-once sequence of bytes. However, streams have one nice feature: they are a simple and correct model for almost any external device connected to a computer. Why correct? Well, when you think about it, the code-level mechanics of writing data to a printer are not all that different from sending data over a modem; the information is sent sequentially, and, once it's sent, it can not be retrieved or "un-sent." Hence, streams are an abstraction that allow client code to access an external resource without worrying too much about the specific resource.
Using the streams library is a two-step process. First, device-specific code that creates the stream objects is executed; this is often called "opening" the stream. Then, information is either read from or written to the stream. This second step is device-independent; it relies only on the stream interfaces. Let's start by looking at the stream classes offered with Java: InputStream and OutputStream.
InputStream is an abstract class that represents a data source. Once opened, it provides information to the client that created it. The
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Viewing a File
To make this discussion more concrete, we will now discuss a simple application that allows the user to display the contents of a file in a JTextArea. The application is called ViewFile and is shown in Example 1-1. Note that the application's main( ) method is defined in the com.ora.rmibook.chapter1.ViewFile class. The resulting screenshot is shown in Figure 1-1.
Figure 1-1: The ViewFile application
Example 1-1. ViewFile.java
public class ViewfileFrame extends ExitingFrame{
//  lots of code to set up the user interface.
//  The View button's action listener is an inner class

	private void copyStreamToViewingArea(InputStream fileInputStream)
         throws IOException {
		BufferedInputStream bufferedStream = new BufferedInputStream(fileInputStream);
		int nextByte;
		_fileViewingArea.setText("");
		StringBuffer localBuffer = new StringBuffer(  );
		while( -1 != (nextByte = bufferedStream.read(  )))   {
			char nextChar = (char) nextByte; 	
			localBuffer.append(nextChar);
		}
		_fileViewingArea.append(localBuffer.toString(  ));
	}

	private class ViewFileAction extends AbstractAction {
		public ViewFileAction(  ) {
			putValue(Action.NAME, "View");
			putValue(Action.SHORT_DESCRIPTION, "View file contents in main text area.");
	}

		public void actionPerformed(ActionEvent event) {
			FileInputStream fileInputStream = _fileTextField.getFileInputStream(  );
			if (null==fileInputStream) {
				_fileViewingArea.setText("Invalid file name");
			}
			else {
				try {
					copyStreamToViewingArea(fileInputStream);
					 fileInputStream.close(  );
				}
				 catch (java.io.IOException ioException)  {
					_fileViewingArea.setText("\n Error occured while reading file");
				}
			}
		}
The important part of the code is the View button's action listener and the copyStreamToViewingArea( )
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Layering Streams
The use of BufferedInputStream illustrates a central idea in the design of the streams library: streams can be wrapped in other streams to provide incremental functionality. That is, there are really two types of streams:
Primitive streams
These are the streams that have native methods and talk to external devices. All they do is transmit data exactly as it is presented. FileInputStream and File-OuputStream are examples of primitive streams.
Intermediate streams
These streams are not direct representatives of a device. Instead, they function as a wrapper around an already existing stream, which we will call the underlying stream. The underlying stream is usually passed as an argument to the intermediate stream's constructor. The intermediate stream has logic in its read( ) or write( ) methods that either buffers the data or transforms it before forwarding it to the underlying stream. Intermediate streams are also responsible for propagating flush( ) and close( ) calls to the underlying stream. BufferedInputStream and BufferedOutputStream are examples of intermediate streams.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Readers and Writers
The last topics I will touch on in this chapter are the Reader and Writer abstract classes. Readers and writers are like input streams and output streams. The primary difference lies in the fundamental datatype that is read or written; streams are byte-oriented, whereas readers and writers use characters and strings.
The reason for this is internationalization. Readers and writers were designed to allow programs to use a localized character set and still have a stream-like model for communicating with external devices. As you might expect, the method definitions are quite similar to those for InputStream and OutputStream. Here are the basic methods defined in Reader:
public void close(  )
public void mark(int readAheadLimit) 
public boolean markSupported(  ) 
public int read(  ) 
public int read(char[] cbuf) 
public int read(char[] cbuf, int off, int len) 
public boolean ready(  ) 
public void reset(  ) 
public long skip(long n) 
These are analogous to the read( ) methods defined for InputStream. For example, read( ) still returns an integer. The difference is that, instead of data values being in the range of 0-255 (i.e., single bytes), the return value is in the range of 0-65535 (appropriate for characters, which are 2 bytes wide). However, a return value of -1 is still used to signal that there is no more data.
The only other major change is that InputStream's available( ) method has been replaced with a boolean method, ready( ), which returns true if the next call to read( ) doesn't block. Calling ready( ) on a class that extends Reader is analogous to checking (available( ) > 0) on InputStream.
There aren't nearly so many subclasses of Reader or Writer as there are types of streams. Instead, readers and writers can be used as a layer on top of streams—most readers have a constructor that takes an InputStream as an argument, and most writers have a constructor that takes an OutputStream
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Chapter 2: Sockets
In this chapter, we review Java's socket classes. Sockets are an abstraction that allow two programs, usually on different machines, to communicate by sending data through streams. Strictly speaking, the socket classes (which are defined in the java.net package) are not part of RMI. However, RMI uses Java's socket classes to handle communication between distinct processes. Thus, a basic knowledge of how sockets work is fundamental to understanding RMI. This chapter's coverage, though far from complete, constitutes the core of what an RMI programmer needs to know.
The Internet is built out of computers that are connected by wires. Each wire serves as a way to exchange information between the two computers it connects. Information is transferred in small, discrete chunks of data called datagrams.
Each datagram has a header and a data area. The header describes the datagram: where the datagram originated, what machines have handled the datagram, the type and length of the data being sent, and the intended destination of the the datagram. The data area consists of the actual information that is being sent. In almost all networking protocols, the data area is of limited size. For example, the Internet Protocol (frequently referred to as IP) restricts datagrams to 64 KB.
The Internet Protocol is also an example of what is frequently called a connectionless protocol—each datagram is sent independently, and there is no guarantee that any of the datagrams will actually make it to their destination. In addition, the sender is not notified if a datagram does not make it to the destination. Different datagrams sent to the same destination machine may arrive out of order and may actually travel along different paths to the destination machine.
Connectionless protocols have some very nice features. Conceptually, they're a lot like the postal service. You submit an envelope into the system, couriers move it around, and, if all goes well, it eventually arrives at the destination. However, there are some problems. First, you have no control over which couriers handle the envelope. In addition, the arrival time of the envelope isn't particularly well-specified. This lack of control over arrival times means that connectionless protocols, though fast and very scalable, aren't particularly well suited for distributed applications.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Internet Definitions
The Internet is built out of computers that are connected by wires. Each wire serves as a way to exchange information between the two computers it connects. Information is transferred in small, discrete chunks of data called datagrams.
Each datagram has a header and a data area. The header describes the datagram: where the datagram originated, what machines have handled the datagram, the type and length of the data being sent, and the intended destination of the the datagram. The data area consists of the actual information that is being sent. In almost all networking protocols, the data area is of limited size. For example, the Internet Protocol (frequently referred to as IP) restricts datagrams to 64 KB.
The Internet Protocol is also an example of what is frequently called a connectionless protocol—each datagram is sent independently, and there is no guarantee that any of the datagrams will actually make it to their destination. In addition, the sender is not notified if a datagram does not make it to the destination. Different datagrams sent to the same destination machine may arrive out of order and may actually travel along different paths to the destination machine.
Connectionless protocols have some very nice features. Conceptually, they're a lot like the postal service. You submit an envelope into the system, couriers move it around, and, if all goes well, it eventually arrives at the destination. However, there are some problems. First, you have no control over which couriers handle the envelope. In addition, the arrival time of the envelope isn't particularly well-specified. This lack of control over arrival times means that connectionless protocols, though fast and very scalable, aren't particularly well suited for distributed applications.
Distributed applications often require three features that are not provided by a connectionless protocol: programs that send data require
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Sockets
Sockets are an abstraction for network connections that first appeared on Unix systems in the mid-1970s. In the intervening 25 years, the socket interface has become a cornerstone of distributed programming. Java supports sockets with the classes and interfaces defined in the java.net package.
Specifically, java.net contains two classes that are the core Java classes used when reliable communication between two different processes is necessary: Socket and ServerSocket. They have the following roles:
Socket
Enables a single connection between two known, established processes. In order to exchange information, both programs must have created instances of Socket.
ServerSocket
Manages initial connections between a client and a server. That is, when a client connects to a server using an instance of Socket, it first communicates with ServerSocket. ServerSocket immediately creates a delegate (ordinary) socket and assigns this new socket to the client. This process, by which a socket-to-socket connection is established, is often called handshaking.
Another way to think of this: sockets are analogous to phone lines; ServerSockets are analogous to operators who manually create connections between two phones.
In order to create a socket connection to a remote process, you must know two pieces of information: the
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
ServerSockets
So far, we've focused on how to write a client program using sockets. Our example code assumed that a server application was already running, and the server was accepting connections on a well-known port. The next logical step in our discussion of sockets is to write an application that will accept connections. Fortunately, this isn't much more complicated than creating a client application. The steps are:
  1. Create an instance of ServerSocket . As part of doing so, you will supply a port on which ServerSocket listens for connections.
  2. Call the accept( ) method of ServerSocket.Once you do this, the server program simply waits for client connections.
The key to using ServerSocket is the accept( ) method. It has the following signature:
public Socket accept(  ) throws IOException
There are two important facts to note about accept( ). The first is that accept( ) is a blocking method. If a client never attempts to connect to the server, the server will sit and wait inside the accept( ) method. This means that the code that follows the call to the accept( ) method will never execute.
The second important fact is that accept( ) creates and returns an instance of Socket. The socket that accept( ) returns is created inside the body of the accept( ) method for a single client; it encapsulates a connection between the client and the server.
Therefore, any server written in Java executes the following sequence of steps:
  1. The server is initialized. Eventually, an instance of ServerSocket is created and accept( ) is called.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Customizing Socket Behavior
In addition to the basic methods for creating connections and sending data, the Socket class defines a number of methods that enable you to set some fairly standard socket parameters. Setting these standard socket parameters won't change how the rest your code interacts with the socket. However, it will change the socket's network behavior. The methods, paired along get( )/set( ) lines, are:
public boolean getKeepAlive(  ) 
public void setKeepAlive(boolean on) 

public int getReceiveBufferSize(  ) 
public void setReceiveBufferSize(int size) 
public int getSendBufferSize(  ) 
public void setSendBufferSize(int size) 

public int getSoLinger(  ) 
public void setSoLinger(boolean on, int linger) 

public int getSoTimeout(  ) 
public void setSoTimeout(int timeout) 

public boolean getTcpNoDelay(  ) 
public void setTcpNoDelay(boolean on) 
In the rest of this section, we discuss these parameters in more detail:
public boolean getKeepAlive( )
public void setKeepAlive(boolean on)
One problem with distributed applications is that if no data arrives over a long period of time, you need to wonder why. On one hand, it could be that the other program just hasn't had any information to send recently. On the other hand, the other program could have crashed. TCP handles this problem by allowing you to send an "Are you still alive?" message every so often to quiet connections. The way to do this is to call setKeepAlive( ) with a value of true. Note that you don't need to worry about one side of the connection dying when you use RMI. The distributed garbage collector and the leasing mechanism (which we'll discuss in Chapter 16) handle this problem automatically.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Special-Purpose Sockets
Socket and ServerSocket are object-oriented wrappers that encapsulate the TCP/IP communication protocol. They are designed to simply pass data along the wire, without transforming the data or changing it in any way. This can be either an advantage or a drawback, depending on the particular application.
Because data is simply passed along the network, the default implementation of Socket is fast and efficient. Moreover, sockets are easy to use and highly compatible with existing applications. For example, consider the WebBrowser application discussed earlier in the chapter. We wrote a Java program that accepted connections from an already existing application (in our case, Netscape Navigator) that was written in C++.
There are, however, two important potential downsides to simply passing along the data:
  • The data isn't very secure.
  • Communications may use excessive bandwidth.
Security is an issue because many applications run over large-scale networks, such as the Internet. If data is not encrypted before being sent, it can easily be intercepted by third parties who are not supposed to have access to the information.
Bandwidth is also an issue because data being sent is often highly redundant. Consider, for example, a typical web page. My web browser has 145 HTML files stored in its cache. The CompressFile application from Chapter 1, on average, compresses these files to less than half their original size. If HMTL pages are compressed before being sent, they can be sent much faster.
Of course, HTML is a notoriously verbose data format, and this measurement is therefore somewhat tainted. But, even so, it's fairly impressive. Simply using compression can cut bandwidth costs in half, even though it adds additional processing time on both the client and server. Moreover, many data formats are as verbose as HTML. Two examples are XML-based communication and protocols such as RMI's JRMP, which rely on object serialization (we'll discuss serialization in detail in Chapter 10).
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Using SSL
The Secure Sockets Layer (SSL) is a standard protocol for both authenticating users and encrypting data that is then sent over ordinary sockets. That is, implementations of SSL are conceptually similar to CompressingSocket—they take data and transform it before sending it over the wire. The only difference is that CompressingSocket compresses, while SSL sockets first authenticate (at the beginning of a session) and then encrypt.
SSL has three helpful features:
  • It's a publicly defined protocol. SSL was first defined and implemented by Netscape. But the specification is publicly available and has been subject to intense public scrutiny.
  • It's commonly used. Almost every language that can use sockets has at least one SSL library package already implemented for it. And it is easy to define a secure version of a protocol by simply specifying that the secure version is a layer on top of SSL instead of simply being defined over cleartext sockets. This, for example, is the way HTTPS (the secure version of HTTP) is defined. Thus, in almost any situation where sockets can be used, SSL can be used with minimal extra programmer overhead and very few code changes.
  • It's good enough. While not absolutely secure, SSL meets the criteria for practical security in a wide variety of situations.
SSL has been around, in one form or another, since 1995. Currently, there are three versions in active use: SSL2, SSL3, and Transport Layer Security (TLS). SSL2 is the oldest version of the spec and is very widely used. SSL3 is newer, and TLS is a successor to SSL3 (the main change from SSL3 is that the Internet Engineering Task Force has taken over stewardship of the standard). Neither SSL3 nor TLS seems to be widely adopted at this point.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Chapter 3: A Socket-Based Printer Server
In the previous two chapters, we covered the basics of using streams and sockets. In this chapter, we'll use what we have learned to build a simple server application. Along the way, we'll confront many of the problems that distributed applications face. And our solutions will help to introduce and explain most of the basic RMI infrastructure.
The application we're going to build is a very simple one; it takes a local printer and makes it available over the network via a socket-based API. Our intended architecture looks like the diagram shown in Figure 3-1.
Figure 3-1: A network printer using a socket-based API
This figure illustrates three main components:
The client application
This is a program running on a separate machine somewhere on the network. There is nothing special about the machine this program runs on (in fact, many different machines can run this program). It is responsible for presenting a user interface to the user, getting print requests, and sending those requests to the server application. The client application is written entirely in Java and is therefore easy to install on any machine with a JVM.
The server application
This is a program that resides on a single, designated machine on the network. The machine it runs on is connected locally to a printer. The server application's roles are to receive print requests over the network from the client program, perform whatever intermediate tasks are necessary, and then forward the request to the printer.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
A Network-Based Printer
The application we're going to build is a very simple one; it takes a local printer and makes it available over the network via a socket-based API. Our intended architecture looks like the diagram shown in Figure 3-1.
Figure 3-1: A network printer using a socket-based API
This figure illustrates three main components:
The client application
This is a program running on a separate machine somewhere on the network. There is nothing special about the machine this program runs on (in fact, many different machines can run this program). It is responsible for presenting a user interface to the user, getting print requests, and sending those requests to the server application. The client application is written entirely in Java and is therefore easy to install on any machine with a JVM.
The server application
This is a program that resides on a single, designated machine on the network. The machine it runs on is connected locally to a printer. The server application's roles are to receive print requests over the network from the client program, perform whatever intermediate tasks are necessary, and then forward the request to the printer.
The printer
In this example, we're assuming that the printer exists and is activated, and that the code for interfacing a Java program to a local printer has been written. Printer manufacturers are fairly good at providing printer drivers. However, if we implement this part of the application, it could require the use of the Java Native Interface to communicate advanced commands to a printer driver written in C or C++. One consequence of this is that the server application may not entirely be a Java program and, therefore, installing the server might involve significant modifications to the underlying operating system.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Basic Objects
It's often useful to start the process of designing a distributed application by pretending it is a single-machine application. Doing this allows architects to focus on finding the domain objects first. Once the domain objects have been discovered, and their roles have been defined, the distributed infrastructure can be built around them.
In this case, we'll start with a very simple interface for our abstract notion of Printer:
public interface Printer extends PrinterConstants {
	public boolean printerAvailable(  );
	public boolean printDocument(DocumentDescription document) throws
         PrinterException;
}
Our goal is to take a concrete implementation of the Printer interface and make it available over the network.
This definition of Printer relies on two additional classes: DocumentDescription and PrinterException. These are both fairly simple classes, designed more to encapsulate related pieces of information than to implement complex behavior. The definition of DocumentDescription begins with five state variables that encapsulate the print request:
public class DocumentDescription {
	public static final int FAST_PRINTING = 0;
	public static final int HIGH_QUALITY_PRINTING = 1;

	public static final int POSTSCRIPT = 0;
	public static final int PDF = 1;

	private DataInputStream _actualDocument;
	private int _documentType;
	private boolean _printTwoSided;
	private int _printQuality;
	private int _length;
The only interesting aspect of this is the decision to use Stream to represent the actual document, rather than storing just a filename. Doing this makes the implementation of the printer server much simpler for two reasons:
  • There is no guarantee that the machine the server is running on has access to the same files as the machine running the client program.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Protocol
Now that the basic objects are in place, we have a better idea of what will happen inside our application: the client will send a DocumentDescription to the server, and the server will respond to whether the print request succeeded. In the event that it didn't, the server will send a PrinterException to the client containing more information.
In order to make this concrete, we need to address two fundamental issues. The first involves how the client will find the server. The client somehow needs to know the machine address and port number of the server process. The traditional way of solving this problem is to define it either as constants in a class or via a well-known text file accessible by both the client and the server. For this implementation, we'll use the former and define some constants in an abstract class:
public abstract class NetworkBaseClass {
	public static final String DEFAULT_SERVER_NAME = "localhost";
	public static final int DEFAULT_SERVER_PORT = 2100;
	public static final int DEFAULT_SERVER_BACKLOG = 10;
....
}
As long as this class is available to both the client and the server, we've solved the location problem.
The next issue is to define and implement an application protocol. In other words, we must address the question of how the client and server communicate once they have connected. In our case, the information flow follows these two steps:
  1. The client sends an instance of DocumentDescription to the server.
  2. The server sends back a response indicating whether the document was successfully printed.
After the client receives the server's response, the connection is closed, and there is no shared state between the client and the server. This means that these two steps completely define our protocol.
The process in which a client takes a request, including arguments and data, and puts it into a format suitable for sending over a socket connection is often referred to as
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Application Itself
Once we've written the data objects and the objects that encapsulate the network protocol, writing the rest of the application is easy. The server doesn't even need a user interface; it consists of the main( ) function, which instantiates a printer, creates an instance of ServerNetworkWrapper, and then calls accept( ) on the instance of ServerNetworkWrapper:
public static void main(String args[]) {
	try {
		File logfile = new File("C:\\temp\\serverLogfile");
		OutputStream outputStream = new FileOutputStream(logfile);
		Printer printer = new NullPrinter(outputStream);
		ServerNetworkWrapper serverNetworkWrapper = new
              ServerNetworkWrapper(printer);
		serverNetworkWrapper.accept(  );
	}
	catch (Exception e) {
		e.printStackTrace(  );
	 }
}
Apart from the user interface, the client application is equally straightforward. Our user interface is shown in Figure 3-2.
Figure 3-2: The user interface for the printer client
The Choose File button uses a JFileChooser to let the user select a file (whose name is displayed in the text area). All the network communication is done using ActionListener, which has been added to the Print File button. And all ActionListener does is instantiate ClientNetworkWrapper and call sendDocumenttoPrinter( ):
private class PrintFile implements ActionListener {
	public void actionPerformed(ActionEvent event) {
		try {
			ClientNetworkWrapper clientNetworkWrapper = new ClientNetworkWrapper(  );
			FileInputStream document = new FileInputStream(_fileChooser
                  getSelectedFile(  ));
			clientNetworkWrapper.sendDocumentToPrinter(document);
		}
		catch (Exception exception) {
			_messageBox.setText("Exception attempting to print " + 
				(_fileChooser.getSelectedFile()).getAbsolutePath(  ) +
				"\n\t Error was: " + exception.toString(  ));

		}
	}
}
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Evolving the Application
At this point, we're done with the first version of the application. We've successfully used sockets to implement the networking portion of a simple, distributed printing application. But the words "first version" are very important. There's a long list of features we haven't implemented—some because we were lazy, others because they probably wouldn't be requested in the first version of an application. For example:
  • Users will want more than one printer to be available.
  • Users will want to have a print queue. Important documents should be moved to the top of a print queue; less urgent jobs should wait until the printer isn't busy.
  • If we're going to have a print queue, it would be nice to be able to explicitly access the queue, both to see the entire queue, and to make queries about our job. It should also be possible to cancel a print request that is in the queue but hasn't already been sent to the printer.
  • As we scale to more users, application responsiveness will become important. This is especially true on a LAN, as it is almost certainly faster to send a document than it is to print it. Hence, we should decouple printing a document from receiving it over the wire. In particular, the current implementation of ServerNetworkWrapper's accept( ) method, shown here, will force the client applications to wait until an existing print job is finished before they can send a document:
    public void accept(  ) {
    	while (true) {
    		Socket clientSocket = null;
    		try {
    			clientSocket = _serverSocket.accept(  );	// blocking call
    		 	processPrintRequest(clientSocket);
    		}
    	}
    }
  • Managers will want to track resource consumption. This will involve logging print requests and, quite possibly, building a set of queries that can be run against the printer's log.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Chapter 4: The Same Server, Written Using RMI
In this chapter, we continue our discussion by reimplementing the printer server using RMI as a distribution mechanism instead of sockets. As part of doing so, I will introduce the core infrastructure of RMI in a familiar setting. In fact, the application itself will look remarkably similar to the socket-based version. By the end of this chapter, you will have received a glimpse at how an RMI application is structured and the basic sequence of steps required to build one.
In the previous chapter, we covered the basics of implementing a socket-based distributed application. In doing so, we reinforced the lessons of the previous two chapters on streams and sockets. In addition, we discovered that the code necessary for writing a socket-based distributed application falls into five basic categories:
  • Code that actually does something useful. This code is commonly referred to as business logic. An example is an implementation of the Printer interface.
  • User interface code for the client application.
  • Code that deals with marshalling and demarshalling of data and the mechanics of invoking a method in another process. This is tedious code, but it is straightforward to write and is usually a significant percentage of the application.
  • Code that launches and configures the application. We used a number of hard-wired constants (in NetworkBaseClass) to enable the client to initially connect with the server. And we wrote two main( ) methods—one to launch the client and one to launch the server.
  • Code whose sole purpose is to make a distributed application more robust and scalable. This usually involves one or more of the following:
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Basic Structure of RMI
In the previous chapter, we covered the basics of implementing a socket-based distributed application. In doing so, we reinforced the lessons of the previous two chapters on streams and sockets. In addition, we discovered that the code necessary for writing a socket-based distributed application falls into five basic categories:
  • Code that actually does something useful. This code is commonly referred to as business logic. An example is an implementation of the Printer interface.
  • User interface code for the client application.
  • Code that deals with marshalling and demarshalling of data and the mechanics of invoking a method in another process. This is tedious code, but it is straightforward to write and is usually a significant percentage of the application.
  • Code that launches and configures the application. We used a number of hard-wired constants (in NetworkBaseClass) to enable the client to initially connect with the server. And we wrote two main( ) methods—one to launch the client and one to launch the server.
  • Code whose sole purpose is to make a distributed application more robust and scalable. This usually involves one or more of the following: client-side caching (so the server does less work); increasing the number of available servers in a way that's as transparent as possible to the client; using naming services and load balancing; making it possible for a server to handle multiple requests simultaneously (threading); or automatically starting and shutting down servers, which allows the
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Architecture Diagram Revisited
While the printer application is simple enough so that the RMI-based application is similar to the socket-based application, RMI does add one more conceptual wrinkle. Recall that in the socket-based version, we used a set of constants to help the client find the server:
public abstract class NetworkBaseClass {
	public static final String DEFAULT_SERVER_NAME = "localhost";
 	public static final int DEFAULT_SERVER_PORT = 2100;
	public static final int DEFAULT_SERVER_BACKLOG = 10;
....
}
That's a bad design strategy. If the server is moved to another computer, or if you want to use the same client to talk to multiple servers, you need to deploy a new version of the client application.
A much better strategy is to have a centralized naming service. A naming service is an application that runs on a central server and functions like a phone book. In order for a client to connect to a server, it must do two things:
  1. It must connect to the naming service and find out where the server is running.
  2. It must then connect to the server.
At first glance, a naming service appears to suffer from the same design flaw as NetworkBaseClass. Instead of hardwiring the location of the server into our client code, we're hardwiring the location of the naming service. There are, however, a number of differences that combine to make this a more palatable solution. Among the most significant are:
  • Naming services are fairly simple applications that place limited demands on a computer. This means that the server running a naming service doesn't need to be upgraded often.
  • Naming services are stable applications with simple APIs. They are not updated or revised often.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Implementing the Basic Objects
Now let's start implementing the RMI-based printer server. As in the socket-based version, we have three basic objects: the Printer interface, the DocumentDescription object, and the PrinterException object. Conceptually, these objects are the same as their counterparts in Chapter 3. However, as might be expected, using RMI will force us to change a few details.
There are two basic changes to the Printer interface: it now extends the Remote interface, and every method is defined to throw RemoteException, a subclass of Exception defined in the package java.rmi. This class is shown in Example 4-1.
Example 4-1. Printer.java
public interface Printer extends PrinterConstants, Remote {
	public boolean printerAvailable(  ) throws RemoteException;
	public boolean printDocument(DocumentDescription document) throws RemoteException,
         PrinterException;
} 
That Printer extends Remote shouldn't be a surprise—the whole point of the application is to turn a local printer into a server that can receive calls from clients' applications running on other computers.
The other change involves adding RemoteException to each method signature. RemoteException is an exception thrown by RMI to signal that something unforeseen has happened at the network level. That is, it's a way for the RMI infrastructure to tell a client application that "something went wrong in the RMI infrastructure." For example, if the server crashes while handling a client's request, RMI will automatically throw a RemoteException on the client side.
Adding RemoteException to every method has one important consequence. Recall that rmic is used to automatically generate a stub class for each implementation of
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Rest of the Server
To finish building our server, we need to write launch code. Launch code is code that is application-specific, but not business-domain specific, and handles the details of registering a server with a naming service such as the RMI registry. In our case, this boils down to two pieces of code: a Java program that runs PrinterServer and a batch file that starts the RMI registry and then runs our program. The former is shown in Example 4-5.
Example 4-5. SimpleServer.java
public class SimpleServer implements NetworkConstants {
	public static void main(String args[]) {
		try {
			File logfile = new File("C:\\temp\\serverLogfile");
			OutputStream outputStream = new FileOutputStream(logfile);
			Printer printer = new NullPrinter(outputStream);
			Naming.rebind(DEFAULT_PRINTER_NAME, printer);
		}
		catch (Exception e) {
			e.printStackTrace(  );
		}
	}
} 
This creates an instance of NullPrinter and then binds it into the registry under the name DEFAULT_PRINTER_NAME. The only surprising detail is this: if everything is successful, our program will exit main( ). Don't worry; this is normal. The fact that the RMI registry has a reference (e.g., a stub) for the server keeps the application alive even though we've exited. I'll explain why, and how this works, in Chapter 16.
Note that we used rebind( ) instead of bind( ) in our launch code. The reason is that bind( ) fails if the name we're binding the server to is already in use. rebind( ), on the other hand, is guaranteed to succeed. If another server is bound into the registry using the name we want to use, that server will be unbound from the name. In reality, bind( ) is rarely used in launch code, but is often used in code that attempts to repair or update a registry.
The format of names that are bound into the registry is fairly simple: they follow the pattern
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Client Application
Once the changes to the data objects have been made and the skeletons and stubs have been generated from the server, the networking part of the client application is a remarkably straightforward piece of code. Recall that our client application had the GUI shown in Figure 4-3.
Figure 4-3: Printer/client application GUI
The only part of this that's changed is the ActionListener attached to the Print File button. And it's much simpler:
private class PrintFile implements ActionListener {
	public void actionPerformed(ActionEvent event) {
		try {
			FileInputStream documentStream = new FileInputStream(_fileChooser
                  getSelectedFile(  ));
			DocumentDescription documentDescription = new
			DocumentDescription(documentStream);

/*
			New network code follows
*/
			Printer printer = (Printer) Naming.lookup(DEFAULT_PRINTER_NAME);
			printer.printDocument(documentDescription);
		}
		catch (PrinterException printerException){
		....
		}
	}
	...
}
All this does is use a predetermined name, which must be the same name as the server used to bind, to locate an object inside the RMI registry. It then casts the object to the correct type (the RMI registry interface, like many Java interfaces, returns instances of Object) and invokes the printDocument( ) method on the server. And that's it! We've finished reimplementing the socket-based printer server as an RMI application.
In this code example, as in many of the examples in this book, the client and server must be located on the same machine. This is because the call to Naming.lookup( ) simply used DEFAULT_PRINTER_NAME (with no hostname or port number specified). By changing the arguments used in the call to Naming.lookup( ), you can turn the example into a truly distributed application.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Summary
In this chapter, we've gone over the basics of developing an RMI application in a cookbook-style way, in order to get acquainted with the basic structure and components of an RMI application. Consequently, we glossed over many of the details. However, the key points to remember are:
  • Simple RMI applications are, in fact, not much more complicated than single-process applications.
  • RMI includes reasonable default solutions for the common problems in building distributed applications (serialization handles marshalling and demarshalling, the registry helps clients find servers, and so on).
  • Even when problems arise (e.g., DocumentDescription), the code is remarkably similar to, and simpler than, the analogous socket code.
  • The conceptual cost to using RMI isn't all that high. In most cases, using RMI amounts to adding an extra layer of indirection to your code.
  • The application evolution problems mentioned in Chapter 3 aren't nearly so forbidding when using RMI. The default mechanisms, and the automatically generated stubs and skeletons, handle many application evolution problems nicely.
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
Chapter 5: Introducing the Bank Example
Now that we've seen two versions of the same application, one written using sockets and one written using RMI, it's time to take a step back and look at the whole process of designing a distributed application. In order to do this, this chapter and the following five chapters all concentrate on a single shared example: a distributed banking system. In this chapter, we'll get things underway by talking about the system requirements of a distributed banking system, sketching a rough architecture for the application and discussing the problems that arise in networked applications. By the end of this chapter, you'll have a better idea of the design decisions and choices that must be made to build a distributed system, as well as how to begin.
When traveling, take advantage of more than 13,000 Bank of America ATMs coast to coast. We're in 30 states and the District of Columbia. As a Bank of America Check Card or ATM cardholder, there's no ATM fee when you use an ATM displaying a Bank of America sign...
—Bank of America Advertisement
A simple banking application implementing an automatic teller machine is, in many ways, the ideal first application for someone learning to design and build distributed programs. Why? The most obvious benefit is that the application is easy to understand; most readers of this book know exactly what an ATM is supposed to do. Moreover, there isn't a great deal of "business logic" involved, and the business logic that does exist is straightforward. Finally, there's very little GUI interface code to deal with. This means that most of our discussion deals with the distributed parts of the application, rather than the details of exactly how to process a transaction or which buttons need to be disabled when.
Another important fact is that a banking application is inherently a distributed application with a centralized server (or cluster of servers). In other words, it adheres to a traditional model of a client-server application, in which the two roles are strongly differentiated:
Additional content appearing in this section has been removed.
Purchase this book now or read it online at Safari to get the whole thing!
The Bank Example
When traveling, take advantage of more than 13,000 Bank of America ATMs coast to coast. We're in 30 states and the District of Columbia. As a Bank of America Check Card or ATM cardholder, there's no ATM fee when you use an ATM displaying a Bank of America sign...
—Bank of America Advertisement
A simple banking application implementing an automatic teller machine is, in many ways, the ideal first application for someone lear