Distributed Object Computing With Voyager

  Enterprise developers everywhere are lining up to find out more about distributed object computing and how best to make use of it. Distributed objects have been a long time coming, with more than five years of buzz about Object Management Group's CORBA, but presently two distinct alternatives to CORBA for the Java developer are coming on strong: RMI (a product of Sun Microsystems, Inc.) and now Voyager (ObjectSpace, Inc.) both offer viable approaches to implementing distributed objects. Each system has, or plans to offer, support for the widely publicized CORBA protocol called IIOP, as well.

What is it about distributed object computing that is so compelling? And how can a newcomer to network programming get started in this exciting but somewhat daunting field? I'll answer those questions while offering a gentle introduction to building real distributed object systems. The latest developments in distributed object systems are proving to be the most powerful and user-friendly. So for this introduction we will take a close look at the newest and sportiest distributed object system out there right now: ObjectSpace Voyager. After you've taken a look at this product—which is free for commercial use—you may not want to look back.

From Client/Server To Distributed Object Computing

In the Beginning

With the advent of TCP/IP and UDP/IP sockets on the UNIX platform in the early 1980s client-server computing began to heat up. A common style of network programming quickly came about in which "client" programs could communicate and interact with distant "server" programs by exchanging text-based messages of varying complexity. The precise agreement between these programs on message format, meaning, and content has come to be known as a network, or wire protocol. The early wire protocols were simple two-way conversations, usually where the client would send a request, expecting the server to respond with one out of a handful of simple responses.

While network programming and network protocols have advanced considerably over the years, ironically, the original work in the field still forms the basis for most of what we do on the Internet today. In fact, all of the predominant protocols underlying the Internet were written according to the same basic recipe used to create the TCP/IP-based protocols from the early 1980s.

The Formative Years

Fast forward to the late 1980s. While the wire protocols were easily understood and could be formally documented in a concise manner, they proved difficult to implement by your average programmer. Developers faced a semantic gap between calling local functions and using remote functionality of a server program. In a distributed program the semantics for calling, or in this case hollering out to some function on a remote machine, were unnatural and ultimately problematic to the developer.

New ideas to provide remote function call semantics, or remote procedure call (RPC) as it came to be known, would hopefully come to the rescue and make network programming simpler and more widespread. Up until the advent of RPC, client/server programming remained one of the deep, dark secrets of highly specialized, low-level software developers. It was hoped that RPC, which later became ground zero for a standards battle, would spark a proliferation of new and as yet unimagined distributed applications.

The First Circle

While RPC did live up to its promise in function, its unwieldy form failed to make life easier for developers. In a finished program, there would no longer be a semantic gap between calling a local function and calling a remote function; both calls would look identical. This was, indeed, progress in distributed computing. But to get to the finished product, application developers were sidetracked and intimidated by issues of binary data byte ordering, tedious functional specifications, and costly supporting tools.
   Meanwhile, object-oriented computing and C++ were becoming mainstream. The focus for the next decade of work in the distributed computing world would shift from remote procedure call to remote method (or member function) invocation, the object-oriented equivalent of RPC. But the wait would be long, and in the interim, the past failure of RPC to deliver caused many developers to take a backward glance at the proven protocols of yester-year. In fact, the utterly simple and now ubiquitous HTTP protocol was created in the same style as the vintage protocols—almost as though remote method invocation and RPC had never existed.

Distributed Objects Come of Age

Today, most of the lessons of RPC have been learned, and still other problems posed by RPC have managed to creep back into some distributed object systems, such as the need for detailed functional specifications and expensive tools that generate copious amounts of source code. But what may have prevented early distributed object systems from overcoming the limitations of RPC is the programming languages on which these systems were based. CORBA (for Common Object Request Broker Architecture), for example, started out with language bindings for the languages COBOL, C, and C++.

Now, with the advent of Java and its modern language features like threads, reflection, serialization and garbage collection, it is possible to build robust distributed object systems that are simple to learn and use. Sun RMI and ObjectSpace Voyager are two examples of this new breed, and the shortcomings of CORBA for Java are gradually being ironed out as well. All of the distributed object systems mentioned will eventually run over the IIOP protocol, which could help bring about a transition from HTTP to IIOP on the Internet, as was forecast by Marc Andreessen of Netscape Communications, Inc. But as we enter this new era of possibility there are strong inertial forces holding back the advancement predicted by Andreessen. Only time will tell whether the simplicity of HTTP or the power of IIOP will prevail as the protocol of choice for the Internet.

