|
|
|
|
Exploring Java, 2nd EditionBy Pat Niemeyer & Josh Peck2nd Edition September 1997 1-56592-271-9, Order Number: 2719 614 pages, DM66,- |
The network is the soul of Java. Most of what is new and
exciting about Java centers around the potential for new kinds of
dynamic, networked applications. This chapter discusses the java.net package, which contains classes for communications
and working with networked resources. These classes fall into two
categories: the sockets API and classes for working
with Uniform Resource Locators (URLs). Figure 11.1 shows all of the classes in
java.net.

Java's sockets interface provides access to the standard network protocols used for communications between hosts on the Internet. Sockets are the mechanism underlying all other kinds of portable networked communications. Your processes can use sockets to communicate with a server or peer applications on the Net, but you have to implement your own application-level protocols for handling and interpreting the data. Higher-level features, like remote method calls and distributed objects, are implemented over sockets.
In this chapter, we'll try to provide some practical and realistic examples of Java network programming using sockets and remote method invocation (RMI). In the next chapter, we'll look at URLs, content handlers, and protocol handlers.
Sockets are a low-level programming interface for networked communications. They send streams of data between applications that may or may not be on the same host. Sockets originated in BSD UNIX and are, in other languages, hairy and complicated things with lots of small parts that can break off and choke little children. The reason for this is that most socket APIs can be used with almost any kind of underlying network protocol. Since the protocols that transport data across the network can have radically different features, the socket interface can be quite complex.[1]
[1] For a discussion of sockets in general, see UNIX Network Programming, by Richard Stevens (Prentice-Hall). For a complete discussion of network programming in Java, see Java Network Programming by Elliotte Rusty Harold (O'Reilly).
Java supports a simplified object-oriented interface to sockets that makes network communications considerably easier. If you have done network programming using sockets in C or another structured language, you should be pleasantly surprised at how simple things can be when objects encapsulate the gory details. If this is the first time you've come across sockets, you'll find that talking to another application can be as simple as reading a file or getting user input. Most forms of I/O in Java, including network I/O, use the stream classes described in Chapter 10. Streams provide a unified I/O interface; reading or writing across the Internet is similar to reading or writing a file on the local system.
Java provides different kinds of sockets to support three different
distinct classes of underlying protocols. In this first section,
we'll look at Java's Socket class,
which uses a connection-oriented protocol. A
connection-oriented protocol gives you the equivalent of a telephone
conversation; after establishing a connection, two applications can
send data back and forth; the connection stays in place even when no
one is talking. The protocol ensures that no data is lost and that it
always arrives in order. In the next section we'll look at the
DatagramSocket class, which uses a
connectionless protocol. A connectionless
protocol is more like the postal service. Applications can send short
messages to each other, but no attempt is made to keep the connection
open between messages, to keep the messages in order, or even to
guarantee that they arrive. A
MulticastSocket is a variation of a
DatagramSocket that can be used to send
data to multiple recipients (multicasting); we don't discuss
multicasting in this book.
In theory, just about any protocol family can be used
underneath the socket layer: Novell's IPX,
Apple's AppleTalk, even the old ChaosNet protocols. But this
isn't a theoretical world. In practice, there's only one
protocol family people care about on the Internet, and only one
protocol family Java supports: the Internet protocols,
IP. The Socket class speaks
TCP, and the DatagramSocket class
speaks UDP, both standard Internet protocols.
These protocols are available on any
system that is connected to the Internet.
When writing network applications, it's common to talk about clients and servers. The distinction is increasingly vague, but the side that initiates the conversation is usually the client. The side that accepts the request to talk is usually the server. In the case where there are two peer applications using sockets to talk, the distinction is less important, but for simplicity we'll use the above definition.
For our purposes, the most important difference between a
client and a server is that a client can create a socket to initiate a
conversation with a server application at any time, while a server
must prepare to listen for incoming conversations in advance. The
java.net.Socket class represents a single side of a
socket connection on either the client or server.
In addition, the
server uses the java.net.ServerSocket class to wait
for connections from clients. An application acting as a server
creates a ServerSocket object and waits, blocked in
a call to its accept() method, until a connection
arrives. When it does, the accept() method creates
a Socket object the server uses to
communicate with the client. A server carries on multiple
conversations at once; there is only a single
ServerSocket, but one active
Socket object for each client, as shown in Figure 11.2.

A client needs two pieces of information to locate and connect to another server on the Internet: a hostname (used to find the host's network address) and a port number. The port number is an identifier that differentiates between multiple clients or servers on the same host. A server application listens on a prearranged port while waiting for connections. Clients select the port number assigned to the service they want to access. If you think of the host computers as hotels and the applications as guests, then the ports are like the guests' room numbers. For one guest to call another, he or she must know the other party's hotel name and room number.
A client application opens a connection to a server by constructing a
Socket that specifies the hostname and port number
of the desired server:
try {
Socket sock = new Socket("wupost.wustl.edu", 25);
}
catch ( UnknownHostException e ) {
System.out.println("Can't find host.");
}
catch ( IOException e ) {
System.out.println("Error connecting to host.");
}
This code fragment attempts to connect a Socket to
port 25 (the SMTP mail service) of the host
wupost.wustl.edu. The client handles the
possibility that the hostname can't be resolved
(UnknownHostException) and that it might not be
able to connect to it (IOException).
As an alternative to using a hostname, you can provide a string version
of the host's IP address:
Socket sock = new Socket("128.252.120.1", 25); // wupost.wustl.edu
Once a connection is made, input and output streams can be retrieved with
the Socket getInputStream()
and getOutputStream() methods. The following
(rather arbitrary and strange) code sends and receives some data with the streams.
try {
Socket server = new Socket("foo.bar.com", 1234);
InputStream in = server.getInputStream();
OutputStream out = server.getOutputStream();
// Write a byte
out.write(42);
// Write a newline or carriage return delimited string
PrintWriter pout = new PrintWriter( out, true );
pout.println("Hello!");
// Read a byte
Byte back = in.read();
// Read a newline or carriage return delimited string
BufferedReader bin = new BufferedReader( new InputStreamReader( in );
String response = bin.readLine();
// Send a serialized Java object
ObjectOutputStream oout = new ObjectOutputStream( out );
oout.writeObject( new java.util.Date() );
oout.flush();
server.close();
}
catch (IOException e ) { } In the exchange above, the client first creates a
Socket for communicating with the server. The
Socket constructor specifies the server's
hostname (foo.bar.com) and a prearranged port
number (1234). Once the connection is established, the client writes a
single byte to the server using the
OutputStream's write()
method. It then wraps a PrintWriter around the
OutputStream in order to send text more
easily. Next, it performs the complementary operations, reading a byte
from the server using InputStream's
read() and then creating a
DataInputStream from which to get a string of
text. Finally, we do something really funky and send a serialized Java
object to the server, using an
ObjectOutputStream. (We'll talk in depth
about sending serialized objects later in this chapter.)
The client then terminates the connection with the
close() method. All these operations have the
potential to generate IOExceptions; the
catch clause is where our application would deal
with these.
After a connection is established, a server application uses the same
kind of Socket object for its side of the
communications. However, to accept a connection from a client, it
must first create a ServerSocket, bound to the
correct port. Let's recreate the previous conversation from the
server's point of view:
// Meanwhile, on foo.bar.com...
try {
ServerSocket listener = new ServerSocket( 1234 );
while ( !finished ) {
Socket client = listener.accept(); // wait for connection
InputStream in = client.getInputStream();
OutputStream out = client.getOutputStream();
// Read a byte
Byte someByte = in.read();
// Read a newline or carriage return delimited string
BufferedReader bin = new BufferedReader( new InputStreamReader( in );
String someString = bin.readLine();
// Write a byte
out.write(43);
// Say goodbye
PrintWriter pout = new PrintWriter( out, true );
pout.println("Goodbye!");
// Read a serialized Java object
ObjectInputStream oin = new ObjectInputStream( in );
Date date = (Date)oin.readObject();
client.close();
}
listener.close();
}
catch (IOException e ) { } First, our server creates a ServerSocket attached
to port 1234. On some systems there are rules about what ports an
application can use. Port numbers below 1024 are usually reserved for
system processes and standard, well-known services, so we pick a port
number outside of this range. The ServerSocket need
be created only once. Thereafter we can accept as many connections as
arrive.
Next we enter a loop, waiting for the
accept() method of the
ServerSocket to return an active
Socket connection from a client. When a connection
has been established, we perform the server side of our dialog, then
close the connection and return to the top of the loop to wait for
another connection. Finally, when the server application wants to
stop listening for connections altogether, it calls the
close() method of the
ServerSocket.[2]
[2] A somewhat obscure security feature in TCP/IP specifies that if a server socket actively closes a connection while a client is connected, it may not be able to bind (attach itself) to the same port on the server host again for a period of time (the maximum time to live of a packet on the network). It's possible to turn off this feature, and it's likely that your Java implementation will have done so.
As you can see, this server is single-threaded; it handles
one connection at a time; it doesn't call
accept() to listen for a new connection until it's
finished with the current connection. A more realistic server would
have a loop that accepts connections concurrently and passes them off
to their own threads for processing. (Our tiny HTTP
daemon in a later section will do just this.)
The examples above presuppose the client has permission to
connect to the server, and that the server is allowed to listen on the
specified socket. This is not always the case. Specifically, applets
and other applications run under the auspices of a
SecurityManager that can impose arbitrary
restrictions on what hosts they may or may not talk to, and whether
they can listen for connections. The security policy imposed by the
current version of Netscape Navigator allows untrusted applets to open
socket connections only to the host that served them. That is, they can
talk back only to the server from which their class files were
retrieved. Untrusted applets are not allowed to open server sockets themselves.
Now, this doesn't mean that an untrusted applet can't cooperate with its server to communicate with anyone, anywhere. A server could run a proxy that lets the applet communicate indirectly with anyone it likes. What the current security policy prevents is malicious applets roaming around inside corporate firewalls. It places the burden of security on the originating server, and not the client machine. Restricting access to the originating server limits the usefulness of "trojan" applications that do annoying things from the client side. You won't let your proxy mail-bomb people, because you'll be blamed.
Many networked workstations run a time service that dispenses their
local clock time on a well-known port. This was a precursor of
NTP, the more general Network Time Protocol. In the
next example, DateAtHost, we'll make a specialized
subclass of java.util.Date that fetches the time
from a remote host instead of initializing itself from the local
clock. (See Chapter 9 for a complete discussion of the
Date class.)
DateAtHost connects to the time service
(port 37) and reads four bytes representing the time on the remote
host. These four bytes are interpreted as an integer representing the
number of seconds since the turn of the
century. DateAtHost converts this to Java's
variant of the absolute time (milliseconds since January 1, 1970, a date
that should be familiar to UNIX users) and then
uses the remote host's time to initialize itself:
import java.net.Socket;
import java.io.*;
public class DateAtHost extends java.util.Date {
static int timePort = 37;
static final long offset = 2208988800L; // Seconds from century to
// Jan 1, 1970 00:00 GMT
public DateAtHost( String host ) throws IOException {
this( host, timePort );
}
public DateAtHost( String host, int port ) throws IOException {
Socket server = new Socket( host, port );
DataInputStream din = new DataInputStream( server.getInputStream() );
int time = din.readInt();
server.close();
setTime( (((1L << 32) + time) - offset) * 1000 );
}
}That's all there is to it. It's not very long, even with a
few frills. We have supplied two possible constructors for
DateAtHost. Normally we'll use the first,
which simply takes the name of the remote host as an argument. The
second, overloaded constructor specifies the hostname and the
port number of the remote time service. (If the time service were
running on a nonstandard port, we would use the second constructor to
specify the alternate port number.) This second constructor does the
work of making the connection and setting the time. The first
constructor simply invokes the second (using the
this() construct) with the default port as an
argument. Supplying simplified constructors that invoke their siblings
with default arguments is a common and useful technique.
The second constructor opens a socket to the specified port on the
remote host. It creates a DataInputStream to wrap
the input stream and then reads a 4-byte integer using the
readInt() method. It's no coincidence that the bytes are
in the right order.
Java's DataInputStream
and DataOutputStream classes work with the bytes of
integer types in network byte order (most
significant to least significant). The time protocol (and other
standard network protocols that deal with binary data) also uses the
network byte order, so we don't need to call any conversion
routines. (Explicit data conversions would probably be necessary if we
were using a nonstandard protocol, especially when talking to a
non-Java client or server.) After reading the data, we're
finished with the socket, so we close it, terminating the connection
to the server. Finally, the constructor initializes the rest of the
object by calling Date's
setTime() method with the calculated time
value.[3]
[3] The conversion first creates a long value, which is the unsigned equivalent of the integer
time. It subtracts an offset to make the time relative to the epoch (January 1, 1970) rather than the century, and multiples by 1000 to convert to milliseconds.
The DateAtHost class can work with a time
retrieved from a remote host almost as easily as
Date is used with the time on the local host. The
only additional overhead is that we have to deal with the possible
IOException that can be thrown by the
DateAtHost constructor:
try {
Date d = new DateAtHost( "sura.net" );
System.out.println( "The time over there is: " + d );
int hours = d.getHours();
int minutes = d.getMinutes();
...
}
catch ( IOException e ) { }
This example fetches the time at the host sura.net
and prints its value. It then looks at some components of the time using
the getHours() and getMinutes()
methods of the Date class.
Have you ever wanted your very own Web server? Well, you're in
luck. In this section, we're going to build
TinyHttpd, a minimal but functional
HTTP daemon. TinyHttpd listens
on a specified port and services simple HTTP
"get file" requests. They look something like this:
GET/path/filename[optional stuff]
Your Web browser sends one or more requests as lines for each document
it retrieves. Upon reading a request, the server tries to open
the specified file and send its contents. If that document contains
references to images or other items to be displayed inline,
the browser continues with additional GET
requests. For best performance (especially in a time-slicing
environment), TinyHttpd services each request in
its own thread.
Therefore, TinyHttpd can service
several requests concurrently.
Over and above the limitations imposed by its simplicity,
TinyHttpd suffers from the limitations imposed by
the fickleness of filesystem access. It's important to remember that file pathnames
are still architecture dependent--as is the concept of a
filesystem to begin with. This example should work, as-is, on
UNIX and DOS-like systems, but
may require some customizations to account for differences on other
platforms. It's possible to write more elaborate code that uses the
environmental information provided by Java to tailor itself to the
local system. (Chapter 10 gives some hints about how.)
WARNING: The next example will serve files from your host without protection. Don't try this at work.
Now, without further ado, here's TinyHttpd:
import java.net.*;
import java.io.*;
import java.util.*;
public class TinyHttpd {
public static void main( String argv[] ) throws IOException {
ServerSocket ss = new ServerSocket( Integer.parseInt(argv[0]) );
while ( true )
new TinyHttpdConnection( ss.accept() ).start();
}
}
class TinyHttpdConnection extends Thread {
Socket client;
TinyHttpdConnection ( Socket client ) throws SocketException {
this.client = client;
setPriority( NORM_PRIORITY - 1 );
}
public void run() {
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream(), "8859_1") );
OutputStream out = client.getOutputStream();
PrintWriter pout = new PrintWriter(
new OutputStreamWriter(out, "8859_1"), true );
String request = in.readLine();
System.out.println( "Request: "+request );
StringTokenizer st = new StringTokenizer( request );
if ( (st.countTokens() >= 2) && st.nextToken().equals("GET") ) {
if ( (request = st.nextToken()).startsWith("/") )
request = request.substring( 1 );
if ( request.endsWith("/") || request.equals("") )
request = request + "index.html";
try {
FileInputStream fis = new FileInputStream ( request );
byte [] data = new byte [ fis.available() ];
fis.read( data );
out.write( data );
out.flush();
} catch ( FileNotFoundException e ) {
pout.println( "404 Object Not Found" ); }
} else
pout.println( "400 Bad Request" );
client.close();
} catch ( IOException e ) {
System.out.println( "I/O error " + e ); }
}
}
Compile TinyHttpd and place it in your class
path. Go to a directory with some interesting documents and start the daemon,
specifying an unused port number as an argument. For example:
% java TinyHttpd 1234
You should now be able to use your Web browser to retrieve files from your host. You'll have to specify the nonstandard port number in the URL. For example, if your hostname is foo.bar.com, and you started the server as above, you could reference a file as in:
http://foo.bar.com:1234/welcome.html
TinyHttpd looks for files relative to its current
directory, so the pathnames you provide should be relative to
that location. Retrieved some files? All righty then,
let's take a closer look.
TinyHttpd is comprised of two classes. The
public TinyHttpd class contains the
main() method of our standalone application. It
begins by creating a ServerSocket, attached to the
specified port. It then loops, waiting for client connections and
creating instances of the second class, a
TinyHttpdConnection thread, to service each
request. The while loop waits for the
ServerSocket accept() method to
return a new Socket for each client connection. The
Socket is passed as an argument to construct the
TinyHttpdConnection thread that handles it.
TinyHttpdConnection is a subclass of
Thread. It lives long enough to process one client
connection and then dies.
TinyHttpdConnection's constructor does two
things. After saving the Socket argument for its
caller, it adjusts its priority.
By lowering its priority to
NORM_PRIORITY-1 (just below the default priority),
we ensure that the threads servicing established connections
won't block TinyHttpd's main thread
from accepting new requests. (On a time-slicing system, this is less
important.)
After our object is constructed, its
start() method is invoked to bring the
run() method to life.
The body of TinyHttpdConnection's
run() method is where all the magic
happens. First, we fetch an OutputStream for talking
back to our client. The second line reads the GET
request from the InputStream into the variable
req. This request is a single newline-terminated
String that looks like the GET
request we described earlier.
For this we use a BufferedInputStream
wrapped around an InputStreamReader.
We'll say more about the InputStreamReader
in a moment.
We then parse the contents of req to extract
a filename. The next few lines are a brief exercise in string
manipulation. We create a StringTokenizer and make
sure there are at least two tokens. Using
nextToken(), we take the first token and make sure
it's the word GET. (If both conditions
aren't met, we have an error.) Then we take the next token
(which should be a filename), assign it to req, and
check whether it begins with "/". If so, we use
substring() to strip the first character, giving us
a filename relative to the current directory. If it doesn't
begin with "/", the filename is already relative to the
current directory. Finally, we check to see if the requested filename
looks like a directory name (i.e., ends in slash) or is empty. In
these cases, we append the familiar default filename
index.html.
Once we have the filename, we try to open the specified file
and load its contents into a large byte array. If all goes well, we write the data out to
the client on the OutputStream. If we can't parse the
request or the file doesn't exist, we wrap our
OutputStream with a PrintStream
to make it easier to send a textual message. Then we return an
appropriate HTTP error message. Finally, we close
the socket and return from run(), removing our
Thread.
In TinyHttpd, we explicitly created the
InputStreamReader for
our BufferedRead and the
OutputStreamWriter for our
PrintWriter. We do
this so that we can specify the character encoding to use when
converting from
text to bytes. If we didn't specify, we'd get the default character encoding
for the local system. For many purposes that's correct, but in this case we
are speaking a well-defined protocol. The RFC for HTTP specifies that
Web clients and servers should use the ISO 8859-1 character encoding, which is,
for our purposes, just ASCII. We specify this encoding explicitly
when we construct the InputStreamReader
and OutputStreamWriter.
The biggest problem with TinyHttpd is that there
are no restrictions on the files it can access. With a little
trickery, the daemon will happily send any file in your filesystem to
the client. It would be nice if we could restrict
TinyHttpd to files that are in the current
directory, or a subdirectory. To make the daemon safer, let's
add a security manager. We discussed the general framework for security
managers in Chapter 9. Normally, a security manager is
used to prevent Java code downloaded over the Net from doing anything
suspicious. However, a security manager will serve nicely to restrict
file access in a self-contained application.
Here's the code for the security manager class:
import java.io.*;
class TinyHttpdSecurityManager extends SecurityManager {
public void checkAccess(Thread g) { };
public void checkListen(int port) { };
public void checkLink(String lib) { };
public void checkPropertyAccess(String key) { };
public void checkAccept(String host, int port) { };
public void checkWrite(FileDescriptor fd) { };
public void checkRead(FileDescriptor fd) { };
public void checkRead( String s ) {
if ( new File(s).isAbsolute() || (s.indexOf("..") != -1) )
throw new SecurityException("Access to file : "+s+" denied.");
}
} The
heart of this security manager is the checkRead()
method. It checks two things: it makes sure that the pathname
we've been given isn't an absolute path, which could name any
file in the filesystem; and it makes sure the pathname doesn't
have a double dot (..) in it, which refers to the
parent of the current directory. With these two restrictions, we can
be sure (at least on a UNIX or
DOS-like filesystem) that we have restricted access
to only subdirectories of the current directory. If the pathname is
absolute or contains "..",
checkRead() throws a
SecurityException.
The other do-nothing method implementations--e.g.,
checkAccess()--allow the daemon to do its work
without interference from the security manager. If we don't
install a security manager, the application runs with no
restrictions. However, as soon as we install any security manager, we
inherit implementations of many "check" routines. The
default implementations won't let you do anything; they just
throw a security exception as soon as they are called. We have to open
holes so the daemon can do its own work; it still has to accept
connections, listen on sockets, create threads, read property lists,
etc. Therefore, we override the default checks with routines that
allow these things.
Now you're thinking, isn't that overly permissive? Not for
this application; after all, TinyHttpd never
tries to load foreign classes from the Net. The only code we are executing
is our own, and it's assumed we won't do anything dangerous. If
we were planning to execute untrusted code, the security manager would
have to be more careful about what to permit.
Now that we have a security manager, we must modify
TinyHttpd to use it. Two changes are necessary: we
must install the security manager and catch the security exceptions it
generates. To install the security manager, add the following code at
the beginning of TinyHttpd's
main() method:
System.setSecurityManager( new TinyHttpdSecurityManager() );
To catch the security exception, add the following
catch clause after
FileNotFoundException's catch
clause:
catch ( SecurityException e )
pout.println( "403 Forbidden" ); Now the daemon can't access anything that isn't within the current directory or a subdirectory. If it tries to, the security manager throws an exception and prevents access to the file. The daemon then returns a standard HTTP error message to the client.
TinyHttpd still has room for
improvement. First, it consumes a lot of memory by allocating a huge
array to read the entire contents of the file all at once. A more
realistic implementation would use a buffer and send large amounts of
data in several passes. TinyHttpd also fails to
deal with simple things like directories. It wouldn't be hard to
add a few lines of code to read a directory and generate
linked HTML listings like most Web servers do.
The Java sockets API is a simplified interface to the general socket mechanisms. In a C environment, where all of the gory details of the network are visible to you, a lot of complex and sometimes esoteric options can be set on sockets to govern the behavior of the underlying protocols. Java gives us access to a few of the important ones. We'll refer to them by their C names so that you can recognize them in other networking books.
The SO_TIMEOUT option sets a timer
on I/O methods of a socket that block so that you don't have to wait forever
if they don't complete successfully. This works for operations like
accept() on server sockets and
read() or
write() on all sockets. If the
timer expires before the operation would complete, an
InterruptedIOException is thrown. You can
catch the exception and
continue to use the socket normally. You set the timer by calling the
setSoTimeout() method of the
Socket class with the timeout period, in
milliseconds, as an int argument.
This works for regular Sockets and
ServerSockets (TCP) and
DatagramSockets (UDP), which we'll discuss
in the next section.
To find out the current timeout value, call
getSoTimeout().
This option turns off a feature of TCP called Nagle's algorithm,
which tries to prevent certain interactive applications from flooding
the network with very tiny packets. Turn this off if you have a fast network
and you want all packets sent as soon as possible.
The Socket
setTcpNoDelay() method takes a
boolean argument specifing whether the delay
is on or off.
To find out whether the TCP_NODELAY option is enabled, call
getTcpNoDelay(), which returns a
boolean.
This option controls what happens to any unsent data when you perform a
close() on an active socket connection.
Normally the system tries to deliver any network buffered data and close the connection gracefully. The setSoLinger() method of the
Socket class takes two arguments: a
boolean that enables or disables the
option, and an int that sets the "linger"
value, in milliseconds. If you set the linger value to 0, any unsent data is discarded, and the TCP connection
is aborted (terminated with a reset).
To find out the current linger value, call
getSoLinger().
Many networks are behind firewalls that prevent applications from opening direct socket connections to the outside network. Instead, they provide a service called SOCKS (named for sockets) that serves as a proxy server for socket connections, giving the administrators more control over what connections are allowed.
Java has built-in support for SOCKS. All you have to do is set some system properties in your application (in an applet, this should be already taken care of for you since you wouldn't have authority to set properties). Here's a list of the properties that configure Java to use a proxy server:
http.proxySetA boolean (true or
false) indicating whether to use the proxy
http.proxyHostThe proxy server name
http.proxyPortThe proxy port number
You can set these properties on the command line using the Java
interpreter's -D option or by calling the
System.setProperty() method. The command
below runs MyProgram using the proxy
server at foo.bar.com on port 1234:
% java -Dhttp.proxySet=true -Dhttp.proxyServer=foo.bar.com
-Dhttp.proxyPort=1234 MyProgram In Java 1.0.2 the names didn't have the http. prefix. Java version 1.1 and later checks for the new names and then the old names. If the firewall does not allow any outside socket connections, your applet or application may still be able to communicate with the outside world by using HTTP to send and receive data. See Chapter 12 for an example of how to perform an HTTP POST from an applet.
TinyHttpd used a Socket to
create a connection to the client using the TCP
protocol. In that example, TCP itself took care of
data integrity; we didn't have to worry about data arriving out
of order or incorrect. Now we'll take a walk on the wild
side. We'll build an applet that uses a
java.net.DatagramSocket, which uses the
UDP protocol. A datagram is sort of like a
"data telegram": it's a discrete chunk of data transmitted
in one packet. Unlike the previous example, where we could get a
convenient OutputStream from our
Socket and write the data as if writing to
a file, with a DatagramSocket we have to work one
datagram at a time. (Of course, the TCP protocol was
taking our OutputStream and slicing the data into
packets, but we didn't have to worry about those details.)
UDP doesn't guarantee that the data will get through. If the data do get through, it may not arrive in the right order; it's even possible for duplicate datagrams to arrive. Using UDP is something like cutting the pages out of the encyclopedia, putting them into separate envelopes, and mailing them to your friend. If your friend wants to read the encyclopedia, it's his or her job to put the pages in order. If some pages got lost in the mail, your friend has to send you a letter asking for replacements.
Obviously, you wouldn't use UDP to send a huge amount of data. But it's significantly more efficient than TCP, particularly if you don't care about the order in which messages arrive, or whether the data arrive at all. For example, in a database lookup, the client can send a query; the server's response itself constitutes an acknowledgment. If the response doesn't arrive within a certain time, the client can send another query. It shouldn't be hard for the client to match responses to its original queries. Some important applications that use UDP are the Domain Name System (DNS) and Sun's Network Filesystem (NFS).
In this section we'll build a simple applet,
HeartBeat, that sends a datagram to its server each
time it's started and stopped. We'll also build a simple standalone server application,
Pulse, that receives these datagrams and prints
them. By tracking the output, you could have a crude measure of who is
currently looking at your Web page at any given time. This is an ideal
application for UDP: we don't want the
overhead of a TCP socket, and if datagrams get
lost, it's no big deal.
First, the HeartBeat applet:
import java.net.*;
import java.io.*;
public class HeartBeat extends java.applet.Applet {
String myHost;
int myPort;
public void init() {
myHost = getCodeBase().getHost();
myPort = Integer.parseInt( getParameter("myPort") );
}
private void sendMessage( String message ) {
try {
byte [] data = message.getBytes();
InetAddress addr = InetAddress.getByName( myHost );
DatagramPacket pack =
new DatagramPacket(data, data.length, addr, myPort );
DatagramSocket ds = new DatagramSocket();
ds.send( pack );
ds.close();
} catch ( IOException e ) {
System.out.println( e ); // Error creating socket
}
}
public void start() {
sendMessage("Arrived");
}
public void stop() {
sendMessage("Departed");
}
}Compile the applet and include it in an HTML
document with an <APPLET> tag:
<applet height=10 width=10 code=HeartBeat>
<param name="myPort" value="1234">
</applet> The myPort parameter should specify the port
number on which our server application listens for data.
Next, the server-side application, Pulse:
import java.net.*;
import java.io.*;
public class Pulse {
public static void main( String [] argv ) throws IOException {
DatagramSocket s = new DatagramSocket( Integer.parseInt(argv[0]) );
while ( true ) {
DatagramPacket packet = new DatagramPacket(new byte [1024], 1024);
s.receive( packet );
String message = new String( packet.getData() );
System.out.println( "Heartbeat from: " +
packet.getAddress().getHostName() + " - " + message );
}
}
}Compile Pulse and run it on your Web server,
specifying a port number as an argument:
% java Pulse 1234
The port number should be the same as the one you used in the
myPort parameter of the
<APPLET> tag for
HeartBeat.
Now, pull up the Web page in your browser. You won't see
anything there (a better application might do something visual as
well), but you should get a blip from the Pulse
application. Leave the page and return to it a few times. Each time
the applet is started or stopped, it sends a message:
Heartbeat from: foo.bar.com - Arrived Heartbeat from: foo.bar.com - Departed Heartbeat from: foo.bar.com - Arrived Heartbeat from: foo.bar.com - Departed ...
Cool, eh? Just remember the datagrams are not guaranteed to arrive (although it's unlikely you'll see them fail), and it's possible that you could miss an arrival or a departure. Now let's look at the code.
HeartBeat overrides the init(),
start(), and stop() methods of
the Applet class, and implements one private method
of its own, sendMessage(), that sends a
datagram. HeartBeat begins its life in
init(), where it determines the destination for its
messages.
It uses the
Applet getCodeBase() and
getHost() methods to find the name of its
originating host and fetches the correct port number from the
myPort parameter of the HTML
tag. After init() has finished, the
start() and stop() methods are
called whenever the applet is started or stopped. These methods merely
call sendMessage() with the appropriate message.
sendMessage() is responsible for sending a
String message to the server as a datagram. It
takes the text as an argument, constructs a datagram packet containing
the message, and then sends the datagram. All of the datagram
information is packed
into a java.net.DatagramPacket object, including the destination and port number. The
DatagramPacket is like an addressed envelope,
stuffed with our bytes. After the DatagramPacket is
created, sendMessage() simply has to open a
DatagramSocket and send it.
The first five lines of sendMessage() build the
DatagramPacket:
try {
byte [] data = message.getBytes();
InetAddress addr = InetAddress.getByName( myHost );
DatagramPacket pack =
new DatagramPacket(data, data.length, addr, myPort );
First, the contents of message are placed into an
array of bytes called data. Next a
java.net.InetAddress object is created from the
name myHost. An InetAddress
simply holds the network address information for a host in a special
format. We get an InetAddress object for our host
by using the static getByName() method of the
InetAddress class. (We can't construct an
InetAddress object directly.) Finally, we call the
DatagramPacket constructor with four arguments: the
byte array containing our data, the length of the data, the
destination address object, and the port number.
The remaining lines construct a default client
DatagramSocket and call its
send() method to transmit the
DatagramPacket; after sending the datagram, we
close the socket:
DatagramSocket ds = new DatagramSocket(); ds.send( pack ); ds.close();
Two operations throw a type of IOException: the
InetAddress.getByName() lookup and the
DatagramSocket
send(). InetAddress.getByName()
can throw an UnknownHostException, which is a type
of IOException that indicates that the host name
can't be resolved. If send() throws an
IOException, it implies a serious client side
problem in talking to the network. We need to catch these exceptions;
our catch block simply prints a message telling us
that something went wrong. If we get one of these exceptions, we can
assume the datagram never arrived. However, we can't assume the
converse. Even if we don't get an exception, we still
don't know that the host is actually accessible or that the data
actually arrived; with a DatagramSocket, we never
find out.
The Pulse server corresponds to the
HeartBeat applet. First, it creates a
DatagramSocket to listen on our prearranged
port. This time, we specify a port number in the constructor; we get
the port number from the command line as a string
(argv[0]) and convert it to an integer with
Integer.parseInt(). Note the difference between
this call to the constructor and the call in
HeartBeat. In the server, we need to listen for
incoming datagrams on a prearranged port, so we need to specify the
port when creating the DatagramSocket. In the
client, we only need to send datagrams, so we don't have to
specify the port in advance; we build the port number into the
DatagramPacket itself.
Second, Pulse creates an empty
DatagramPacket of a fixed size to receive an
incoming datagram. This alternative constructor for
DatagramPacket takes a byte array and a length as
arguments. As much data as possible is stored in the byte array when
it's received. (A practical limit on the size of a
UDP datagram is 8K.) Finally,
Pulse calls the
DatagramSocket's receive()
method to wait for a packet to arrive. When a packet arrives, its
contents are printed.
As you can see, working with DatagramSocket
is slightly more tedious than working with
Sockets. With datagrams, it's harder to spackle
over the messiness of the socket interface. However, the Java
API rather slavishly follows the
UNIX interface, and that doesn't help. I
don't see any reason why we have to prepare a datagram to hand
to receive() (at least for the current
functionality); receive() ought to
create an appropriate object on its own and hand it to us, saving us
the effort of building the datagram in advance and unpacking the data
from it afterwards. It's easy to imagine other conveniences; perhaps
we'll have them in a future release.
Earlier in this chapter we showed a hypothetical conversation in which a client and server exchanged some primitive data and a serialized Java object. Passing an object between two programs may not have seemed like a big deal at the time, but in the context of Java as a portable byte-code language, it has profound implications. In this section we'll show how a protocol can be built using serialized Java objects.
Before we move on, it's worth considering network protocols. Most programmers would consider working with sockets to be "low level" and unfriendly. Even though Java makes sockets much much easier to use than many other languages, sockets still only provide an unstructured flow of bytes between their endpoints. If you want to do serious communications using sockets, the first thing you have to do is come up with a protocol that defines the data you'll be sending and receiving. The most complex part of that protocol usually involves how to marshall (package) your data for transfer over the Net and unpack it on the other side.
As we've seen, Java's DataInputStream and
DataOuputStream classes solve
this problem for simple data types. We can read and write numbers,
Strings,
and Java primitives in a recognizable format that can be understood on any
other Java platform.
But to do real work we need to be able to put simple types together
into larger structures.
Java object serialization
solves this problem elegantly, by allowing us to send our data
just as we use it, as the state of Java objects. Serialization can
pack up entire graphs of interconnected objects
and put them back together at a later time, possibly in another
context.
In the following example, a client will send a serialized object to the server, and the server will respond in kind. The client object represents a request, and the server object represents a response. The conversation ends when the client closes the connection. It's hard to imagine a simpler protocol. All the hairy details are taken care of by object serialization, so we can keep them out of our design.
To start we'll define a class, Request, to
serve as a base class for the
various kinds of requests we make to the server. Using a common base class is
a convenient way to identify the object as a type of request. In a real
application, we might also use it to hold basic information like
client names and passwords, time stamps, serial numbers, etc. In our
example, Request can be an empty class
that exists so others can extend it:
public class Request implements java.io.Serializable { }
Request implements
Serializable, so all of its subclasses
will be
serializable by default. Next we'll create some specific kinds
of Requests. The first,
DateRequest, is also a trivial class.
We'll use
it to ask the server to send us a
java.util.Date object as a response:public class DateRequest extends Request { }Next, we'll create a generic WorkRequest object.
The client sends a WorkRequest to get the
server to perform work
for it. The server calls the request object's
execute() method and returns
the resulting object as a response:
public class WorkRequest extends Request {
public Object execute() { return null; }
}
For our application, we'll subclass
WorkRequest to create
MyCalculation, which
adds code that performs a specific calculation; in this case, we'll just
square a number:
public class MyCalculation extends WorkRequest {
int n;
public MyCalculation( int n ) {
this.n = n;
}
public Object execute() {
return new Integer( n * n );
}
}
As far as data is concerned, MyCalculation
really doesn't do much; it only transports an
integer value for us. Keep
in mind that a request object could hold lots of data,
including references to many other objects in complex structures like
arrays or linked lists.Now that we have our protocol, we need the server. The
Server class below
looks a lot like the TinyHttpd server that
we developed earlier in this chapter:
import java.net.*;
import java.io.*;
public class Server {
public static void main( String argv[] ) throws IOException {
ServerSocket ss = new ServerSocket( Integer.parseInt(argv[0]) );
while ( true )
new ServerConnection( ss.accept() ).start();
}
}
class ServerConnection extends Thread {
Socket client;
ServerConnection ( Socket client ) throws SocketException {
this.client = client;
setPriority( NORM_PRIORITY - 1 );
}
public void run() {
try {
ObjectInputStream in =
new ObjectInputStream( client.getInputStream() );
ObjectOutputStream out =
new ObjectOutputStream( client.getOutputStream() );
while ( true ) {
out.writeObject( processRequest( in.readObject() ) );
out.flush();
}
} catch ( EOFException e3 ) { // Normal EOF
try {
client.close();
} catch ( IOException e ) { }
} catch ( IOException e ) {
System.out.println( "I/O error " + e ); // I/O error
} catch ( ClassNotFoundException e2 ) {
System.out.println( e2 ); // Unknown type of request object
}
}
private Object processRequest( Object request ) {
if ( request instanceof DateRequest )
return new java.util.Date();
else if ( request instanceof WorkRequest )
return ((WorkRequest)request).execute();
else
return null;
}
}
The Server services each request in a
separate thread. For
each connection, the run() method creates
an ObjectInputStream and an
ObjectOutputStream, which the server uses
to receive the request
and send the response. The
processRequest() method decides
what the request means and comes up with the response.
To figure out what kind of request we have, we use the
instanceof operator to look at the
object's type. Finally, we get to our Client, which is
even simpler:
import java.net.*;
import java.io.*;
public class Client {
public static void main( String argv[] ) {
try {
Socket server =
new Socket( argv[0], Integer.parseInt(argv[1]) );
ObjectOutputStream out =
new ObjectOutputStream( server.getOutputStream() );
ObjectInputStream in =
new ObjectInputStream( server.getInputStream() );
out.writeObject( new DateRequest() );
out.flush();
System.out.println( in.readObject() );
out.writeObject( new MyCalculation( 2 ) );
out.flush();
System.out.println( in.readObject() );
server.close();
} catch ( IOException e ) {
System.out.println( "I/O error " + e ); // I/O error
} catch ( ClassNotFoundException e2 ) {
System.out.println( e2 ); // Unknown type of response object
}
}
}
Just like the server, Client creates the pair of object streams.
It sends a DateRequest and prints the
response; it then sends
a MyCalculation object and prints the
response. Finally, it closes the
connection. On both the client and the server, we call the
flush() method after each call to
writeObject(). This method forces the
system to send any buffered data, and is important because it ensures
that the other side sees the entire request before we wait for a
response.
When the client closes the connection, our server catches the
EOFException
that is thrown and ends the session. Alternatively, our
client could write a special object, perhaps
null, to end the session; the server could
watch for this item in its main loop.
The order in which we construct the object streams is important. We
create the output streams first because the
constructor of an ObjectInputStream tries
to read a header from the stream
to make sure that the InputStream really
is an object stream. If
we tried to create both of our input streams first, we would deadlock waiting
for the other side to write the headers.
Finally, we can run the example. Run the
Server, giving it a port number
as an argument:
% java Server 1234Then run the
Client, telling it the
server's hostname and port number:
% java Client flatland 1234
You should see the following result:
Fri Jul 11 14:25:25 PDT 1997 4
All right, the result isn't that impressive, but it's easy to imagine more substantial applications. Imagine that you needed to perform some complex computation on many large data sets. This might take days on your PC, but you just happen to have a supercomputer in the back room. Using a protocol like the one we've just developed, it's simple to transfer the data to the supercomputer, perform the computation, and return the results.
There is one catch in this scenario: both the
client and server need access to the necessary classes. That is,
all of the Request classes--including MyCalculation, which is really the property of the Client--have to be in
the class path of both the client
and the server.
Given that Java is portable, can't we just ship the byte-code
along with the serialized object data? After all, we transport Java classes
between Java applications all the time when we run applets. We can, but with a bit more work. We could create this solution on our own,
using a network classloader to load the classes for us. But we don't
have to: Java's RMI facility automates that
for us. The ability to send serialized data and classes over the
network makes Java a powerful tool for developing advanced
applications.
The most fundamental means of interobject communication in Java is method invocation. Mechanisms like the Java event model are built on simple method invocations between objects that share a virtual machine. Therefore, when we want to communicate between virtual machines on different hosts, it's natural to want a mechanism with similar capabilities and semantics. Java's Remote Method Invocation mechanism does just that. It lets us get a reference to an object on a remote host and use it as if it were in our own virtual machine. RMI lets us invoke methods on remote objects, passing real objects as arguments and getting real objects as returned values.
Remote invocation is nothing new. For many years C programmers have used remote procedure calls (RPC) to execute a C function on a remote host and return the results. The primary difference between RPC and RMI is that RPC, being an offshoot of the C language, is primarily concerned with data structures. It's relatively easy to pack up data and ship it around, but for Java, that's not enough. In Java we don't work with simple data structures; we work with objects, which contain both data and methods for working on the data. Not only do we have to be able to ship the state of an object over the wire (the data), but the recipient has to be able to interact with the object after receiving it.
It should be no surprise that RMI uses object serialization, which allows us to send graphs of objects (objects and all of the connected objects that they reference). When necessary, RMI uses dynamic class loading and the security manager to transport Java classes safely. The real breakthrough of RMI is that it's possible to ship both code and data around the Net.
Before an object can be used with RMI, it must be serializable. But that's not sufficient. Remote objects in RMI are real distributed objects. As their name suggests, a remote object can refer to an object on a different machine; it can also refer to an object on the local host. The term remote means that the object is used through a special kind of object reference that can be passed over the network. Like normal Java objects, remote objects are passed by reference. Regardless of where the reference is used, the method invocation occurs at the original object, which still lives on its original host. If a server returns a reference to a remote object to you, you can call the object's methods; the actual method invocations will happen on the remote object's server. If a client creates a remote object and passes a reference to a server, the server can use the reference to invoke methods on the original object on the client side.
Non-remote objects are simpler. They are just normal serializable objects. The catch is that when you pass a non-remote object over the network it is simply copied. So references to the object on one host are not the same as those on the remote host. This is acceptable for many simple kinds of objects, especially objects that cannot be modified.
No, we're not talking about a gruesome horror movie. Stubs and skeletons are used in the implementation of remote objects. When you invoke a method on a remote object (which could be on a different host), you are actually calling some local code that serves as a proxy for that object. This is the stub. (It is called a stub because it is something like a truncated placeholder for the object.) The skeleton is another proxy that lives with the real object on its original host. It receives remote method invocations from the stub and passes them to the object.
You never have to work with stubs or skeletons directly; they are hidden from you (in the closet). Stubs and skeletons for your remote objects are created by running the rmic (RMI compiler) utility. After compiling your Java source files normally, you run rmic.
So far we've been referring to remote objects as objects (and they are,
of course). But to be more specific, remote objects are objects that
implement a special remote interface that specifies which of the
object's methods can be invoked remotely. The remote interface must extend
the java.rmi.Remote interface. Your
remote object must implement its remote
interface; so does the stub object that is automatically generated for
you. In the rest of your code, you should refer to the remote object
using its interface--not the object's actual class. Because both the
real object and stub implement the
remote interface, they are equivalent as far as we are concerned; we
never have to worry about whether we have a reference to a stub or an
actual implementation of the object locally. This "type equivalence"
means that we can use normal language features like casting with remote
objects.
All methods in the remote interface must declare that they
can throw the exception
java.rmi.RemoteException. This exception
(actually, one of many subclasses to
RemoteException) is thrown when any kind
of networking error happens: for example, the server could crash, the
network could fail, or you could be requesting an object that for some
reason isn't available.
Here's a simple example of the remote interface that defines the
behavior of MyRemoteObject; we'll give it
two methods that can be invoked remotely, both of which return some
kind of Widget object:
public interface MyRemoteObject
extends java.rmi.Remote {
public Widget doSomething() throws java.rmi.RemoteException;
public Widget doSomethingElse() throws java.rmi.RemoteException;
}
The actual implementation of a remote object (not the interface we
discussed previously) must extend
java.rmi.server.UnicastRemoteObject. This
is the RMI equivalent to the familiar
Object class. It provides implementations
of equals(),
hashcode(), and
toString() that make sense for remote
objects. It also "exports" the object by preparing the Java run-time system to accept
network connections for this object. It's possible to do this work yourself, but it
isn't necessary.
Here's a remote object class that matches the
MyRemoteObject interface; we haven't
supplied implementation for the two methods or the constructor:
public class RemoteObjectImpl
implements MyRemoteObject
extends java.rmi.UnicastRemoteObject {
public RemoteObjectImpl() throws java.rmi.RemoteException {...}
public Widget doSomething() throws java.rmi.RemoteException {...}
public Widget doSomethingElse() throws java.rmi.RemoteException {...}
// other non-public methods
...
}This class can have as many additional methods as it needs;
presumably, most of them will be private,
but that isn't strictly necessary. We have to supply a constructor
explicitly, even if the constructor does nothing, because the
constructor (like any method) can throw a
RemoteException; we therefore can't use
the default constructor.
The name UnicastRemoteObject begs the
question, "what other kinds of remote objects are there?" Right now,
none. It's possible that JavaSoft will develop remote objects using
other protocols or multicast techniques in the future.
The registry is the RMI phone book. You use the registry to look up a reference to a registered remote object on another host. We've already described how remote references can be passed back and forth by remote method calls. But the registry is needed to bootstrap the process; the client needs some way of looking up some initial object.
The registry is implemented by a class called
Naming and an application called
rmiregistry, which must be running before you start a Java
program that uses the registry.
To use the registry, create an instance of a remote object and
have it bind itself to a particular name in the registry. (Remote
objects that bind themselves to the registry usually provide a
main() method for this purpose.) The name
can be anything you choose; it takes the form of a slash (/) separated
path. When a client object wants to find your object, it constructs a
special URL with the rmi: protocol, the
hostname, and the object name. On the client, the RMI
Naming class then talks to the registry
and returns the remote object reference.
Which objects need to register themselves with the registry? Certainly, any object that the client has no other way of finding. A call to a remote method can return another remote object without using the registry. Likewise, a call to a remote method can have another remote object as its argument, without requiring the registry. You could design your system so that only one object registers itself, and then serves as a factory for any other remote objects you need. In other words, it wouldn't be hard to build a simple object request "bouncer" (I won't say "broker") that returns references to various objects. Why avoid using the registry? The current RMI registry is not very sophisticated, and lookups tend to be slow. It is not intended to be a general purpose directory service but simply to bootstrap RMI communications. It wouldn't be surprising if JavaSoft releases a much improved registry in the future, but that's not the one we have now.
The first thing we'll implement using RMI is a duplication of the
simple serialized object protocol from the previous section. We'll
make a remote RMI object called Server on
which we can invoke methods to
get a Date object or execute a
WorkRequest object.
First, we'll define our Remote interface:
import java.rmi.*;
import java.util.*;
public interface Server extends java.rmi.Remote {
Date getDate() throws java.rmi.RemoteException;
Object execute( WorkRequest work ) throws java.rmi.RemoteException;
}
The Server interface extends the
java.rmi.Remote interface, which
identifies objects that implement it as remote objects. We supply two
methods that take the place of our old protocol:
getDate() and
execute().
Next, we'll implement this interface in a class called
MyServer that holds
the bodies of these methods. (In this example, we're not using the
convention of adding Impl to the interface
name to create the actual object name. Using this convention, the
name of the server would be ServerImpl.)
public class MyServer
extends java.rmi.server.UnicastRemoteObject implements Server {
public MyServer() throws RemoteException { }
// Implement the Server interface
public Date getDate() throws RemoteException {
return new Date();
}
public Object execute( WorkRequest work ) throws RemoteException {
return work.execute();
}
public static void main(String args[]) {
System.setSecurityManager(new RMISecurityManager());
try {
Server server = new MyServer();
Naming.rebind("NiftyServer", server);
} catch (java.io.IOException e) {
// Problem registering server
}
}
}
MyServer extends
java.rmi.UnicastRemoteObject, so when we
create an instance
of MyServer it will automatically be
exported and start listening to the
network. We start by providing a
constructor that throws RemoteException.
This exception accommodates errors
that might occur in exporting an instance. We can't use the default
constructor provided by the compiler, because the automatically
generated constructor won't throw the exception. Next,
MyServer implements the
methods of the remote Server interface.
These methods are straightforward.
The last method in this class is main().
This method lets the object set itself up as a server.
main()
starts by installing a special security manager,
RMISecurityManager. This is a special
security manager that watches any stub classes loaded over the network
by RMI. It prevents someone from handing you a misbehaving stub, in
addition to performing the other functions of a security manager.
main()
creates an instance of the MyServer
object and then calls the static method
Naming.rebind() to register the object
with the registry. The arguments to
rebind() are the name of the remote object
in the registry (NiftyServer), which clients will use to look up the
object, and reference to the server object itself. We could have
called bind() instead, but
rebind() is less prone to problems: if
there's already a NiftyServer registered,
rebind() replaces it.
We wouldn't need
the main() method or this
Naming business if we weren't expecting
clients to use the registry to find the server. That is, we could
omit main() and still use this object as a
remote object. We would be limited to passing the object in method
invocations or returning it from method invocations--but in many
situations (not ours) those aren't big limitations.
Now we need our client:
public class MyClient {
public static void main(String [] args) throws RemoteException {
System.setSecurityManager(new RMISecurityManager());
new MyClient( args[0] );
}
public MyClient(String host) {
try {
Server server = (Server)
Naming.lookup("rmi://"+host+"/NiftyServer");
System.out.println( server.getDate() );
System.out.println( server.execute( new MyCalculation(2) ) );
} catch (java.io.IOException e) {
// I/O Error or bad URL
} catch (NotBoundException e) {
// NiftyServer isn't registered
}
}
}When we run MyClient, we pass it the
hostname of the server
on which the registry is running. The
main() method installs the
RMISecurityManager and then creates an
instance of the MyClient object, passing
the hostname from the command line as an argument to the constructor.
The constructor for MyClient
uses the hostname to construct a
URL for the object. The URL will look something like this:
rmi://hostname/NiftyServer, where
NiftyServer is the name under which
we registered our Server. We pass the URL
to the static Naming.lookup()
method. If all goes well, we get back a reference to a
Server! Of course, the registry has no
idea what kind of object it will return;
lookup() therefore returns an
Object, which we cast to
Server.
Compile all of the code. Then run RMI compiler to make the stub
and skeleton files for MyServer:
% rmic MyServerLet's run the code. For the first pass, we'll assume that you have all of the class files, including the stubs and skeletons generated by rmic, available in the class path on both the client and server machines. (You can run this example on a single host to test it if you want.) Make sure your class path is correct and then start the registry; then start the server:
% rmiregistry & % java MyServer
On a Windows system, run rmiregistry in another window by preceding it with the start command. Finally, on the client machine, run
MyClient, passing the hostname of
the server:
% java MyClient myhostThe client should print the date and the number four, which the server graciously calculated.
Before running the example, we told you to distribute all the class files to both the client and server machines. However, RMI was designed to ship classes, in addition to data, around the network; you shouldn't have to distribute all the classes in advance. Let's go a step further, and have RMI load classes for us, as needed.
First, we need to tell RMI where to find any other classes it needs. We can use the system property
java.rmi.server.codebase to specify a URL
on an HTTP server when we run our client or server. This URL specifies the base directory in which RMI
will begin its search for classes. When RMI sends a serialized object
(i.e., an object's data) to some client, it also sends this URL. If
the recipient needs the class file in addition to the data, it fetches
the file via HTTP. To be more precise: if the object needed is a
remote object, the recipient fetches the desired class's stub, which
was created by rmic. Remember that stubs are stand-ins for
the objects themselves; their job is to talk to the object, which
remains on the server. If the object needed doesn't implement the
Remote interface, the recipient fetches
the object's class file itself, and uses the object locally.
Therefore, we don't have to distribute class files; we can let clients
download them as necessary. In Figure 11.3, we see
MyClient going to the registry to get a
reference to the Server object. Then
MyClient dynamically downloads the stub
class for MyServer from the HTTP daemon
running on the server host.

We can now split our class files between the server and client
machines. For example, we could withhold the
MyCalculation class from the server, since
it really belongs to the client. Instead, we can make the
MyCalculation class available via an HTTP
daemon on some machine (probably our client's) and specify the URL
when we run MyClient:
% java -Djava.rmi.server.codebase='http://myserver/foo/' MyClientIn this case we would expect that
MyCalculation would be accessible at the
URL http://myserver/foo/MyCalculation.class.So far, we haven't done anything that we couldn't have done with the
simple object protocol. We only used one remote object,
MyServer, and we got its reference from
the RMI registry. Now we'll extend our example to pass some remote
references between the client and server.
We'll add two methods to our remote Server
interface:
public interface Server extends java.rmi.Remote {
...
StringEnumeration getList() throws java.rmi.RemoteException;
void asyncExecute( WorkRequest work, WorkListener listener )
throws java.rmi.RemoteException;
}
getList() retrieves a new kind of object
from the server: a
StringEnumeration. The
StringEnumeration is a simple list of
strings, with some methods for accessing the strings in order. We
will make it a remote object so that implementations of
StringEnumeration can stay on the server.Next we'll spice up our work request feature by adding an
asyncExecute()
method. asyncExecute() lets us hand off a
WorkRequest object as before,
but it does the calulation on its own time. The return type for
asyncExecute() is
void, because it doesn't actually return a
value; we get the result later.
With the request, our client passes a reference to a
WorkListener
object that is to be notified when the
WorkRequest is done. We'll have our
client implement WorkListener itself.
Because this is to be a remote object, our interface must extend
Remote, and its methods must throw
RemoteExceptions:
public interface StringEnumeration extends Remote {
public boolean hasMoreItems() throws RemoteException;
public String nextItem() throws RemoteException;
}
Next, we provide a simple implementation of
StringEnumeration, called
StringEnumerator:
public class StringEnumerator
extends java.rmi.server.UnicastRemoteObject implements StringEnumeration {
String [] list;
int index = 0;
public StringEnumerator( String [] list ) throws RemoteException {
this.list = list;
}
public boolean hasMoreItems() throws RemoteException {
return index < list.length;
}
public String nextItem() throws RemoteException {
return list[index++];
}
}
The StringEnumerator extends
UnicastRemoteObject.
Its methods are simple: it can give you the next string in the list,
and it can tell you whether there are any strings that you haven't
seen yet.
Next, we'll define the WorkListener remote
interface. This is the interface that defines how an object should
listen for a completed WorkRequest. It
has one method, workCompleted(), which the
server that is executing a WorkRequest
calls when the job is done:
public interface WorkListener extends Remote {
public void workCompleted( WorkRequest request, Object result )
throws RemoteException;
}
Next, let's add the new features to
MyServer. We need to add implementations
of the getList() and
asyncExecute() methods, which we just
added to the Server interface:
public class MyServer
extends java.rmi.server.UnicastRemoteObject implements Server {
...
public StringEnumeration getList() throws RemoteException {
return new StringEnumerator(
new String [] { "Foo", "Bar", "Gee" } );
}
public void asyncExecute( WorkRequest request , WorkListener listener )
throws java.rmi.RemoteException {
Object result = request.execute();
listener.workCompleted( request, result );
}
}
getList() just returns a
StringEnumerator with some
stuff in it. asyncExecute() calls a
WorkRequest's
execute() method and
notifies the listener when it's done. (Our implementation of
asyncExecute() is a little cheesy. If we
were forming a more complex calculation we would want to start a
thread to do the calculation, and return immediately from
asyncExecute(), so the client won't block.
The thread would
call workCompleted() at a later time, when
the computation was done. In this simple example, it would take
longer to start the thread than to perform the calculation.)
We have to modify MyClient to
implement the remote WorkListener
interface. This turns MyClient into a remote
object, so we must make it a
UnicastRemoteObject. We also add the
workCompleted() method that the
WorkListener interface requires:
public class MyClient extends java.rmi.server.UnicastRemoteObject
implements WorkListener {
...
public void workCompleted( WorkRequest request, Object result )
throws RemoteException {
System.out.println("Async work result = " + result);
}
}
Finally, we want MyClient to exercise the
new features. Add these lines after the calls to
getDate() and
execute():
// MyClient constructor
...
StringEnumeration se = server.getList();
while ( se.hasMoreItems() )
System.out.println( se.nextItem() );
server.asyncExecute( new MyCalculation(100), this );
We use getList() to get the enumeration
from the server, then loop,
printing the strings.
We also call asyncExecute() to perform
another calculation; this time,
we square the number 100.
The second argument to asyncExecute() is the WorkListener to notify
when the data is ready; we pass a reference to ourself
(this).
Now all we have to do is compile everything and run rmic to make the stubs for all our remote objects:
rmic MyClient MyServer StringEnumeratorRestart the RMI registry and
MyServer on
your server, and run the
client somewhere. You should get the following:
Fri Jul 11 23:57:19 PDT 1997 4 Foo Bar Gee Async work result = 10000
Java supports one important alternative to RMI, called CORBA (Common Object Request Broker Architecture). We won't say much about CORBA, but you should know it exists. CORBA is a standard developed by the Object Management Group (OMG), of which Sun Microsystems is one of the founding members. Its major advantage is that it works cross language: a Java program can use CORBA to talk to objects written in other languages, like C or C++. This is a considerable advantage if you want to build a Java front end for an older program that you can't afford to reimplement. CORBA also provides some other services that aren't yet available in Java. CORBA's major disadvantage is that it's complex. JavaSoft has announced that they will be making efforts to integrate RMI and CORBA, but it's too early to see where these efforts will lead.
© 1999, O'Reilly & Associates, Inc.