|
December 14, 1999 This issue presents tips, techniques, and sample code for the following topics: This issue of the JDC Tech Tips is written by Glen McCluskey.
|
// 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 <stdio.h>
#include <stddef.h>
#include <string.h>
/* 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
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 <stdio.h>
#include <string.h>
#include <jni.h>
// 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 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 server program creates a remote object and registers it using the name "searchobj". It also installs a security manager. (The client 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 server program, the client program installs a security manager to protect against malicious operations by loaded classes.
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.
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.
Note: This tip works only in 1.2.2
Download the RMI Code from this Tech Tip
There are a number of RMI papers available online. These four provide basic information about RMI, describe codebases, and answer common questions:
RMI
Get Started Doc
FAQ
Code Base
This paper describes how to use the Java Native Interface to combine Java and C/C++ code:
Note
The names on the JDC mailing list are used for internal Sun Microsystems 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
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:
|
| ||||||||||||