Example: Implementing Remote Objects

In large part, the choice between HTTP-based and IIOP-based technology will be made by the developers who design the future vehicles we will drive out onto the Internet, and who always look for better ways to program. To that end, I turn your attention to an illustrative example of distributed objects. This quick test drive will show how to create remote objects and work with the Voyager system. With a simple working example in hand we will pop open the hood and look inside the example to see how this technology works its apparent magic.

Defining the Problem

Let's say you're designing a program to keep users at your corporate headquarters informed about the weather conditions outside (because most of them probably sit in cubicles with no windows). Your program will rely on a single PC outfitted with special weather monitoring equipment. Let's say this unique "WeatherPC" can report the following conditions: temperature, humidity, barometric pressure, wind speed, and direction. Furthermore, let us assume that the WeatherPC takes a reading once a minute, and you would like to make this up-to-date information available to everyone in the office who has a computer.

Here is how we might proceed to build the system using distributed objects. First, let's design a Java interface to represent the functionality we would like to extract from the weather station. It might look like what you see in Listing 1.

Next, we'll create a class that implements this functionality. Leaving out the details of how the class actually interacts with the weather monitoring equipment, we'll simply delegate that job to a class called WeatherSniffer (not shown). The implementation class, called WeatherServiceImp is shown in Listing 2.

This is simple enough, though you may be wondering why go through the trouble of using a Java interface in this situation? While it is not really necessary, using an interface here will help to illustrate the inner workings of the example in the next section. Before jumping into distributed objects, let's look at how the code we have so far could be used to build a stand-alone weather program intended to run on the WeatherPC.

A Local Object Solution

If we were concerned only with writing a program to display weather conditions on the WeatherPC itself, it might suffice to stop right here. The short program of Listing 3 has the makings of what we would need.

The WeatherWatcher1 program is straightforward. It creates a WeatherServiceImp object and invokes methods on that object through the WeatherService interface. Bear in mind that WeatherServiceImp delegates all of the work to a class called WeatherSniffer, which we understand has access to special equipment onboard the WeatherPC. This program would be sufficient if we were content to display the current weather on a single PC. But what about those poor cubicle-bound employees who also would like to know what the weather is like outside? If they were to run the program on their own computers, it wouldn't work because the WeatherSniffer class needs access to the special equipment of the WeatherPC. To provide for the other users, surprisingly, requires few modifications to the program you see in Listing 3.

A Remote Object Solution

The ObjectSpace Voyager 2.0 system is capable of taking an existing Java class and "treating" it to create a new class with an identical interface. This special helper class, in Voyager terminology, is called a "proxy" class. It is created in the image of another class and seems just like the original to anyone who uses it. But there's a catch: a proxy object does no real work by itself. Instead, a proxy object uses network communications to create and remotely control an instance of the real class it represents (you'll see how this works a little later). In other words, the proxy object acts like a mediator between the caller and the real object. It is through proxies that all remote methods calls are made.

In a previous release, Voyager 1.0 provided a command line utility for generating proxy classes, but with version 2.0 the utility is no longer needed. Instead, Voyager uses Java reflection to inspect a class and create the byte codes of a proxy class for it at runtime. This is a remarkable improvement over the earlier approach, because it obviates the need for any source code generation at all, and eliminates a tedious precompilation step. Whenever Voyager needs a proxy class, it generates byte codes for it on the fly and uses the Java class loader to bring the new code into the Java VM. In fact, the new classes need not even be written to disk!

As far as the developer is concerned, creation of proxy objects is fairly simple. One need only ask the Voyager API to "construct" a proxy object using a regular object as a template. I've modified the WeatherWatcher program to construct a proxy object for the WeatherServiceImp class. The new version of the program is shown in Listing 4. As you can see, the program is substantially unchanged except for references to the Voyager system on two lines: an import statement, and the WeatherService declaration.

This minor change to the program makes a world of difference in how it works. But before stepping through its new behavior, there is one more part of the Voyager system you should know about. To support the creation and "housing" of remote objects requires that a Java virtual machine (JVM) be running on each actual machine involved. To provide this, Voyager comes with a server program built right in along with its other classes. The server is written in Java, although no source code for the server is provided. It plays the same role as the ORB (object request broker) does in CORBA systems: creating and managing remote objects on behalf of the programs that use them over a network. The Voyager server can be run this way:


$ java com.objectspace.voyager.system.Main 8000



