|
Running even the simplest remote object example requires quite a bit more setup than does running a standalone program or applet. You must run programs on both the server and client computers. The necessary object information must be separated into client-side interfaces and server-side implementations. Also, a special lookup mechanism allows the client to locate objects on the server. To get started with the actual coding, we walk through each of these requirements, using a simple example. In our first example, we generate a couple of objects of a type Product on the server computer. We then run a program on a client computer that locates and queries these objects. Interfaces and ImplementationsYour client program needs to manipulate server objects, but it doesn't actually have copies of them. The objects themselves reside on the server. The client code must still know what it can do with those objects. Their capabilities are expressed in an interface that is shared between the client and server and so resides simultaneously on both machines. interface Product // shared by client and server extends Remote { String getDescription() throws RemoteException; } Just as in this example, all interfaces for remote objects must extend the Remote interface defined in the java.rmi package. All the methods in those interfaces must also declare that they will throw a RemoteException. The reason for the declaration is that remote method calls are inherently less reliable than local callsit is always possible that a remote call will fail. For example, the server or the network connection may be temporarily unavailable, or there may be a network problem. Your client code must be prepared to deal with these possibilities. For these reasons, the Java programming language forces you to catch the RemoteException with every remote method call and to specify the appropriate action to take when the call does not succeed. The client accesses the server object through a stub that implements this interface. Product p = . . .; // see below how the client gets a stub reference String d = p.getDescription(); System.out.println(d); In the next section, you will see how the client can obtain a reference to this kind of remote object. Next, on the server side, you must implement the class that actually carries out the methods advertised in the remote interface. public class ProductImpl // server extends UnicastRemoteObject implements Product { public ProductImpl(String d) throws RemoteException { descr = d; } public String getDescription() throws RemoteException { return "I am a " + descr + ". Buy me!"; } private String descr; } NOTE
This class has a single method, geTDescription, that can be called from the remote client. You can tell that the class is a server for remote methods because it extends UnicastRemoteObject, which is a concrete Java platform class that makes objects remotely accessible. NOTE
Server classes generally extend the class RemoteServer from the java.rmi.server package, but RemoteServer is an abstract class that defines only the basic mechanisms for the communication between server objects and their remote stubs. The UnicastRemoteObject class that comes with RMI extends the RemoteServer abstract class and is concreteso you can use it without writing any code. The "path of least resistance" for a server class is to derive from UnicastRemoteObject, and all server classes in this chapter do so. Figure 5-5 shows the inheritance relationship between these classes. Figure 5-5. Fundamental RMI classesA UnicastRemoteObject object resides on a server. It must be alive when a service is requested and must be reachable through the TCP/IP protocol. This is the class that we extend for all the server classes in this book and is the only server class available in the current version of the RMI package. Sun or third-party vendors may, in the future, design other classes for use by servers for RMI. NOTE
When you use RMI (or any distributed object mechanism, for that matter), you will have to master a somewhat bewildering set of classes. In this chapter, we use a uniform naming convention for all of our examples that we hope makes it easier to recognize the purpose of each class (see Table 5-1).
Stub Class GenerationAs of JDK 5.0, all stub classes are generated automatically, using the proxy mechanism discussed in Volume 1, Chapter 6. However, before JDK 5.0, you had to manually generate stubs with the rmic tool, as in the following example. rmic -v1.2 ProductImpl This call to the rmic tool generates a class file ProductImpl_Stub.class. If you still use JDK 1.1, call rmic -v1.1 ProductImpl Two files are generated: the stub file and a second class file named ProductImpl_Skel.class. NOTE
Locating Server ObjectsTo access a remote object that exists on the server, the client needs a local stub object. How can the client request such a stub? The most common method is to call a remote method of another server object and get a stub object as a return value. There is, however, a chicken-and-egg problem here. The first server object has to be located some other way. The Sun RMI library provides a bootstrap registry service to locate the first server object. A server program registers objects with the bootstrap registry service, and the client retrieves stubs to those objects. You register a server object by giving the bootstrap registry service a reference to the object and a name. The name is a string that is (hopefully) unique. // server ProductImpl p1 = new ProductImpl("Blackwell Toaster"); Context namingContext = new InitialContext(); namingContext.bind("rmi:toaster", p1); The client code gets a stub to access that server object by specifying the server name and the object name in the following way: // client Product p = (Product) namingContext.lookup("rmi://yourserver.com/toaster"); RMI URLs start with rmi:// and are followed by a server, an optional port number, another slash, and the name of the remote object. Another example is: rmi://localhost:99/central_warehouse By default, the server is localhost and the port number is 1099. NOTE
For security reasons, an application can bind, unbind, or rebind registry object references only if it runs on the same host as the registry. This prevents hostile clients from changing the registry information. However, any client can look up objects. The RMI naming service is integrated into the Java Naming and Directory Information (JNDI) service. In JDK 1.3 and below, you use a standalone RMI naming service, like this: Naming.bind("toaster", p1); // on the server Product p = (Product) Naming.lookup("rmi://yourserver.com/toaster"); The code in Example 5-1 through Example 5-3 shows a complete server program that registers two Product objects under the names toaster and microwave. TIP
Example 5-1. ProductServer.java1. import java.rmi.*; 2. import java.rmi.server.*; 3. import javax.naming.*; 4. 5. /** 6. This server program instantiates two remote objects, 7. registers them with the naming service, and waits for 8. clients to invoke methods on the remote objects. 9. */ 10. public class ProductServer 11. { 12. public static void main(String args[]) 13. { 14. try 15. { 16. System.out.println("Constructing server implementations..."); 17. 18. ProductImpl p1 = new ProductImpl("Blackwell Toaster"); 19. ProductImpl p2 = new ProductImpl("ZapXpress Microwave Oven"); 20. 21. System.out.println("Binding server implementations to registry..."); 22. Context namingContext = new InitialContext(); 23. namingContext.bind("rmi:toaster", p1); 24. namingContext.bind("rmi:microwave", p2); 25. System.out.println("Waiting for invocations from clients..."); 26. } 27. catch (Exception e) 28. { 29. e.printStackTrace(); 30. } 31. } 32. } Example 5-2. ProductImpl.java1. import java.rmi.*; 2. import java.rmi.server.*; 3. 4. /** 5. This is the implementation class for the remote product 6. objects. 7. */ 8. public class ProductImpl 9. extends UnicastRemoteObject 10. implements Product 11. { 12. /** 13. Constructs a product implementation 14. @param n the product name 15. */ 16. public ProductImpl(String n) throws RemoteException 17. { 18. name = n; 19. } 20. 21. public String getDescription() throws RemoteException 22. { 23. return "I am a " + name + ". Buy me!"; 24. } 25. 26. private String name; 27. } Example 5-3. Product.java1. import java.rmi.*; 2. 3. /** 4. The interface for remote product objects. 5. */ 6. public interface Product extends Remote 7. { 8. /** 9. Gets the description of this product. 10. @return the product description 11. */ 12. String getDescription() throws RemoteException; 13. } Starting the ServerOur server program isn't quite ready to run yet. Because it uses the bootstrap RMI registry, that service must be available. To start the RMI registry under UNIX, execute the statement rmiregistry & Under Windows, call start rmiregistry at a DOS prompt or from the Run dialog box. (The start command is a Windows command that starts a program in a new window.) Now you are ready to start the server. Under UNIX, use the command: java ProductServer & Under Windows, use the command: start java ProductServer If you run the server program as java ProductServer then the program will never exit normally. This seems strangeafter all, the program just creates two objects and registers them. Actually, the main function does exit immediately after registration, as you would expect. However, when you create an object of a class that extends UnicastRemoteObject, a separate thread that keeps the program alive indefinitely is started. Thus, the program stays around to allow clients to connect to it. TIP
Listing Remote ObjectsBefore writing the client program, let's verify that we have succeeded in registering the remote objects. Call NamingEnumeration<NameClassPair> e = namingContext.list("rmi:"); to get an enumeration of all server objects with an rmi: URL. The enumeration yields objects of type NameClassPair, a helper class that contains both the name of the bound object and the name of its class. We only care about the names: while (e.hasMore()) System.out.println(e.next().getName()); Example 5-4 contains the complete program. The output should be toaster microwave Example 5-4. ShowBindings.java1. import java.rmi.*; 2. import java.rmi.server.*; 3. import javax.naming.*; 4. 5. /** 6. This programs shows all RMI bindings. 7. */ 8. public class ShowBindings 9. { 10. public static void main(String[] args) 11. { 12. try 13. { 14. Context namingContext = new InitialContext(); 15. NamingEnumeration<NameClassPair> e = namingContext.list("rmi:"); 16. while (e.hasMore()) 17. System.out.println(e.next().getName()); 18. } 19. catch (Exception e) 20. { 21. e.printStackTrace(); 22. } 23. } 24. } The Client SideNow, we can write the client program that asks each newly registered product object to print its description. Client programs that use RMI should install a security manager to control the activities of the dynamically loaded stubs. The RMISecurityManager is such a security manager. You install it with the instruction System.setSecurityManager(new RMISecurityManager()); NOTE
NOTE
Example 5-5 shows the complete client program. The client simply obtains references to two Product objects in the RMI registry and invokes the getdescription method on both objects. Example 5-5. ProductClient.java[View full width] 1. import java.rmi.*; 2. import java.rmi.server.*; 3. import javax.naming.*; 4. 5. /** 6. This program demonstrates how to call a remote method 7. on two objects that are located through the naming service. 8. */ 9. public class ProductClient 10. { 11. public static void main(String[] args) 12. { 13. System.setProperty("java.security.policy", "client.policy"); 14. System.setSecurityManager(new RMISecurityManager()); 15. String url = "rmi://localhost/"; 16. // change to "rmi://yourserver.com/" when server runs on remote machine yourserver.com 17. try 18. { 19. Context namingContext = new InitialContext(); 20. Product c1 = (Product) namingContext.lookup(url + "toaster"); 21. Product c2 = (Product) namingContext.lookup(url + "microwave"); 22. 23. System.out.println(c1.getDescription()); 24. System.out.println(c2.getDescription()); 25. } 26. catch (Exception e) 27. { 28. e.printStackTrace(); 29. } 30. } 31. } Running the ClientBy default, the RMISecurityManager restricts all code in the program from establishing network connections. However, the program needs to make network connections
NOTE
To allow the client to connect to the RMI registry and the server object, you supply a policy file. We discuss policy files in greater detail in Chapter 9. For now, just use and modify the samples that we supply. Here is a policy file that allows an application to make any network connection to a port with port number of at least 1024. (The RMI port is 1099 by default, and the server objects also use ports 1024.) grant { permission java.net.SocketPermission "*:1024-65535", "connect"; }; NOTE
In the client program, we instruct the security manager to read the policy file by setting the java.security.policy property to the file name. (We use the file client.policy in our example programs.) System.setProperty("java.security.policy", "client.policy"); Alternatively, you can specify the system property setting on the command line: java -Djava.security.policy=client.policy ProductClient NOTE
If the RMI registry and server are still running, you can proceed to run the client. Or, if you want to start from scratch, kill the RMI registry and the server. Then follow these steps:
The program simply prints I am a Blackwell Toaster. Buy me! I am a ZapXpress Microwave Oven. Buy me! This output doesn't seem all that impressive, but consider what goes on behind the scenes when the client program executes the call to the getdescription method. The client program has a reference to a stub object that it obtained from the lookup method. It calls the geTDescription method, which sends a network message to a receiver object on the server side. The receiver object invokes the getdescription method on the ProductImpl object located on the server. That method computes a string. The receiver sends that string across the network. The stub receives it and returns it as the result (see Figure 5-6). javax.naming.InitialContext 1.3 Figure 5-6. Calling the remote getDescription method
javax.naming.Context 1.3
javax.naming.NamingEnumeration<T> 1.3
javax.naming.NameClassPair 1.3
java.rmi.Naming 1.1
Preparing for DeploymentDeploying an application that uses RMI can be tricky because so many things can go wrong and the error messages that you get when something does go wrong are so poor. We have found that it really pays off to stage the deployment locally. In this preparatory step, separate the class files into three subdirectories: server download client The server directory contains all files that are needed to run the server. You will later move these files to the machine running the server process. In our example, the server directory contains the following files: server/ ProductServer.class ProductImpl.class Product.class CAUTION
The client directory contains the files that are needed to start the client. These are client/ ProductClient.class Product.class client.policy You will deploy these files on the client computer. Finally, the download directory contains those class files needed by the RMI registry, the client, and the server, as well as the classes they depend on. In our example, the download directory looks like this: download/ Product.class If your clients run JDK 1.4 or lower, also supply the stub classes (such as ProductImpl_Stub.class). If the server runs JDK 1.1, then you need to supply the skeleton classes (such as ProductImpl_Skel.class). You will later place these files on a web server. CAUTION
Now you have all class files partitioned correctly, and you can test that they can all be loaded. You need to run a web server to serve the class files on your computer. If you don't have a web server installed, download Tomcat from http://jakarta.apache.org/tomcat and install it. Make a directory tomcat/webapps/download, where tomcat is the base directory of your Tomcat installation. Make a subdirectory tomcat/webapps/download/WEB-INF, and place the following minimal web.xml file inside the WEB-INF subdirectory: <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> </web-app> Then copy the class files from the download staging directory into the directory tomcat/webapps/download. Next, edit the client.policy file. It must give the client these permissions:
Change the file to look like this: grant { permission java.net.SocketPermission "*:1024-65535", "connect"; permission java.net.SocketPermission "*:80", "connect"; }; Finally, you are ready to test your setup.
If both the server and the client started up without a hitch, then you are ready to go to the next step and deploy the classes on a separate client and server. If not, you need to do some more tweaking. TIP
Deploying the ProgramNow that you have tested the deployment of your program, you are ready to distribute it onto the actual clients and servers. Move the classes in the download directory to the web server. Make sure to use that URL when starting the server. Move the classes in the server directory onto your server and start the RMI registry and the server. Your server setup is now finalized, but you need to make two changes in the client. First, edit the policy file and replace * with your server name: grant { permission java.net.SocketPermission "yourserver.com:1024-65535", "connect"; permission java.net.SocketPermission "yourserver.com:80", "connect"; }; Finally, replace localhost in the RMI URL of the client program with the actual server. String url = "rmi://yourserver.com/"; Product c1 = (Product) namingContext.lookup(url + "toaster"); . . . Then, recompile the client and try it. If everything works, then congratulations are in order. If not, you may find the sidebar checklist helpful. It lists a number of problems that can commonly arise when you are trying to get RMI to work. TIP
Logging RMI ActivityThe Sun RMI implementation is instrumented to produce logging messages, using the standard Java logging API. (See Volume 1, Chapter 11 for more information on logging.) To see the logging in action, make a file logging.properties with the following content: handlers=java.util.logging.ConsoleHandler .level=FINEST java.util.logging.ConsoleHandler.level=FINEST java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter You can fine-tune the settings by setting individual levels for each logger rather than setting the global level to FINEST. Table 5-2 lists the RMI loggers.
Start the RMI registry with the option
Start the client and server with
It is best to start the RMI registry, client, and server in different windows. Alternatively, you can log the messages to a filesee Volume 1, Chapter 11 for instructions. Here is an example of a logging message that shows a class loading problem: The RMI registry cannot find the Product class. It needs it to build a dynamic proxy.
|
|