Friday, March 17, 2017

My Notes on the Java RMI and Static Ports

[I wrote this 9/23/2008 with Java 1.6, but it's probably still accurate]

Java Remote Method Invocation (RMI) is a technology that makes it very easy to implement server client architectures. Java RMI lets you to make method calls to remote objects without having the client treat the remote object very differently from a local object. I've been using RMI a fair amount recently, so I thought I'd go over some of the more interesting things I've learned. Primarily, how it works and how to control the ports it uses.

A Brief Overview about how RMI Works

First there are the "Stubs."  Stubs are essentially specialized proxies to a specific server. Internally, they hold the IP and Port to a server. They know how to make requests to the server and how to process the response from that server. They're Java objects and are used exactly the same as a local object once you've obtained the stub.

Then there's the "RMI Registry."  The RMI Registry is a server that stores name to stub matchings. All its clients know the IP and Port number for the Registry. The clients contact the registry to get stub objects for servers whose IP and Port number they don't necessarily know. The Registry listens on port 1099 unless configured otherwise.

Finally, there's a server run for each stub that's registered in the RMI Registry.

And so the following happens when RMI is used:
  1. The RMI Registry that stores name to stub pairings is created.
  2. Whenever a new name and stub is bound in the Registry, a matching server is created (usually on a dynamic port) for the stub to communicate with.
  3. The client knows the IP and Port of an RMI Registry. The client uses that IP and Port to ask the registry for the stub matching a certain name.
  4. When a client calls a method on the stub, the stub contacts the matching server to process the request. The matching server processes the response and returns a result to the stub. The stub in turn processes the response and returns a value as if the method was called locally.
This really is a brief overview, for a lot more detail about the specifics, give http://www.developer.com/java/ent/article.php/10933_3455311_1 a thorough read. One thing not mentioned is that RMI is multi-threaded. I'm not entirely sure about the following point, but I think that a new thread is automatically created for every stub request. I don't know of anyway to make RMI non multi-threaded (in case you can't have every client sending information to the same JVM.)

But You Don't Care How It Works, do you?

The great thing about RMI is that the programmer doesn't really have to worry about the network communications at all. They just need to get a Java object (the stub) from the Registry and make method calls on it. The stub, skeleton, and the registry do all the network stuff for them. So all a programmer really cares about is implementing RMI. For that information, Java's own tutorial is probably the best source. So go and read it at http://java.sun.com/docs/books/tutorial/rmi/server.html. Really, the rest of this article won't make sense without knowing how to use RMI.

Stop Randomly Choosing Ports!!!

For the project I was working on, it was important to control the IP and the Ports RMI used. I was using Solaris 10 with Trusted Extensions and wanted Cross Domain capabilities, which required me to explicitly identify certain ports as "Multi Level Ports." You can see how dynamically assigning ports to RMI servers was no good for me. Furthermore, I was using several different IP Addresses on the machine and did not want RMI to to accept connections on all of them. So what this really came down to was controlling the sockets that are normally dynamically created by the Registry and RMI Servers.

I wrote my own RMIServerSocketFactory to gain control over the sockets in RMI. It is as follows:
CustomSocketFactory.java

view plaincopy to clipboardprint?
  1. ///////////////////////////////////////////////////////////////////////////  
  2. //  
  3. // CustomSocketFactory - This class is used to provide client and server  
  4. //   sockets with configurable ports and ip addresses.  By default,  
  5. //   RMISocketFactory chooses these dynamically.  Note that the backlog  
  6. //   for ServerSockets is hardcoded.  
  7. //  
  8. ///////////////////////////////////////////////////////////////////////////  
  9.   
  10. import java.io.IOException;  
  11. import java.io.Serializable;  
  12. import java.net.InetAddress;  
  13. import java.net.ServerSocket;  
  14. import java.net.Socket;  
  15. import java.rmi.server.RMISocketFactory;  
  16.   
  17. /** 
  18.  * CustomSocketFactory 
  19.  */  
  20. public class CustomSocketFactory extends RMISocketFactory implements Serializable  
  21. {  
  22.     /** 
  23.      * The InetAddress to be used for the Sockets this factory serves. 
  24.      */  
  25.     private InetAddress ipInterface = null;  
  26.   
  27.     /** 
  28.      * The port to be used with Sockets this factory serves.  By 
  29.      * default this value is set to -1 (which is an impossible port), 
  30.      * which tells the factory to allow the port be be dynamically determined. 
  31.      */  
  32.     private int port = -1;  
  33.   
  34.     /** 
  35.      * Default Constructor.  IP and Port will be dynamically assigned. 
  36.      */  
  37.     public CustomSocketFactory() {}  
  38.   
  39.     /** 
  40.      * Constructor that specifies the INetAddress to use.  The port will still 
  41.      * be served dynamically. 
  42.      * 
  43.      * @param ipInterface The InetAddress to be associated with Sockets served by 
  44.      * this factory. 
  45.      */  
  46.     public CustomSocketFactory(InetAddress ipInterface)  
  47.     {  
  48.         this.ipInterface = ipInterface;  
  49.     }  
  50.   
  51.     /** 
  52.      * Constructor that specifies the INetAddres and port to use. 
  53.      * 
  54.      * @param ipInterface The INetAddress to be associated with Sockets served by 
  55.      * this factory. 
  56.      * @param port The port to be associated with Sockets served by this 
  57.      * factory. 
  58.      */  
  59.     public CustomSocketFactory(InetAddress ipInterface, int port)  
  60.     {  
  61.         this.ipInterface = ipInterface;  
  62.         this.port = port;  
  63.     }  
  64.   
  65.     /** 
  66.      * Returns a ServerSocket with the ip address and port used dependant 
  67.      * on the constructor used. 
  68.      * 
  69.      * @param port The dynamic port to be used for the socket served.  This 
  70.      * is not used if the constructor specifying a port was used 
  71.      * for this factory. 
  72.      * @return A socket. 
  73.      */  
  74.     public ServerSocket createServerSocket(int port) throws IOException  
  75.     {  
  76.         ServerSocket serverSocket = null;  
  77.   
  78.         //Decides whether or not to use the supplied port  
  79.         ifthis.port != -1 )  
  80.         {  
  81.             port = this.port;  
  82.         }  
  83.   
  84.         try  
  85.         {  
  86.             //NOTE: The backlog is hardcoded to 50 here.  This was  
  87.             //  done for no particular reason and could be dynamically  
  88.             //  set by making new method calls.  
  89.             serverSocket = new ServerSocket(port, 50, ipInterface);  
  90.         } catch( IOException e ) {  
  91.             throw e;  
  92.         }  
  93.   
  94.         return serverSocket;  
  95.     }  
  96.   
  97.     /** 
  98.      * Returns a Socket with the ip address and port used dependant 
  99.      * on the constructor used. 
  100.      * 
  101.      * @param port The dynamic port to be used for the socket served.  This 
  102.      * is not used if the constructor specifying a port was used 
  103.      * for this factory. 
  104.      * @param dummy This INetAddress is not used. 
  105.      * @throws Exception Throws for any exception. 
  106.      * @return A socket. 
  107.      */  
  108.     public Socket createSocket( String dummy, int port) throws IOException  
  109.     {  
  110.         ifthis.port != -1 )  
  111.         {  
  112.             port = this.port;  
  113.         }  
  114.   
  115.         return new Socket( ipInterface, port );  
  116.     }  
  117.   
  118.     /** 
  119.      * Returns true if the object passed in is identical to this object. 
  120.      * 
  121.      * @param that The object to be compared with this object. 
  122.      * @return True if equal, otherwise false. 
  123.      */  
  124.     public boolean equals( Object that )  
  125.     {  
  126.         return (that != null && that.getClass() == this.getClass() );  
  127.     }  
  128. }  
