Dr. Dobb's Journal September 2001
The pervasiveness of the Internet and area networks has pushed computing from a desktop environment into a distributed one. Resources employed by applications are often distributed across a network of computers. For example, a spreadsheet application running on a desktop workstation may have been launched from a disk drive that is stored on a remote application server. The data used by the spreadsheet application may be loaded from a database server hosted on a different computer. Furthermore, the processed data could in turn be published on a web server that is hosted on yet another computer.
It is not difficult to imagine scenarios that employ distributed computing technologies, mainly because distributed computing has become so commonplace that it is hardly noticed. For software engineers, however, this is exactly the challenge they must constantly conquer. Software must be designed with the capability of operating in a dynamic distributed-computing environment.
Some common motivations for designing distributed applications are:
Any one of these motivations is sufficient reason for developing software applications as distributed agents. These types of applications require that data distribution be effective, adaptive, efficient, and flexible to evolving environments and requirements.
In this article, I present a generic model designed to serve as a blueprint for development of scalable distributed-computing applications. I included Java source code that you can use to assemble simple or complex distributed-computing applications.
The most promising advantage of developing with Java is its platform independence. This yields the potential for hosting a developed application across various platforms. Java's employment of the object-oriented paradigm is another advantage. An object-oriented application uses class inheritance that goes a long way in making the distributed model scalable. However, a more important advantage of Java technology is its abstract interface facility. A Java interface lets you simply declare variables and methods (subroutines) without actually implementing them. The interface is useful because other classes can implement the interface and define exactly how those methods will operate. This means that an interface can be declared once and defined many times and in different ways. Later, it will become more evident how Java's interface facility is the primary key to making this model both scalable and distributed. Java does have other built-in advantages such as multilevel networking, multithreading, convenient error handling, and security.
There are a few basic requirements that a distributed application must be able to satisfy:
Figure 1 illustrates the basis for the distributed-computing model I present here. This figure depicts a simple client-server application that utilizes information stored in a database. The design of this model lets a client application easily execute in different modes. It may be executed directly on the database host computer, or it may operate remotely in another computer on the network and utilize different communication protocols. However, as far as the client application is concerned, the data always originates from the same source, the interface manager (IntfMgr: see Listing One). Since an interface only declares methods, the actual implementation of those methods can be defined differently to accommodate the necessary protocols employed for data transfer.
In Figure 1, the majority of the work is performed in a local manager class labeled LocalMgr (Listing Two). The local manager essentially implements the interface manager (IntfMgr) and explicitly defines the methods that extract the information from the database on the host computer.
The keystones for this model are the interface manager (IntfMgr) and its implementation in the LocalMgr class. All other implementations of the IntfMgr (SocketMgr, RmiMgr, CorbaMgr) are simply bindings that connect the local manager with a client application through different means. To better illustrate how this model functions, a simplified step-by-step sample implementation will be presented. For the purpose of brevity, only the RMI manager will be implemented and bound to the local manager. Figure 2 shows the simplified model.
The first step in the development process is to generate the interface construct; see Listing One. Basically, the interface is just a source file that contains only the declarations of methods that will be implemented, or defined, in other classes. For this generic example, there are only two simple methods declared in the interface. The first method, getHostOS, is declared to return a String object containing the operating-system name of the hosting computer. The second method declared getHostTime, returns a String containing the hosting system date and time.
The LocalMgr class (Listing Two) implements the interface IntfMgr and defines the getHostOS and getHostTime methods. This class is where the actual work gets done. Of course, the LocalMgr class could have much more sophisticated methods. For instance, the methods could query a database or sample data from sensors, and return more complex objects than a String object.
Using the interface is simple for a client application. Listing Three is source code for the client (DCEx.java) that will use the interface manager described earlier. Simply, a reference is declared for the interface IntfMgr and labeled mgr. The declared interface manager (mgr) is then defined by setting it equal to its new implementation of LocalMgr. At this time, DCEx is ready to employ the methods declared in the interface as implemented (defined) by LocalMgr. The DCEx program only calls the mgr.getHost method and prints out the returned string containing the name of the operating system.
After building the foundation (the interface), the other building blocks may be constructed on top to create a more useful distributed application. In the example being presented, the client application is extended to a distributed application by using Java RMI.
Through RMI, Java provides a powerful mechanism to distribute objects across a network. Essentially, RMI is "a set of classes and interfaces that enables one Java object to call the public methods of another Java object running on a different virtual machine" (see Beginning Java, by I. Horton, Wrox Press, 1997). In practice, RMI lets you design distributed applications just as if they were nondistributed applications. A step further is taken for this example, and RMI is simply used as a scaling step to reach the previously implemented LocalMgr methods.
To accomplish the RMI extension, first create an RMI manager (RmiMgr; Listing Four) that implements the original interface manager (IntfMgr). Listing Four shows this implementation. Notice that RmiMgr delegates the final definition of the implemented methods (getHostOS and getHostTime) to yet another interface, the RmiIntf. The RmiIntf manager is in turn implemented remotely through the RmiImpl object that is registered in a remote host's RMI registry; see Listings Five and Six. Then the implementation of the RMI interface (RmiImpl) once again delegates the actual definition of the declared methods to the LocalMgr implementation. Here lies the strength of scaling an application by employing interfaces and reusing previously defined classes and methods. Consequently, the RMI interface and implementation simply become transports for a previously developed capability.
Only a simple modification is needed in the DCEx client to make it executable from a remote computer. Listing Seven shows the modified DCEx client utilizing the manager RmiMgr. LocalMgr is commented out. Also, the remote host name and port number must be provided. These must match the host name and port number where the RMI server (RmiImpl object) is initialized.
The implementation of the RMI manager (RmiImpl) can be automatically registered in the RMI registry of a host computer through a simple RMI server program; as in Listing Eight. The example program MyRmiServerReg creates a registry using a specified port number (1099 default RMI port). An instance of the RmiImpl object is created and bound to the created registry. The DCEx client can reach the RmiImpl object by providing the RmiMgr with the remote host name and a port number. In Listing Four, the RmiMgr constructor explicitly looks for the RmiImpl object at a given host name and port number.
Explanations of other RMI intricacies are out of scope for this article. However, all the necessary source-code listings are included here to fully implement the RMI server. You need only to deal with proper compilation of source code. Java source code is compiled using the Java compiler javac: javac DCEx.java, javac RmiImpl.java, and so on. Then the RMI compiler (rmic) must be used to produce a Skeleton and Stub to serve as the transport between the client (DCEx) and remote object (RmiImpl). This is accomplished by executing rmic RmiImpl, where RmiImpl refers to its previously compiled class (RmiImpl.class, for example).
To keep this example simple, I've omitted a security manager. However, Java does provide security models (RMISecurityManager, for example) that can be easily added to the DCEx client and to the RMI server.
In this article, I've presented a generic model for building scalable distributed-computing applications that can easily adapt to support various communication protocols. The strength of this basic model lies in the effective use of Java's interface facility. Basically, this model illustrates the interface's ability to provide multiple definitions for a declared base set of methods (polymorphism). Through the interface facility, an application can scale several levels deep and be used to developed sophisticated applications. The source code presented here also serves as a clear illustration of how to deploy a distributed application.
Employing this type of interface model provides many conveniences for developers and users. Interfaces can be viewed as isolators that can prevent extensive modifications to a software application when it changes computing environments. Ultimately, the principal beneficiary of this interface model will be the client software application. By employing this model, client applications will be minimally impacted when there are changes to underlying data infrastructure or to data delivery mechanisms.
DDJ
public interface IntfMgr
{
public String getHostOS() throws Exception;
public String getHostTime() throws Exception;
}
import java.util.Date;
public class LocalMgr implements IntfMgr
{
protected String strMgrType;
LocalMgr() { strMgrType = "Local"; }
public String getHostOS() throws Exception {
return System.getProperty("os.name");
}
public String getHostTime () throws Exception {
Date d = new Date();
Return d.toString();
}
}
// Client for Distributed Computing Example
import java.util.*;
public class DCEx
{
DCEx() { }
public static void main (String argv[ ])
throws Exception
{ // Let the interface be defined by local manager
IntfMgr mgr = new LocalMgr();
// Execute a manager method
String str = mgr.getHostOS();
System.out.println("Returned Host name: "+str);
}
}
// RMI Manager
import java.rmi.*;
import java.rmi.registry.*;
public class RmiMgr implements IntfMgr
{
private RmiIntf mgrRmi;
// Constructor
public RmiMgr(String host, int port)
throws Exception {
String strURL = "rmi://"+host+":"+ port+"/RmiImpl";
mgrRmi = (RmiIntf)Naming.lookup(strURL);
}
public String getHostOS()
throws Exception {
return mgrRmi.getHostOS();
}
public String getHostTime()
throws Exception {
return mgrRmi.getHostTime();
}
}
// Interface RmiIntf
import java.rmi.*;
public interface RmiIntf extends Remote
{
public String getHostOS()
throws Exception, RemoteException;
public String getHostTime()
throws Exception, RemoteException;
}
// Implementation of the interface RmiIntf
import java.rmi.*;
import java.util.*;
public class RmiImpl
extends java.rmi.server.UnicastRemoteObject
implements RmiIntf
{
IntfMgr mgr;
public RmiImpl()
throws Exception, RemoteException {
mgr = new LocalMgr();
}
public String getHostOS()
throws Exception, RemoteException {
return mgr.getHostOS();
}
public String getHostTime()
throws Exception, RemoteException {
return mgr.getHostTime();
}
}
// Client for Distributed Computing Example
import java.util.*;
public class DCEx
{
DCEx() { }
public static void main ()
{ // Interface is not defined by LocalMgr anymore
// IntfMgr mgr = new LocalMgr();
// manager is now defined by RMI manager
IntfMgr mgr = new RmiMgr("remoteHostName", 1099 );
// Execute a manager method
String str = mgr.getHostOS();
System.out.println("Returned Host name: "+str);
}
}
import java.rmi.*;
import java.rmi.registry.*;
public class MyRmiServerReg
{
public static void main(String args[])
{
try {
int port=1099;
Registry reg =
LocateRegistry.createRegistry(port);
RmiImpl rmiImpl = new RmiImpl();
Naming.rebind("//:"+port+ "/RmiImpl", rmiImpl);
System.out.println(
"*** MyRmiServerReg: rmiregistry is ready "+
" to execute remote method invocations of " +
" RmiImpl on port# "+port);
}
catch (Exception e) {
System.out.println("Rmi Server Error:" + e.getMessage());
}
}
}