|
Articles Index
Qusay H. Mahmoud
December 2001
Distributed object-based applications can be easily developed
using Java Remote Method Invocation (RMI). The simplicity of RMI,
however, comes at the expense of network communication overhead.
Low-level sockets can be used to develop client/server systems,
but since most Java I/O classes are not object friendly, how can
you transport full-blown objects over sockets? Object serialization
is the mechanism that allows you to read/write full-blown objects to
byte streams.
Combining low-level sockets and object serialization gives you a
powerful, efficient alternative to RMI that enables you to transport
objects over sockets and overcome the overhead incurred in using RMI.
This article:
- Gives you a brief overview of object serialization
- Shows you how to work with object serialization
- Illustrates how to work with existing objects and custom objects
- Shows you how to transport objects over sockets
- Provides examples of multi-threaded servers
- Provides an object-based sample implementation of the daytime protocol
Finally, a brief comparison between RMI and sockets with object
serialization is presented.
Overview of Object Serialization
Object serialization is a mechanism that is useful in any program
that wants to save the state of objects to a file and later read
those objects to reconstruct the state of the program, or to send
an object over the network using sockets. Serializing a class can
be easily done simply by having the class implement the
java.io.Serializable interface. This interface is a
marker interface. In other words, it does not have any methods
that need to be implemented by the class implementing it. It is
mainly used to inform the Java virtual machine (JVM)1 that you want
the object to be serialized.
There are two main classes that are used for reading and writing
objects to streams: ObjectOutputStream and ObjectInputStream.
The ObjectOutputStream provides the writeObject
method for writing an object to an output stream, and the
ObjectInputStream provides the readObject
method for reading the object from an input stream. It is important
to note that the objects used with these methods must be serialized.
That is, their classes must implement the Serializable
interface.
Serializing Existing Classes
Now that you know the basics of object serialization, let's see
how to read/write objects, or instances of existing serialized
classes, to streams. To write an object to an output stream, create
an output stream then use the writeObject method to write
it to the output stream. The source code in Code Sample 1 shows how to
save a Date object to a file.
Note: the Date class is serializable. In other words, it implements the Serializable interface.
Code Sample 1: SaveDate.java
import java.io.*;
import java.util.Date;
public class SaveDate {
public static void main(String argv[]) throws Exception {
FileOutputStream fos = new FileOutputStream("date.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Date date = new Date();
oos.writeObject(date);
oos.flush();
oos.close();
fos.close();
}
}
|
Reading the object and reconstructing its state is just
as easy. The source code in Code Sample 2 shows how to read a
serialized object and print its information.
Code Sample 2: ReadDate.java
import java.io.*;
import java.util.Date;
public class ReadDate {
public static void main(String argv[]) throws Exception {
FileInputStream fis = new FileInputStream("date.out");
ObjectInputStream ois = new ObjectInputStream(fis);
Date date = (Date) ois.readObject();
System.out.println("The date is: "+date);
ois.close();
fis.close();
}
}
|
In the example above we have worked with an instance of
the Date class, which is an existing serialized
Java class. The question that may come to mind is: are all
existing Java class serialized? The answer is: No.
Either because they don't need to be, or it doesn't make sense
to serialize some classes. To find out if a class is serializable,
use the tool serialver that comes with the JDK. You can
either use it from the command line as follows:
c:\> serialver java.util.Date
java.util.Date: static final long serialVersionUID = 7523967970034938905L;
(In this example, we are testing if the Date
class is serializable. The output here means that the Date
class is serializable and it print its version unique identifier.)
Or, alternatively, you can use the GUI-based
serialver tool using the command:
c:\> serialver -show
This command pops up a window similar to Figure 1, where
you can write the name of the class (including its path)
that you want to check. Figure 1 shows that the Date
class is serializable.
Figure 1: Date is a serializable class
Again, not all Java classes are serializable. For example,
Figure 2 shows that the Socket class is not
serializable.
Figure 2: Socket is not a serializable class
Serializing Custom Classes
Now, let's see how to serialize a custom class. In this example,
we create a custom class, UserInfo which is shown
in Code Sample 3. To make it serializable, it implements the
Serializable interface.
Code Sample 3: UserInfo.java
import java.io.*;
import java.util.*;
public class UserInfo implements Serializable {
String name = null;
public UserInfo(String name) {
this.name = name;
}
public void printInfo() {
System.out.println("The name is: "+name);
}
}
|
The next step is to create a class that creates a instance of
the UserInfo class and writes the object to an output
stream as shown in Code Sample 4. The output stream in this example is a
file called "name.out". The important thing to note from Code Sample 4
is that the writeObject method can be called any number
of times to write any number of objects to the output stream.
Code Sample 4: SaveInfo.java
import java.io.*;
import java.util.Date;
public class SaveInfo {
public static void main(String argv[]) throws Exception {
FileOutputStream fos = new FileOutputStream("name.out");
ObjectOutputStream oos = new ObjectOutputStream(fos);
// create two objects
UserInfo user1 = new UserInfo("Java Duke");
UserInfo user2 = new UserInfo("Java Blue");
// write the objects to the output stream
oos.writeObject(user1);
oos.writeObject(user2);
oos.flush();
oos.close();
fos.close();
}
}
|
Finally, we write a class that reads the objects that
have been saved, and invokes a method as shown in Code Sample 5.
Again, as with writeObject, the readObject
method can be called any number of times to read any
number of objects from the input stream.
Code Sample 5: ReadInfo.java
import java.io.*;
import java.util.Date;
public class ReadInfo {
public static void main(String argv[]) throws Exception {
FileInputStream fis = new FileInputStream("name.out");
ObjectInputStream ois = new ObjectInputStream(fis);
// read the objects from the input stream (the file name.out)
UserInfo user1 = (UserInfo) ois.readObject();
UserInfo user2 = (UserInfo) ois.readObject();
// invoke a method on the constructed object
user1.printInfo();
user2.printInfo();
ois.close();
fis.close();
}
}
|
To try out this example, compile the source files:
UserInfo.java, SaveInfo.java,
and ReadInfo.java. Run SaveInfo,
then ReadInfo, and you would see some output
similar to this:
The name is: Java Duke
The name is: Java Blue
Transporting Objects over Sockets
Now that we have seen how to write and read objects to/from
I/O streams in a single process, let's see how to transport
objects over sockets. First, we will see how to transport
existing object (such as the Date object), then we
will see how to transport custom objects.
Transporting an existing object
The daytime protocol is a widely used protocol by computers
running UNIX or any other operating system. The server in this
protocol listens on port 13 waiting for client requests. When
a client opens a connection to port 13, the server replies with
the current day and time. You can try it out simply by telneting
to port 13 of a machine that is running the daytime protocol.
Try this:
c:\> telnet prep.ai.mit.edu 13
If all goes well, you should see a Connection to host
lost message along with the current day and time of
the remote machine: prep.ai.mit.edu.
This protocol is quite easy to implement. Note, however,
that the protocol requires that the current day and time
are sent using standard ASCII characters. However, we are
going to send that information as an object (the Date object).
Also, in order to run a service on port 13, you must have
special administrative privileges; we will run our service
on a different port number, greater than 1023.
Here we develop a multi-threaded DateServer
that listens on port 3000 and waits for requests from clients.
Whenever there is a request, the server replies by sending a
Date object (over sockets) to the client as shown
in Code Sample 6.
Code Sample 6: DateServer.java
import java.io.*;
import java.net.*;
import java.util.*;
public class DateServer extends Thread {
private ServerSocket dateServer;
public static void main(String argv[]) throws Exception {
new DateServer();
}
public DateServer() throws Exception {
dateServer = new ServerSocket(3000);
System.out.println("Server listening on port 3000.");
this.start();
}
public void run() {
while(true) {
try {
System.out.println("Waiting for connections.");
Socket client = dateServer.accept();
System.out.println("Accepted a connection from: "+
client.getInetAddress());
Connect c = new Connect(client);
} catch(Exception e) {}
}
}
}
class Connect extends Thread {
private Socket client = null;
private ObjectInputStream ois = null;
private ObjectOutputStream oos = null;
public Connect() {}
public Connect(Socket clientSocket) {
client = clientSocket;
try {
ois = new ObjectInputStream(client.getInputStream());
oos = new ObjectOutputStream(client.getOutputStream());
} catch(Exception e1) {
try {
client.close();
}catch(Exception e) {
System.out.println(e.getMessage());
}
return;
}
this.start();
}
public void run() {
try {
oos.writeObject(new Date());
oos.flush();
// close streams and connections
ois.close();
oos.close();
client.close();
} catch(Exception e) {}
}
}
|
Note: the DateServer is a multi-threaded
server that is implemented by inheriting from the Thread
class. Another approach to developing multi-threaded servers is
to implement the Runnable interface instead
(inheritance vs. composition).
The client, DateClient, does not have to
send any messages to the DateServer once a
connection has been established. It simply receives a
Date object that represents the current day
and time of the remote machine. The client receives the
object and prints the date as shown in Code Sample 7.
Code Sample 7: DateClient.java
import java.io.*;
import java.net.*;
import java.util.*;
public class DateClient {
public static void main(String argv[]) {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
Socket socket = null;
Date date = null;
try {
// open a socket connection
socket = new Socket("yourMachineNameORipAddress", 3000);
// open I/O streams for objects
oos = new ObjectOutputStream(socket.getOutputStream());
ois = new ObjectInputStream(socket.getInputStream());
// read an object from the server
date = (Date) ois.readObject();
System.out.print("The date is: " + date);
oos.close();
ois.close();
} catch(Exception e) {
System.out.println(e.getMessage());
}
}
}
|
To run this example, the first step is to replace
the bold line in DateClient with the
machine name or IP address where the DateServer
will run. If both, the DateServer and
DateClient, will run on the same machine
then you can use "localhost" or "127.0.0.1" as the machine
name. The next step is to compile the source files
DateServer.java and DateClient.java.
Then run the DateServer in one window (if you are
working under Windows) or in the background (if you are working
under UNIX) and run the DateClient. The client
should print the current date and time of the remote machine.
Transporting Custom Objects
In the previous example, we have worked with existing
objects. What if you want to transport your own custom
objects. Is the process different?
In this example, we write an array multiplier server.
The client sends two objects, each representing an array;
the server receives the objects, unpack them by invoking a
method and multiplies the arrays together and sends the
output array (as an object) to the client. The client
unpacks the array by invoking a method and prints the
new array.
We start by making the class, whose objects will be
transportable over sockets, serializable by implementing
the Serializable interface as shown in Code Sample
8.
Code Sample 8: SerializedObject.java
import java.io.*;
import java.util.*;
public class SerializedObject implements Serializable {
private int array[] = null;
public SerializedObject() {
}
public void setArray(int array[]) {
this.array = array;
}
public int[] getArray() {
return array;
}
}
|
The next step is to develop the client. In this example,
the client creates two instances of SerializedObject
and writes them to the output stream (to the server), as shown
from the source code in Code Sample 9.
Code Sample 9: ArrayClient.java
import java.io.*;
import java.net.*;
public class ArrayClient {
public static void main(String argv[]) {
ObjectOutputStream oos = null;
ObjectInputStream ois = null;
// two arrays
int dataset1[] = {3, 3, 3, 3, 3, 3, 3};
int dataset2[] = {5, 5, 5, 5, 5, 5, 5};
try {
// open a socket connection
Socket socket = new Socket("YourMachineNameORipAddress", 4000);
// open I/O streams for objects
oos = new ObjectOutputStream(socket.getOutputStream());
ois = new ObjectInputStream(socket.getInputStream());
// create two serialized objects
SerializedObject so1 = new SerializedObject();
SerializedObject so2 = new SerializedObject();
SerializedObject result = null;
int outArray[] = new int[7];
so1.setArray(dataset1);
so2.setArray(dataset2);
// write the objects to the server
oos.writeObject(so1);
oos.writeObject(so2);
oos.flush();
// read an object from the server
result = (SerializedObject) ois.readObject();
outArray = result.getArray();
System.out.print("The new array is: ");
// after unpacking the array, iterate through it
for(int i=0;i<outArray.length;i++) {
System.out.print(outArray[i] + " ");
}
oos.close();
ois.close();
} catch(Exception e) {
System.out.println(e.getMessage());
}
}
}
|
Now we need to develop the server, ArrayMultiplier.
This server is similar to Code Sample 6. The only difference is in
the processing. In this example, the server receives two objects,
unpacks them and then multiplies the arrays together and finally
sends the output as an object to the client. The
ArrayMultiplier is shown in Code Sample 10.
Code Sample 10: ArrayMultiplier
import java.io.*;
import java.net.*;
public class ArrayMultiplier extends Thread {
private ServerSocket arrayServer;
public static void main(String argv[]) throws Exception {
new ArrayMultiplier();
}
public ArrayMultiplier() throws Exception {
arrayServer = new ServerSocket(4000);
System.out.println("Server listening on port 4000.");
this.start();
}
public void run() {
while(true) {
try {
System.out.println("Waiting for connections.");
Socket client = arrayServer.accept();
System.out.println("Accepted a connection from: "+
client.getInetAddress());
Connect c = new Connect(client);
} catch(Exception e) {}
}
}
}
class Connect extends Thread {
private Socket client = null;
private ObjectInputStream ois = null;
private ObjectOutputStream oos = null;
public Connect() {}
public Connect(Socket clientSocket) {
client = clientSocket;
try {
ois = new ObjectInputStream(client.getInputStream());
oos = new ObjectOutputStream(client.getOutputStream());
} catch(Exception e1) {
try {
client.close();
}catch(Exception e) {
System.out.println(e.getMessage());
}
return;
}
this.start();
}
public void run() {
SerializedObject x = null;
SerializedObject y = null;
int dataset1[] = new int[7];
int dataset2[] = new int[7];
int result[] = new int[7];
try {
x = (SerializedObject) ois.readObject();
y = (SerializedObject) ois.readObject();
dataset1 = x.getArray();
dataset2 = y.getArray();
// create an array by multiplying two arrays
for(int i=0;i<dataset1.length;i++) {
result[i] = dataset1[i] * dataset2[i];
}
// ship the object to the client
SerializedObject output = new SerializedObject();
output.setArray(result);
oos.writeObject(output);
oos.flush();
// close connections
ois.close();
oos.close();
client.close();
} catch(Exception e) {}
}
}
|
To run this example, modify the ArrayClient source
specifying the machine name or IP address where the ArrayMultiplier
server will run. Note that if you wish to run the server and
client of this particular example on two different machines then
both machines must have a copy of the SerializedObject
class. This breaks information hiding and force tight coupling. A
solution to this problem would be to write an interface that extends
the Serializable interface and then have the
SerializedObject class in Code Sample 8 implement the new
interface. Using this technique, you only need to provide copies of
the interface to the client and server, but not implementation.
If you run the ArrayMultiplier and ArrayClient
successfully, you should get the output:
The new array is: 15 15 15 15 15 15 15
RMI vs. Sockets and Object Serialization
The Remote Method Invocation (RMI) is a Java system that can
be used to easily develop distributed object-based applications.
RMI, which makes extensive use of object serialization, can be
expressed by the following formula:
RMI = Sockets + Object Serialization + Some Utilities
The utilities are the rmi registry and the compiler to
generate stubs and skeletons.
If you are familiar with RMI, you would know that developing
distributed object-based applications in RMI is much simpler
than using sockets. So why bother with sockets and object
serialization then?
The advantages of RMI in comparison with sockets are:
- Simplicity: RMI is much easier to work with than sockets
- No protocol design: unlike sockets, when working with RMI there is no need to worry about designing a protocol between the client and server -- a process that is error-prone.
The simplicity of RMI, however, comes at the expense of
the network. There is a communication overhead involved
when using RMI and that is due to the RMI registry and
client stubs or proxies that make remote invocations transparent.
For each RMI remote object there is a need for a proxy,
which slows the performance down.
Object Serialization Pitfall
When working with object serialization it is important to keep in mind
that the ObjectOutputStream maintains a hashtable mapping the
objects written into the stream to a handle. When an object is written to
the stream for the first time, its contents will be copied to the stream.
Subsequent writes, however, result in a handle to the object being written
to the stream. This may lead to a couple of problems:
- If an object is written to the stream then modified and written a
second time, the modifications will not be noticed when the stream is
deserialized. Again, the reason is that subsequent writes results in the
handle being written but the modified object is not copied into the
stream. To solve this problem, call the
ObjectOutputStream.reset method that discards the memory of
having sent an object so subsequent writes copy the object into the
stream.
- An
OutOfMemoryError may be thrown after writing a
large number of objects into the ObjectOutputStream. The
reason for this is that the hashtable maintains references to objects that
might otherwise be unreachable by an application. This problem can be
solved simply by calling the ObjectOutputStream.reset method
to reset the object/handle table to its initial state. After this call,
all previously written objects will be eligible for garbage collection.
The reset method resets the stream state to be the same as if
it had just been constructed. This method may not be called while objects
are being serialized. Inappropriate invocations of this method result in
an IOException.
Conclusion
This article presented an overview of object serialization.
The examples throughout this article show how easy it is to
work with object serialization and more importantly how to
use this mechanism to transport full-blown objects over sockets.
The choice between using RMI or sockets + object serialization really depends on the project and its requirements. It is a trade-off between simplicity (RMI) and efficiency (sockets + object serialization). If performance is an issue then sockets + object serialization is an attractive alternative to RMI.
Resources
About the Author
Qusay H. Mahmoud provides
Java consulting and training services. He has published dozens of
articles on Java, and is the author of Distributed Programming
with Java (Manning Publications, 1999).
1 As used on this web site, the terms Java virtual
machine or Java VM mean a virtual machine for the Java platform.
|