The "8000" argument used above is a port number through which the server can be contacted by external programs. Connecting to a server always requires a port number. Your Web browser usually connects to Web servers on port 80, without your needing to specify the port number, but occasionally you will see URLs with port numbers attached, for example, http://www.yourcompany.com:8080.

Back to the example program. Let's say the WeatherPC is known as weather-pc.yourcompany.com. Further, assume the Voyager server is running on the WeatherPC with port 8000, and the WeatherWatcher2 program will run on an employee's desktop computer. Now we can step through the WeatherWatcher2 program to see how the remote object version behaves. Here again is the modification made in the second version:


WeatherService service = 
	(WeatherService) Voyager.construct("WeatherServiceImp", 
	"weather-pc.yourcompany.com:8000");



Instead of creating our WeatherServiceImp object with "new", this time we delegate the job to Voyager's construct() method. The construct() method needs to know the name of the class to create, and the server on which to create it remotely. This statement accomplishes quite a bit: it not only creates a remote object, but gives us a way to interact with that object by creating a proxy object for it, too. Instead of returning a WeatherServiceImp object, the construct()_method returns a proxy object instead. The proxy is a regular local Java object, which represents—or stands in for as a surrogate—a remote WeatherServiceImp object created and running in a completely different JVM. Every action carried out on the surrogate will be passed along over the network to the real object on the remote computer. With the remote object created, let's see what happens a little bit later in the program. Consider the line that follows:


