Received: from SOUTH-STATION-ANNEX.MIT.EDU by po10.MIT.EDU (5.61/4.7) id AA02104; Tue, 14 Dec 99 20:17:36 EST Received: from hermes.javasoft.com by MIT.EDU with SMTP id AA03977; Tue, 14 Dec 99 20:17:17 EST Received: (from nobody@localhost) by hermes.java.sun.com (8.9.3+Sun/8.9.1) id BAA16316; Wed, 15 Dec 1999 01:16:49 GMT Date: Wed, 15 Dec 1999 01:16:49 GMT Message-Id: <199912150116.BAA16316@hermes.java.sun.com> X-Authentication-Warning: hermes.java.sun.com: Processed from queue /bulkmail/data/ed_24/mqueue4 X-Mailing: 193 From: JDCTechTips@sun.com Subject: JDC Tech Tips December 14, 1999 To: JDCMember@sun.com Reply-To: JDCTechTips@sun.com Errors-To: bounced_mail@hermes.java.sun.com Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii X-Mailer: Beyond Email 2.2 J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, December 14, 1999. This issue covers using remote method invocation to access legacy databases. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING REMOTE METHOD INVOCATION TO ACCESS LEGACY DATABASES Remote Method Invocation (RMI) is a way to communicate between two Java(tm) programming language applications that are remote from each other in a network. RMI differs from lower-level communication mechanisms like sockets in a couple of important areas. One is that you can use RMI to do remote method calls in a natural way. That is, you can obtain a reference to an object on a remote server and then make method calls on the object. You can also do remote method calls with sockets, but you have to set up your own arrangements for argument passing, return value passing, and so on; this approach isn't integrated into the Java programming language very well. RMI takes care of these details for you. Another difference is that RMI requires Java support. If you have a server and a client, and they communicate using RMI, then both need to be implemented as Java programs. This is because of the underlying Java facilities used to implement RMI, such as serialization. It's also because of the Java language semantics implied by RMI, such as interfaces. But the requirement that you use Java on both ends doesn't prevent you from using RMI with other languages. And that's one of the things that this tip illustrates. The tip presents a client/server RMI-based application that accesses a legacy database via C and C++ functions. Here "legacy" means an older application, one that isn't necessarily written using Java programming or that is network aware. So this is a fairly complex application, one where you have a: o Database o C function that accesses a database o C++ wrapper function that implements a Java native method, and calls a C function o Server that creates a remote object and registers it with an RMI registry o Client that looks up the remote object reference via the registry. The client makes a method call on the object to access the database through the native method wrapper and the C function. The RMI registry, server, and client all run on the same machine, but the tip fully illustrates areas like security and codebases. One important thing to note about this tip... although the tip demonstrates a working RMI application, it is not a tutorial on RMI. The last section of the tip points to documentation that you read to learn more about RMI. How This Tip is Organized Because the application is fairly complex, this tip is longer than the typical JDC Tech Tip. To make it easier to follow, the tip is organized in three parts: Part 1. Developing the Application. This part defines the pieces that comprise the application. The tip provides source code for each of these pieces. Part 2. Putting it all Together. This part describes how to put all the pieces together into a working application. Part 3. Running the application. This part describes the actions you need to take to run the application. Part 1. Developing the Application Developing an RMI application involves steps such as defining an interface for remote objects and implementing the interface. This part of the tip describes how to develop this particular RMI application, that is, one that accesses a remote database through C and C++ functions. 1. Define an Interface For Remote Objects The first step in developing an RMI application is to define an interface that remotely-called objects must implement. In this application, the interface is called Search, and it is in a package called rmitest: // Search.java package rmitest; import java.rmi.Remote; import java.rmi.RemoteException; public interface Search extends Remote { public String findEntry(String w) throws RemoteException; } Search declares a single method findEntry, that is used to look up an entry in a database (such as a phone or address list), and return the entry that it found. In other words, the server application registers (with an RMI registry) an instance of a class that implements the Search interface. A client can then consult the registry to obtain a reference to the instance's stub. The client can call methods via the stub to effect database lookups. A further discussion of stubs is found later in the tip. An interface that describes remote class functionality must extend the java.rmi.Remote interface. Also, methods must declare that they throw java.rmi.RemoteException. RemoteException is used to handle the special types of failures that can occur in RMI, such as a network failure. 2. Define an RMI Remote Object That Implements the Interface After the interface for a remote class has been defined, you need to implement the interface, that is, define a remote class. In this example, the class is called SearchImpl: // SearchImpl.java package rmitest; import java.rmi.server.UnicastRemoteObject; import java.rmi.RemoteException; public class SearchImpl extends UnicastRemoteObject implements Search { // load C/C++ shared library static { System.loadLibrary("rmilib"); } // constructor public SearchImpl() throws RemoteException { super(); } // public remotely-callable method that finds an entry public String findEntry(String w) { return findEntry0(w); } // native C++ method to actually look up an entry private native static String findEntry0(String w); } All this class does is load a shared library, that contains the C and C++ functions used to search the database. Calls to findEntry are simply passed through to a native method findEntry0. SearchImpl extends a class java.rmi.server.UnicastRemoteObject. This class provides some of the basic RMI functionality, such as support for remote object references. 3. Define a C Function That Searches a Database The C part of this application is a function that accesses some type of legacy database. For this application, a function called "func" searches a text file for a string. It then returns the first line of the file that contains the string. You can use a function like this to look up names in a phone directory. The function consults a text file "data.txt", which is the database. /* func.c */ #include #include #include /* database */ char* db = "data.txt"; /* process input -> output */ void func(char* in, char* out) { char inbuf[256]; int found = 0; FILE* fp; /* input string is empty */ if (in == NULL || !strcmp(in, "")) { strcpy(out, "*** not found ***"); return; } /* open database */ fp = fopen(db, "r"); if (fp == NULL) { strcpy(out, "*** database open error ***"); return; } /* search database and return first matching line */ while (fgets(inbuf, sizeof inbuf, fp) != NULL) { if (strstr(inbuf, in) != NULL) { found = 1; break; } } fclose(fp); /* found an entry, clip off newline */ if (found) { size_t len = strlen(inbuf); if (len >= 1 && inbuf[len - 1] == '\n') inbuf[len - 1] = 0; strcpy(out, inbuf); } else { strcpy(out, "*** not found ***"); } } 4. Define a C++ Wrapper / Java Native Method For the C Function After you define the C function, you define a C++ wrapper function for the C function. You might ask "why not use the C function directly as a native method callable from Java?" The reason for the C++ wrapper is that native methods implemented in C/C++ require a particular name or signature. If you're accessing legacy functions and data, you may not be able to change the name of existing functions. So another function is defined as a wrapper around the legacy function. This function has a name: Java_rmitest_SearchImpl_findEntry0 that corresponds to a Java native method: rmitest.SearchImpl.findEntry0 How do you know what name to give this wrapper function? You can use the "javah" tool to generate a declaration for the wrapper, by saying: javah -jni -o rmilib.cpp rmitest.SearchImpl This command generates a Java(tm) Native Interface (JNI) declaration for a native method. You don't need to use the javah tool in this application because the declaration of the native method, and its implementation, are provided here: // rmilib.cpp #include #include #include // C function that searches database extern "C" void func(char*, char*); // declaration for native method rmitest.SearchImpl.findEntry0() extern "C" { JNIEXPORT jstring JNICALL Java_rmitest_SearchImpl_findEntry0 (JNIEnv *env, jclass, jstring str) { char inbuf[256]; char outbuf[256]; // get the input string const char* s = env->GetStringUTFChars(str, NULL); if (s == NULL) return NULL; // copy it out to a char buffer strcpy(inbuf, s); env->ReleaseStringUTFChars(str, s); // call C function func(inbuf, outbuf); // format output for return return env->NewStringUTF(outbuf); } } This wrapper takes an input string and converts it to a C-style string (a sequence of bytes terminated by a null byte). The wrapper then calls the C legacy function and formats the string for return to the calling Java program. After the two C/C++ functions are compiled, they are grouped in a shared library. This makes the functions callable from a Java application. 5. Define a Server Program So far you've defined a remote interface and class, and a couple of C and C++ functions that issue remote object calls. But how do you make a remote object "do" something? The first step is to register a remote object instance. This means you create a remote object in a server and then call java.rmi.Naming.rebind to associate that object with a name. This process uses a registry program that can remember the name-object association. If an object is registered, a client program can call java.rmi.Naming.lookup with the associated name; lookup will then return the registered object. More precisely, lookup returns a stub to the remote object. Stubs are described in the section on clients later in the tip. The Java(tm) Development Kit has a program called "rmiregistry" that you use as a registry. You start this program before starting the RMI server and client. The rmiregistry program remembers name-object associations specified by the RMI server program. This allows an RMI client program to look up an association by name, and obtain a stub reference to a remote object. Here is the server program for the application. // Server.java package rmitest; import java.rmi.Naming; public class Server { public static void main(String args[]) { // install RMI security manager System.setSecurityManager(new SecurityManager()); // create a remote object and register it try { SearchImpl si = new SearchImpl(); Naming.rebind("searchobj", si); } catch (Exception e) { System.err.println(e); } } } Notice that the client program creates a remote object and registers it using the name "searchobj". It also installs a security manager. (The server program will need to do this too.) Without a security manager, the RMI class loader will not download classes from remote locations. The security manager protects against malicious operations by loaded classes. 6. Define a Client Program That Calls the Server The final piece you need to define is a client program. This program takes an input string that you specify and uses it to look up information in the database. Specifically, the client issues RMI calls to the findEntry method of the remote object that the server program registered. Recall that the server program registers a remote object using the name "searchobj". The client program uses Naming.lookup to look up the remote object; it also specifies the host name where the server is running: //localhost/searchobj The name "localhost" is a special name for the local machine with IP address 127.0.0.1 (another name for this is the "loopback" address). In this example, both the client and the server are running on the same machine. If you run the server on a different host, you would replace "localhost" with that host name. Here is the client program for the application. // Client.java package rmitest; import java.rmi.Naming; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Client { // do call on remote object public static String lookup(String str) { String t = null; try { String host = "//localhost/searchobj"; Search s = (Search)Naming.lookup(host); t = s.findEntry(str); } catch (Exception e) { System.err.println(e); } return t; } public static void main(String args[]) { JFrame frame = new JFrame("RMI Client"); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // set RMI security manager System.setSecurityManager(new SecurityManager()); // set up input and output areas final JTextField field = new JTextField(25); field.requestFocus(); final JLabel label = new JLabel(" "); // process input field.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { label.setText(lookup(field.getText())); field.requestFocus(); } }); // set up panels and so forth JPanel panel1 = new JPanel(); JPanel panel2 = new JPanel(); panel1.add(field); panel2.add(label); frame.getContentPane().add("North", panel1); frame.getContentPane().add("South", panel2); frame.pack(); frame.setVisible(true); } } The heart of this code is the three lines: String host = "//localhost/searchobj"; Search s = (Search)Naming.lookup(host); t = s.findEntry(str); The first two of these look up a remote object by name ("searchobj"). The last line does the database lookup using the remote object's stub. You might wonder whether the client "really" is operating on the remote object. In other words, does the client have local to itself the actual remote object? The answer is "no", and this gets at the heart of what's going on inside of RMI. In Part 2 of this tip, "Putting it All Together," you will run a tool called the RMI compiler (rmic). This tool generates a "stub" for the SearchImpl class. The stub exists on the server, but can be dynamically downloaded to the client. So when you run the client, it interacts with the remote object via the stub. It's the stub (not the object) that is responsible for formatting and transmitting method arguments ("marshalling") to the RMI system on the server. A "skeleton" class on the server side "unmarshals" this information and makes the actual call on the remote object. The process is reversed to transmit return values back to the client. In this example, the client side has available locally the class files Client.class and Search.class. The stub class SearchImpl_Stub.class is downloaded on demand from the server. The actual remote class SearchImpl.class is not downloaded to the client. So the remote object stays on the server, and the client interacts with it via the stub class instance. Another thing to keep in mind is the concept of a "codebase." This is where stubs are stored. In this application, a codebase is specified when you run the server. The specification looks like this: -Djava.rmi.server.codebase=http://localhost:2001/ When you register a remote object using Naming.rebind, a codebase for the object is recorded. When the object's stub needs to be downloaded, it can be found using the codebase. In this application, a simple HTTP server is used to serve up stub .class files from the codebase. This server uses port 2001 on localhost. Codebases have some relation to CLASSPATH settings. When rmiregistry tries to find a file, it looks at the CLASSPATH first and then the codebase. This particular application is configured so that the codebase settings must be observed for the application to work. Notice that like the client program, the server program installs a security manager to protect against malicious operations by loaded classes. Part 2. Putting It All Together Here's how to take the various pieces of the application that you defined and assemble them into a working application: 1. Create a base directory. This tip uses the name base. The directory might actually be something like: /usr/jones/rmibase on UNIX, or: c:\rmibase on Windows. If you're using Windows, replace all / with \, but leave http:// paths alone. 2. Create a directory structure under base: base/client base/client/rmitest base/server base/server/rmitest base/src base/examples base/examples/classServer 3. This tip assumes that JDK 1.2.x is installed in: /jdkbase 4. Copy all of the above source files to base/src. 5. Change directories to base/src, and compile the C legacy function by saying: cc -c func.c This compiles func.c to an object file (func.obj for Win32, or func.o for Solaris). If you're using a Windows C/C++ compiler like Borland C++, you might say: bcc32 -c -P- func.c func.c is a C function, not a C++ one, so use appropriate compiler options to specify C compilation. 6. Compile the C++ native function by saying: c++ -c -I/jdkbase/include -I/jdkbase/include/win32 rmilib.cpp where "c++" is your local C++ compiler. If you're using Solaris, replace "win32" with "solaris". This step also produces an object file (rmilib.obj or rmilib.o). 7. Create a shared library by saying: bcc32 -tWD rmilib.obj func.obj With Solaris you say: cc -G -o librmilib.so rmilib.o func.o In other words, you combine the two object files into a shared library. You should now have a library rmilib.dll (Win32) or librmilib.so (Solaris). Move this library to base/server. Note that there's a platform-dependent mapping between the library name specified to System.loadLibrary and an actual shared library name on your system. For example, loading a library named "abc" implies a name "abc.dll" for Win32, and "libabc.so" for Solaris. This application assumes that the current directory (".") is in the search path for loading shared libraries into a Java program. If you have trouble with this area, you might wish to change your "java.library.path" setting. This setting is forced in the server invocation below. 8. In base/server create a small text file called data.txt, with lines like this: Jane Jones 457-9231 Tom Garcia 143-5876 Bill Smith 456-8918 Separate the fields of each line with spaces. This is the legacy database that will be searched by the application. 9. Change directories to base/src, and say: javac Search.java SearchImpl.java Server.java Client.java 10. Copy: Search.class SearchImpl.class Server.class to base/server/rmitest. 11. Copy: Search.class Client.class Client$1.class Client$2.class to base/client/rmitest. 12. Change directories to base/server, and say: rmic -classpath . -d . rmitest.SearchImpl This step generates the SearchImpl_Skel.class and SearchImpl_Stub.class files. Part 3. Running the Application Here's what actions you need to take to run the application. 1. Create a desktop window and say: rmiregistry Do this from a directory positioned such that server/rmitest is not reachable via your CLASSPATH. In other words, start rmiregistry from somewhere unrelated to this application's source and .class files. This is done to avoid confusing CLASSPATH and codebase locations when searching for stub files. The rmiregistry program doesn't display or print anything. It just waits for registry requests. 2. Download the class server from: ftp://ftp.javasoft.com/pub/jdk1.1/rmi/class-server.zip This is a small download (5K). Unzip the files into the directory base/examples/classServer. Change directories to base/examples/classServer and say: javac ClassFileServer.java ClassServer.java 3. Run the class server in a new window by changing directories to base and saying: java examples.classServer.ClassFileServer 2001 base/server 2001 is the class server's port, and base/server is where the server looks for .class files. So the server will look for SearchImpl_Stub.class with the pathname base/server/rmitest/SearchImpl_Stub.class. 4. Run the RMI server in a window by changing directories to base/server and saying: java \ -Djava.library.path="." \ -Djava.security.policy=base/server/java.policy.server \ -Djava.rmi.server.codebase=http://localhost:2001/ \ rmitest.Server java.policy.server is a policy file in base/server. It's a text file that should contain these lines: grant { permission java.lang.RuntimePermission "loadLibrary.*"; permission java.util.PropertyPermission "user.dir", "read"; permission java.net.SocketPermission "*:1024-65535", "connect,accept"; }; The policy file specifies permissions used by the security manager installed in the server. 5. Run the client in a window by changing directories to base/client and saying: java \ -Djava.security.policy=base/client/java.policy.client \ rmitest.Client java.policy.client is a policy file in base/client. It's a text file that should contain these lines: grant { permission java.net.SocketPermission "*:1024-65535", "connect,accept"; }; 6. Enter a string into the input area of the client GUI. The application will use the string to search the legacy database. It will return and display the first line in the database that contains a match. Further Information There are a number of RMI papers available online. These four provide basic information about RMI, describe codebases, and answer common questions: http://java.sun.com/products/jdk/rmi/ http://java.sun.com/products/jdk/1.2/docs/guide/rmi/getstart.doc.html http://java.sun.com/products/jdk/1.2/docs/guide/rmi/faq.html http://java.sun.com/products/jdk/1.2/docs/guide/rmi/codebase.html This paper describes how to use the Java Native Interface to combine Java and C/C++ code: http://java.sun.com/docs/books/tutorial/native1.1/ . . . . . . . . . . . . . . . . . . . . . . . - NOTE The names on the JDC mailing list are used for internal Sun Microsystems(tm) purposes only. To remove your name from the list, see Subscribe/Unsubscribe below. - FEEDBACK Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@java.sun.com - SUBSCRIBE/UNSUBSCRIBE The JDC Tech Tips are sent to you because you elected to subscribe when you registered as a JDC member. To unsubscribe from JDC Email, go to the following address and enter the email address you wish to remove from the mailing list: http://developer.java.sun.com/unsubscribe.html To become a JDC member and subscribe to this newsletter go to: http://java.sun.com/jdc/ - ARCHIVES You'll find the JDC Tech Tips archives at: http://developer.java.sun.com/developer/TechTips/index.html - COPYRIGHT Copyright 1999 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://developer.java.sun.com/developer/copyright.html This issue of the JDC Tech Tips is written by Glen McCluskey. JDC Tech Tips December 14, 1999