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.