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 theSerializable
interface.
Code Sample 1: SaveDate.java
import java.io.*; |
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.*; |
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.*; |
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.*; |
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.*; |
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.*; |
Note: the
DateServer
is a multi-threaded server that is implemented by inheriting from theThread
class. Another approach to developing multi-threaded servers is to implement theRunnable
interface instead (inheritance vs. composition).
< a>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.*; |
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.*; |
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.*; |
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.*; |
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 theObjectOutputStream
. 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 theObjectOutputStream.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
- Object Serialization
- Sockets programming in Java: A Tutorial
- All About Sockets
- Advanced Object Serialization
- RMI Trail of the Java Tutorial
- The Daytime Protocol
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).
'Dev > Java' 카테고리의 다른 글
Java System Properties (0) | 2007.12.08 |
---|---|
Java Programming Language: Design Principles and Proposals (0) | 2007.11.29 |
.getClass() 와 .class의 차이점 (0) | 2007.11.18 |