출처 : http://java.sun.com/developer/technicalArticles/ALT/sockets/

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
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
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).

< 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.*;
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

Coffecup Logo

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.

'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
Posted by yeori
,