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 bytecode language, it has big implications. In this section, we 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 tedious and complex. Even though Java makes sockets much easier to use than many other languages, sockets still provide only 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 are sending and receiving. The most complex part of that protocol usually involves how to marshal (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, String
s, and Java primitives in a standard
format that can be understood on any other Java platform. To do real work,
however, 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 in the state in which we will use it—as Java
objects. Serialization can even pack up entire graphs of interconnected
objects and put them back together at a later time in another Java
VM.
In the following example, a client sends a serialized object to the server, and the server responds in kind. The object sent by the client represents a request and the object returned by the server represents the 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, which allows us to work with standard Java objects as we are used to doing.
To start, we 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,
such as client names and passwords, timestamps, serial numbers, and
so on. In our example, Request
can be an empty class that exists so
that others can extend it:
//file: Request.java
public
class
Request
implements
java
.
io
.
Serializable
{}
Request
implements Serializable
, so all its subclasses are
serializable by default. Next, we create some specific kinds of Request
s. The first, DateRequest
, is also a trivial class. We use
it to ask the server to send us a java.util.Date
object as a response:
//file: DateRequest.java
public
class
DateRequest
extends
Request
{}
Next, we create a generic WorkRequest
object. The client sends a
WorkRequest
to get the server to
perform some computation. The server calls the WorkRequest
object’s execute()
method to do the work on the server
side and then returns the resulting object as a response:
//file: WorkRequest.java
public
abstract
class
WorkRequest
extends
Request
{
public
abstract
Object
execute
();
}
For our application, we subclass WorkRequest
to create MyCalculation
, which adds code that performs a
specific calculation; in this case, we just square a number:
//file: MyCalculation.java
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 content is concerned, MyCalculation
really doesn’t do much; it only
really transports an integer value for us. But keep in mind that a
request object could hold lots of data, including references to many
other objects in complex structures, such as arrays or linked lists. The
only requirement is that all the objects to be sent must be serializable
or must be able to be discarded by marking them as transient (see Chapter 12). Note that MyCalculation
also contains behavior—the
execute()
operation. While Java
object serialization sends only the data content of a class, in our
discussion of RMI later in this chapter, we’ll see how Java’s ability to
dynamically download bytecode for classes can make both the data content
and behavior portable over the network.
It’s also important to note that even without dynamically loading classes over the network (which is uncommon in practice), this design pattern, sometimes called the command pattern, is an important one. Using polymorphism to hide behavior details of tasks from the server allows the application to be easily extended. Polymorphism and Java object serialization are a powerful combination.
Now that we have our protocol, we need the server. The following
Server
class looks a lot like the
TinyHttpd
server we developed earlier
in this chapter:
//file: Server.java
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
();
}
}
// end of class Server
class
ServerConnection
extends
Thread
{
Socket
client
;
ServerConnection
(
Socket
client
)
throws
SocketException
{
this
.
client
=
client
;
}
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
handles 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:
//file: Client.java
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, ensuring 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 run the example, giving it a port number as an argument:
%
java
Server
1234
Then we run the Client
, telling
it the server’s hostname and port number:
%
java
Client
flatland
1234
The result should look something like this:
Sun
Mar
5
14
:
25
:
25
PDT
2006
4
All right, the result isn’t that impressive, but it’s easy to think of more substantial applications. Imagine that you need to perform a complex computation on many large datasets. Using serialized objects makes maintenance of the data objects natural and sending them over the wire trivial. There is no need to deal with byte-level protocols at all.
As we mentioned earlier, there is one catch in this scenario:
both the client and server need access to the necessary classes. That
is, all the Request
classes—including MyCalculation
,
which is really the property of the Client
—must be deployed in the classpath on
both the client and the server machines. In the next section, we’ll
see that it’s possible to send the Java bytecode along with serialized
objects to allow completely new kinds of objects to be transported
dynamically over the network. We could create this solution on our
own, adding to the earlier example using a network class loader to
load the classes for us. But we don’t have to: Java’s RMI facility
handles that for us. The ability to send both serialized data and
class definitions over the network is not always needed, but it makes
Java a powerful tool for prototyping and developing advanced
distributed applications.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.