Kevin is a developer of distributed-systems management products for Tivoli Systems, an IBM company. He can be reached at [email protected]
Using Java and C++ together can offer many advantages to client/server developers. Java's goal of platform independence makes it ideal for GUI development of client applications. C++, on the other hand, provides the flexibility and performance required by a server executing hundreds of transactions per client. A problem to be solved is how to provide a seamless way of exchanging information between a Java client and C++ server.
In this article, I'll address the issues surrounding data exchange between Java and C++, using object serialization over TCP/IP socket connections. Object serialization is a method by which object information is exchanged between a sender and receiver. To serialize an object, the sender writes a stream of bytes to a data stream. In this case, it's a data stream between two TCP/IP socket connections. The data stream contains attribute and class information about the object being serialized. The receiver reads the data stream and is able to reconstruct the corresponding object.
An advantage of object serialization is that it provides for the persistence of object states. Object persistence is the ability of an object to preserve its state and restore it at a later time. As an example, a persistent customer-order object would write information about the customer's name, order number, price, and the like, to a TCP/IP socket output stream. The receiver can reconstruct a replica of the customer-order object based on attribute information. Another advantage of this implementation is that the reading and setting of attribute information is done at the object level. In other words, the object itself knows how to read and write itself to the data stream.
Figure 1 illustrates the classes used to support object serialization between Java and C++. Each box represents a Java class or a C++ class. The top of the box contains the class name. The bottom contains the data members. And the middle contains the methods that the class implements. The lines represent the relationships the classes have with each other. Objects to be serialized inherit from the abstract base class SerializableObject. Classes that inherit from it must implement SerializableObject::write() and SerializableObject::read(). SerializableObject::write() describes the object's attributes to be written to the TCP/IP output stream. On the receiving end, SerializableObject::read() describes the attributes to be read from the input stream. Both methods make use of SerializableOutputStream and SerializableInputStream to perform the low-level methods of reading and writing to the TCP/IP data stream.
For the Java client implementation, Java's java.net.socket class is used to establish a connection to the server. On the C++ server side, Socket and ServerSocket classes are implemented. The ServerSocket class is similar to the java.net.serversocket class. Mainly, it's used to encapsulate the TCP/IP accept function. The accept function is idle until it receives a port connection request from a client. When it receives a connection, it returns a socket handle. The ServerSocket::accept() stores the socket handle in a Socket object, and returns the object to the caller.
Once a connection is established between the client and server, input and output stream data can be written to the socket. SerializableInputStream and SerializableOutputStream is used to serialize the data between the client and server. SerializableInputStream is used for reading data, while SerializableOutputStream is used for writing data. SerializableOutputStream implements methods to allow for writing integer, byte, character, or strings to the TCP/IP socket data stream. Conversely, SerializableInputStream implements methods to read the data types from the data stream. Both classes are implemented in Java and C++ and accept a socket as a parameter. For the Java implementation, the classes make use of Java's DataInputStream and DataOutputStream classes for a TCP/IP socket connection. These allow for reading and writing Java data types in a machine-independent way. The C++ implementation uses the TCP/IP recv() and send() functions to receive and send data.
Using DataInputStream and DataOutputStream when going from a Java-based to an Intel-based platform requires some juggling. Since Java has its origins with Sun Microsystems (maker of the UNIX-based Solaris operating system), it's no surprise that it uses a Big-endian format like most UNIX operating systems. Big-endian format has the most-significant byte at the lowest address. Java writes data to the TCP/IP output stream in Big-endian format. When going from Java to an Intel platform, the bytes have to be shifted to Little-endian format and vice versa when going from Intel to Java. The byte shifting is handled in the C++ implementation of the SerializableInputStream and SerializableOutputStream (which can be seen in the code example, Figure 4, to be discussed later in this article). The exception to the Big-endian and Little-endian conversion rules is the serialization of strings. Java provides writeUTF() and readUTF() methods in the DataOutputStreams and DataInputStreams, respectively. The Universal character set Transformation Format (UTF) was created by the X/Open-UniForum Joint Internationalization Group (JIG) and subsequently adopted by ISO, under the designation UTF-8. It's designed to handle the encoding and decoding of Unicode strings and represents a universal way of exchanging string information. The first two bytes represent the string's length, followed by the character information. Figure 2 identifies the character representation for different character ranges for Java's DataInputStream and DataOutputStream, and is derived from the Sun's Java API documentation.
To illustrate, I created a Java application (available electronically; see "Resource Center," page 3) and a simple C++ server (also available electronically). The client and server serialize a TimeDate object.
The client uses the class to display the server time. The server is idle, waiting for a server port connection from a client. Once connected, it waits to read a TimeObject from the client. My example server demonstrates sending and receiving a serialized object, but is somewhat limited since it accepts only one client at a time. Ideally, you would spawn off separate threads to handle each client connection.
Figure 3 is an action diagram indicating what happens between the client and server. It assumes that a socket connection has been established between the two. On the Java client side, number (1) shows the TimeObject being created. The TimeObject inherits from SerializableObject and implements the SerializableObject::read() and SerializableObject::write() methods. Number (2) shows the creation of the SerializableOutputStream that contains the low-level methods to write attribute date types to the TCP/IP stream. To serialize a TimeObject, SerializableOutputStream::writeObject() is called, passing it the TimeObject (3). The writeObject() method needs to know how to serialize a TimeObject, so the method calls the TimeObject::write() method (4). The method begins writing attribute information to the TCP/IP data stream, calling the appropriate method for the data type being serialized (5).
Meanwhile, on the server side, a TimeDate object is created in C++ that is identical in attributes to the TimeDate object for the client (1). The corresponding SerializableInputStream object is created to read from the TCP/IP input stream (2). The SerializableInputStream::readObject() is called to read input for a TimeObject (3). The readObject() method invokes the corresponding TimeObject::read() method for this object (6). The TimeObject::read() begins reading the data types from the input stream and initializes the object's private attributes with the data read from the input stream (7). It should be noted that the read() on the data stream makes use of the TCP/IP recv() function to read byte information. The recv() function will block until there is data to be read from the stream.
After the TimeDate::setServerTime() method is called, the server serializes the data back to the Java client. As can be imagined, the process is reversed when it's time for the server to serialize the TimeDate object back to the client. Figure 4 illustrates a code fragment displaying how the server serializes information back to the client. The server creates a SerializableOutputStream and calls SerializableOutputStream::writeObject() to write the TimeObject back to the client. The client in turn creates a SerializableInputStream and makes use of the SerializableInputStream::readObject() to read the object from the input stream. At this point, the Java TimeDate contains the same attribute information that the C++ TimeDate contained. The TimeDate::getTotalTime() is called to display the server time in the Java list box.
Even though the classes to support object serialization are useful as is, there are still some enhancements that should be made to make classes more robust and flexible:
- Add additional exception handling. There are many areas where additional checking should be applied. The most obvious is probably the checking of the data types being serialized. It can become unpredictable if a writeInt() is done by the sender and a readChar() is applied by the receiver. One simple solution would be to encode an ID to indicate what data type has been written. An exception could be thrown if the ID does not match the expected data type.
- Add additional data types that can be exchanged between Java and C++. The example presented supports integer, char, string, and byte. To be truly useful, additional data types need to be supported.
- Add support for UTF-8 encoding. The implementation only supports one-byte encoding of character data for the SerializableOutputStream::writeString() and SerializableInputStream::readString() methods.
- Better support for serialization of applets. The sample program is written as a Java application and not an applet. Part of the reason for this is the simplicity of the server and Java's security when running an applet in a browser. When running inside of a browser, such as Netscape's Navigator, the applet is only allowed to connect to the server from which it originated.
- Another problem with the sample program is that it's very synchronous. For every writeObject there has to be a corresponding readObject at the other end. This is too restrictive for any server implementation. Ideally, you would like to be able to send any type of serializable object and have the server or client figure out what kind of object it is. This can be done by including class information in the data stream. The class information could contain the class name to identify the object, and in the case of going from C++ to Java, the class name could be used to instantiate the class during Java run time.
In those development projects where a hybrid approach to using Java and C++ is required, object serialization provides a simple way to bridge the gap. Object serialization allows for object persistence by preserving object attribute information at the object level, providing the added advantage that the objects themselves contain the information as to what attributes should be serialized.
Copyright © 1998, Dr. Dobb's Journal