System.out.println("Temperature: " + service.getTemperature();



The method getTemperature() in the local proxy is what is being called here, but the proxy doesn't do the actual work of the method. Instead, the proxy takes the arguments—if any, but in this case there are none—and passes them across the network to the actual WeatherServiceImp object on the remote machine. Then the proxy remotely invokes the real getTemperature() method on the remote object. Finally, the proxy gathers the return value of the remote method from the network connection and returns it back in the local program. In other words, the statement above, though coded to look just the same as in the first program, can transparently call on a remote object to accomplish its job.

Recall the use in both programs of the WeatherService interface. Using an interface to represent classes that may either be local (as is the case with WeatherServiceImp) or remote (when a proxy is being used) cuts down on the amount of code that must change between the WeatherWatcher1 and WeatherWatcher2 programs. Moreover, the use of an interface illustrates how distributed objects can, at some level, be made transparent even to the program that uses such objects. In a larger program it is likely that the WeatherService interface would be passed around to many different classes. Those classes would never need to know whether WeatherService represented a local object (WeatherServiceImp) or a remote object (WeatherServiceImp's proxy).

The second version of the WeatherWatcher program, while barely different from the first, is no longer what it appears. It makes the unique features of the WeatherPC accessible from any other computer on its network. Now, the weather conditions outside of the office can be monitored by any employee right from his or her own computer. In practical terms, this overall approach has enormous power to transform the way we think about programs, because the object-oriented code you write can now have far-reaching effects across a network.

Recapitulation

This example covered quite a bit of new ground. It demonstrated the ability to make a unique resource available to other computers on a network. We saw how, almost as an afterthought, a stand-alone Java program could be converted into a distributed application utilizing a remote object through the Voyager server. Some subtle questions about the program still linger, though. For example, how is it that creating a WeatherServiceImp proxy object can result in the remote creation of a WeatherServiceImp object? And second, the method getWindSpeed() returns an integer, while getWindDirection() returns a String object. How can these different data types be returned from the remote object to the local surrogate without special programming?

How It Works

Sockets, Reflection, Serialization, Threads

Right out of the box, the Java classes make working with TCP/IP sockets relatively easy. Setting up a network connection between two programs is just a matter of enlisting two built-in Java classes: Socket and ServerSocket. With a connection established, Java makes input and output over that connection easy by using the same stream classes you would use for file I/O. For the designers of products like Voyager, these tools are a godsend. Needless to say, standard C or C++ have never offered anything comparable.

In addition, Java Reflection makes it possible to programmatically discover the member variables and methods of a class at runtime. Reflection can even be used to create objects and invoke methods dynamically using the special classes Constructor and Method. This new language feature makes it possible for Voyager to inspect the methods of an arbitrary class, generate a proxy class to match, and load up the byte codes for the proxy class.

Serialization is the process of taking the member data of an object and representing it as a serial stream of bytes, usually for the purpose of storing the data in a file or database. Serialization, when combined with a socket connection can also be used to transmit the state of objects from one place to another. What's more, objects endowed with serialization capability can read in their state from a serial data stream, too. And best of all it doesn't take any programming to get the benefits of serialization.

Finally, Java is a threaded language, meaning that lots of things can be going on concurrently within a single process. Threads relieve programmers from tricky input/output situations where multiple socket connections are involved, like when reading and writing need to take place simultaneously. All of these language elements taken together pave the way to distributed object computing tools like Voyager, which can be seamlessly integrated into your programs.

Putting Them All Together

Given these four essential tools; sockets, reflection, serialization, and threads, we can begin to put the pieces together to see how distributed objects in Voyager are made to work. First, the creation of a remote object will be described, step-by-step, followed by the operations involved in remote method invocation.

Remote Object Creation
1.   A proxy object is created locally, given a class name, and the host and port number of a Voyager server. In our example, this looks like:

Voyager.construct("WeatherServiceImp",
"weather-pc.yourcompany.com:8000");


2.   The construct() method calls on the Voyager class library to create a socket connection to the Voyager server named in the constructor (unless a connection to that address already exists).

3.   With a socket connection established, the Voyager server dynamically loads the byte codes of the named class, in our case WeatherServiceImp.

4.   After the WeatherServiceImp class is loaded, Java reflection is used to construct an instance of the class in the remote JVM of the server.

5.   A unique identifier for the remote object is created by the Voyager server and a reference to the object itself is stored in a table on the server along with the identifier.

6.   The unique identifier is sent back over the socket to the proxy object, which keeps a copy of the name on hand, creating a means of connecting, or binding, the surrogate object to the actual object.

7.   The local proxy object can then be used to refer to and remotely invoke methods on an object that exists within the remote Voyager server.


Remote Method Invocation
1.   In a local, proxy object-say an instance of WeatherServiceImp's proxy-a method is called. For the purpose of this discussion, let's say it is the getWindDirection() method.

2.   The socket connection established during the creation of the remote object is used again, this time to send along the unique identifier of the object on the other side of the connection.

3.   In addition to identifying the object of interest on the Voyager server, it is necessary to identify which method should be invoked remotely. This information is sent to the server as well.

4.   And, if the method requires arguments, such arguments (as passed to the surrogate method) are written out across the socket connection using serialization.

5.   After deserializing the method arguments, if any, and turning that serial data into either Java primitives or objects, the Voyager server uses reflection to invoke the desired method.

6.   The return value of the method is first obtained by the Voyager server which then uses serialization to send the value back over the socket connection to the proxy object.

7.   Finally, having read the return value data from the socket and deserialized it, the proxy object gives the return value (again, either a built in type or an object) over to the caller.


It is crucial to note that serialization can be done on any data type imaginable, from a simple integer to a height-balanced tree. Serialization makes it possible to use remote objects and their methods just as one would use local objects, with almost no restrictions on the form or structure of the data types involved.
You may be wondering where threads come into play. To support multiple distributed objects and multiple remote clients, the Voyager libraries employ numerous threads on both sides of every network connection with specialized roles like listening for connections, keeping in touch with active objects, and determining when objects are no longer in use. That last task is called distributed garbage collection. Because in Java we don't explicitly free objects from memory, the task of memory management is taken care of for you in Voyager as well, though Voyager also allows for objects to have a specified life span.

This walk-through should give you a better grasp of what goes on behind the scenes with a typical remote object in the Voyager system. Other tools for implementing distributed objects work along similar lines, but significant differences do exist. For example, with a CORBA product you won't be able to convert existing classes into remote objects on the fly at runtime. Nor will you be able to easily take advantage of serialization to toss complex objects back and forth across a network.

Conclusion

If you're eager to try out distributed object computing on your own, I encourage you to download Voyager from the ObjectSpace Web site and give it a whirl. It has good documentation with interesting example programs to stir up your imagination. Even if you don't always have access to more than one computer, you can use Voyager on a single machine by running separate programs concurrently-these programs will still communicate with TCP/IP sockets if you set your computer name to "localhost." It's like having a network in a computer, and it's an easy way to get started with distributed objects. Another easy path of entry into the distributed object's world is through RMI, which you probably already have if you program with Sun's Java JDK. Finally, certain CORBA products are available free for evaluation purposes. Diving right in with distributed object computing can be a lot of fun, and it might just make your next project a whole lot easier.


Russ Ethington lives and works in New York City, where he writes distributed computing software on Wall Street. He can be contacted at [email protected].