Now I simply specified that SocketFactory when I created the RMI Registry and the stubs/servers! Here's a very simple example:

AddServerInterface.java
view plaincopy to clipboardprint?
  1. import java.rmi.Remote;  
  2. import java.rmi.RemoteException;  
  3.   
  4. public interface AddServerInterface extends Remote  
  5. {  
  6.     public int add( int a, int b ) throws RemoteException;  
  7. }  
  1. import java.net.InetAddress;  
  2. import java.rmi.RemoteException;  
  3. import java.rmi.registry.LocateRegistry;  
  4. import java.rmi.registry.Registry;  
  5. import java.rmi.server.UnicastRemoteObject;  
  6.   
  7. public class AddServer implements AddServerInterface  
  8. {  
  9.     public AddServer() throws Exception  
  10.     {        
  11.         //Set up the RMI.  Use hard coded values for this example.  
  12.         String registryIp = "10.10.1.1";  
  13.         int registryPort = 3645;  
  14.         String serverIp = "10.10.1.1";  
  15.         int serverPort = 3646;  
  16.           
  17.         //Create the Add Server, supplying the Socket Factories to assign our IP and Port.  
  18.         CustomSocketFactory clientSocketFactory = new CustomSocketFactory( InetAddress.getByName(serverIp), serverPort );          
  19.         CustomSocketFactory serverSocketFactory = new CustomSocketFactory( InetAddress.getByName(serverIp), serverPort );  
  20.         AddServerInterface stub = (AddServerInterface)UnicastRemoteObject.exportObject(this,serverPort,clientSocketFactory,serverSocketFactory);  
  21.           
  22.         //Get or Create the RMI registry.  
  23.         Registry registry = null;          
  24.         try  
  25.         {  
  26.             //Create Registry  
  27.             CustomSocketFactory sf = new CustomSocketFactory( InetAddress.getByName(registryIp), registryPort );  
  28.             registry = LocateRegistry.createRegistry( registryPort, null, sf );  
  29.         }  
  30.         catch( RemoteException re )  
  31.         {  
  32.             //Registry was already created, so just connect  
  33.             registry = LocateRegistry.getRegistry(registryPort);    
  34.         }  
  35.           
  36.         //Bind the Add Server to the registry  
  37.         registry.rebind( "AddServer", stub );   
  38.     }  
  39.       
  40.     public int add( int a, int b ) throws RemoteException  
  41.     {  
  42.         return a+b;  
  43.     }  
  44.       
  45.     public static void main( String args[] ) throws Exception  
  46.     {  
  47.         try  
  48.         {              
  49.             AddServer addServer = new AddServer();  
  50.         } catch ( Exception e ) {  
  51.             System.out.println( "Main error: " );  
  52.             e.printStackTrace();  
  53.         }  
  54.     }  
  55. }  
So now the registry listens on 10.10.1.1/3645 and the Add Server listens on 10.10.1.1/3646. It works like a charm. Go ahead, try an example client:

  1. //Required for RMI Support  
  2. import java.net.InetAddress;  
  3. import java.rmi.Remote;  
  4. import java.rmi.registry.LocateRegistry;  
  5. import java.rmi.registry.Registry;  
  6.   
  7. public class AddClient  
  8. {  
  9.     public AddClient() {}  
  10.       
  11.     public static void Main( String args[] )  
  12.     {  
  13.         try   
  14.         {          
  15.            //Connect to the add server.    
  16.            Registry registry = LocateRegistry.getRegistry( "10.10.1.1"3645 );  
  17.            Remote remote = registry.lookup( "AddServer" );  
  18.            if( remote == null )  
  19.            {  
  20.               System.out.println( "Could not find it" );  
  21.            }  
  22.            AddServerInterface addServer = (AddServerInterface)remote;  
  23.            
  24.        System.out.println( "1 + 1 = " + addServer.add( 11 ) );             
  25.         } catch ( Exception e ) {  
  26.            e.printStackTrace();  
  27.         }          
  28.     }  
  29. }  

No comments: