Cover | Table of Contents | Colophon
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.
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.
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.
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
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.
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");
}
}
}
copyStreamToViewingArea( )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:
FileInputStream and
File-OuputStream are examples
of primitive streams.
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.
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.
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)
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.
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.
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 OutputStreamjava.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.
java.net package.
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
Socket.
ServerSocket
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.
ServerSocket
. As part of
doing so, you will supply a port on which
ServerSocket listens for connections.
accept( ) method of
ServerSocket.Once you do
this, the server program simply waits for client connections.
ServerSocket is the accept( )
method. It has the following signature:
public Socket accept( ) throws IOException
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.
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.
ServerSocket is created and accept(
) is called.
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)
public boolean getKeepAlive( )
public void setKeepAlive(boolean on)
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.
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.
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++.
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.
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.
Printer:
public interface Printer extends PrinterConstants {
public boolean printerAvailable( );
public boolean printDocument(DocumentDescription document) throws
PrinterException;
}
Printer interface
and make it available over the network.
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;
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:
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.
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;
....
}
DocumentDescription to the server.
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( );
}
}
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( ));
}
}
}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);
}
}
}
Printer interface.
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.
Printer interface.
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.
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;
....
}
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:
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.
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.
public interface Printer extends PrinterConstants, Remote {
public boolean printerAvailable( ) throws RemoteException;
public boolean printDocument(DocumentDescription document) throws RemoteException,
PrinterException;
}
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.
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.
RemoteException to every method has one
important consequence. Recall that rmic is used to
automatically generate a stub class for each implementation of
PrinterServer and a batch file that starts the RMI
registry and then runs our program. The former is shown in Example 4-5.
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( );
}
}
}
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.
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.
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){
....
}
}
...
}
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.
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.