These notes summarize an Augur Systems presentation given at a New England Java User Group meeting in 2000, with some later revisions. It's old, but hopefully still useful for any RMI programmers. (Let us know.)
RMI and Serialization go hand in hand. They are the tools that provide a common robust framework to obviate the need for home-grown socket network communications, and also provide a means for portable object persistence. Like all technologies that are deceivingly simple to use, RMI and Serialization provide a fair share of pitfalls and tough lessons to be learned. The following notes highlight some of the most important tips we have learned in our Java development experience. Some may be obvious to a Java veteran, but others may provide subtle insights. Comments or questions are welcomed.
As of Java 1.2, the RMI stub and skeleton requirements have changed (due to the availability of Java introspection). Skeletons are no longer used, and stubs have also been optimized. So save some jar-file space by using the "-v1.2" option when running the RMI Compiler, "rmic".
The LocateRegistry class has methods that will generate an RMI Registry thread on the given port within your current JVM. This is often more efficient and convenient than trying to manage a separate 'rmid' process running on the operating system.
While these two machine aliases may 'ping' the same, they represent different IP addresses, and will cause havoc with the RMI Registry. Always use the host name or IP address that is accessible to your intended clients! If your server's real address is hidden from your client by a NAT firewall then life will be harder for you... read on for some help.
The "java.rmi.server.hostname" system property can be set to define the hostname or IP address that RMI should use to identify the local host. This can be useful when a machine has more than one IP address.
Java has a nasty habit of always talking to the Domain Name Server, even when you've already provided an IP address. If there is no DNS, or if the particular IP address is not defined for a reverse lookup, then the system will hang until the lookup times-out... up to 5 minutes! Then, the RMI operation will continue like nothing was wrong (since it already has the IP address). This can lead to some annoying situations. Unfortunately it also affects remote clients that pass remote references of themselves to a server... this can be a real problem on the Internet where you have no control of another network's DNS.
[update 28-Sep-2001] This delay problem does not occur for clients when using IBM's JVM on the server side. Apparently, IBM's JVM is satisfied when receiving a remote client's IP address, while Sun's JVM insists on some additional lookup.
The DNS delay is further defined in Sun Bug# 4975882
Workaround: For my situation, I was able to reference my RMI objects by IP address only. This avoided the reverse lookup. (My customer was not able to get their DNS fixed due to some management policy.) Part of my solution included a custom RmiSocketFactory, which ensured that the client used the IP address for all communications. (RMI stubs sent to the client may have an unroutable address if the RMI server is behind a NAT firewall.) The custom factory is here, also mentioned below, in the section titled "Workaround..."
You can just instantiate the factory, then call setProxy() with the IP address.
For flexibility, my WebStart-based application has a JSP launch web page that grabs the host name used by the client to reach the server, and then passes this host info back to the WebStart client as a property in the JNLP file generated when the user selects a link on the JSP page. (Actually, my "*.jnlp" file on the web server is processed by the web server as a JSP file so the JSP code is in there... We just had to reconfigure the web server to treat the JNLP extension the same as the JSP extension.)
So, for all this to work correctly, the user has to load my JNLP file by referencing the public IP address of the web server. Then the web server grabs that IP address from the HTTP request, and makes it one of the properties in the JNLP XML data that is returned to the client. Then when the WebStart client starts up, it immediately instantiates the custom RmiSocketFactory, and calls setProxy() with the given IP address. From then on, any RMI calls will be sent via the IP address, no matter what the server stubs may contain. This works great for dealing with servers behind NAT, and for avoiding the delays associated with this bug. The only downside is that my customer is forced to type an IP address in their browser. (They could set up a local "hosts" lookup file, but that's equally painful, and shouldn't be required if this bug is fixed.) If the customer types a DNS-based name, my solution still gets around the NAT, but will suffer from the bug's delays if the DNS doesn't allow reverse lookups.
Note: the above solution assumes your client only needs to communicate with one RMI server. The factory class supports a Map lookup for multiple servers... read its Javadoc for more info.
Client pull via RMI is obvious... a client looks up a server object, and then makes remote calls. This is great, but there are some situations where server push makes more sense. This can be easily accomplished by extending the client class from UnicastRemoteObject, implementing a Remote, and generating a stub (and skeleton if using Java 1.1) just like any remote server object... except that you don't need to bind with an RMI Registry (difficult for an applet in a sandbox). The client can just pass its own reference (this) to the server, just like an AWT listener passes itself to an object via a addListener() method.
Whenever possible (i.e. within the same JVM), always use the instantiated Remote object reference rather than looking up the Remote stub in the RMI Registry. Stubs perform a lot of overhead, including socket connections and serialization. The original object reference is just like any other object in your JVM and can be very efficiently accessed. Although there will still be a socket server thread running in the JVM (created by UnicastRemoteObject's constructor), it is only used when you talk through the stub. $tubs == $ockets & $erialization
During development/testing your client may fail due to a dreaded NullPointerException, or some other exception that occurred on the server. RMI nicely packages that info for you without trashing the server, but the server-side debugging information is unaccessible. One easy trick is to just re-write your offending remote server method to catch the exception and print a stack trace, e.printStackTrace(). Then you'll have better info for pinpointing your server problem.
Network Address Translation (NAT) is a great tool for protecting local networks and conserving real IP addresses. Unfortunately, it plays havoc with client/server protocols, including RMI. You can think of an RMI server object (anything extending java.rmi.server.UnicastRemoteObject) as just a fancily wrapped java.net.ServerSocket; and a stub (java.rmi.server.RemoteStub) encapsulates the host and port information for a client application/applet to contact that server object. A stub instance gets generated by the server, which only knows it's local IP address. Usually the private side of a NAT router assigns ranges of local IP addresses which are "private" (unroutable on the Internet). So the client will receive a stub which points to an unroutable address. You can see that if the stub contains the private IP address of a machine, then the poor client will be trying to contact a private IP address which has no route back to the actual server machine. (You should look at a NAT tutorial if this is not clear.)
There's one very tricky (i.e. interesting, but very non-recommended) way around this problem: NAT routers can allow a particular private IP address to be "nailed" to the NAT router's public address (a trick often used to expose a service, like a web site, to the world). The server application's "java.rmi.server.hostname" Java property must be set (e.g. via the -D java command line option) to the public IP address of the NAT router, so that all generated stubs reference the accessible address rather than the server machine's actual private address (the default if the "java.rmi.server.hostname" property is not set). This is dangerous since your server is now completely exposed on the Internet, and if compromised, then exposes the rest of your private network. NAT does have the capability to map only particular port numbers to private server address. This can be leveraged when you only need to create a fixed number of RMI objects. The constructor for UnicastRemoteObject takes a port number, which defines where the server object will be bound. So if you configure NAT to map those port numbers correctly, your communications will succeed. Unfortunately, you may not always be able to control the one or more NAT firewalls in the way. You'll also be restricted from creating RMI objects dynamically since it would be very difficult to coordinate the number of objects and their port assignments with the NAT configuration. Creating open pools of port mappings through the firewall is usually discouraged for security reasons, and still limits the number of dynamic objects you can instantiate.
Usually server machines are not placed behind a NAT router for this very reason. So in this case, client-side applications/applets (even those running behind a NAT router) are usually ok... they will receive a stub (with a non-private, routable IP address) from the server to which it can initiate contact. Note that the reverse is still a problem: if the client application/applet tries to create an RMI server object (e.g. a "ClientIsStillHere" RMI object which responds to "pings" from the server machine), then the server machine will not be able to contact the RMI server object residing on the client machine (hidden by NAT). Worse yet, the application (both client and server-side) may temporarily freeze when the client passes it's stub to the server (i.e. via a safe client-to-server RMI call) if the server tries to resolve the name for the client stub's private IP address, which won't exist, and must time-out. This freeze seems to be especially long for Solaris servers.
So a general rule of thumb is to never create RMI server objects in client-side applications/applets, since it is very likely the client machines will be behind a NAT router. Unfortunately that means you can't do "push" communications from the server to the client (e.g. stock price alerts). All communication must be "pull", so the client may have to periodically poll the server for any new information. You may not see this problem while testing both the client and server code within your own intranet, but your external customers/friends will.
[Update August 30, 2006]
Workaround for when server objects are hidden behind NAT
Some research suggested creating a java.rmi.server.RMISocketFactory to work around the NAT firewall problem. The general idea is that a custom RMISocketFactory, installed on the client side, can implement a new version of the createSocket(String host, int port) method. This method is used by the client's RMI framework when it needs to contact the remote server object. Note that the host is passed as a parameter, presumably retrieved from the stub. The work-around idea is that you can ignore that host (which is probably the server's real address, but inaccessible directly), and replace it with the correct (NATed) host for the client side of the firewall.
[Update July 4, 2007]
Here's an implementation: ASIRmiSocketFactory.java You can call setProxy() with the name/address of the server, as accessible by the client. You can get this from the client session at run-time via Applet.getCodeBase().
Use these field modifiers to reduce serialization where possible... fields that have any one or more of these modifiers will not have their values serialized. For static or transient fields, their values will be set to their type's default value: null, 0, or 'false' for objects, numbers, or booleans, respectively. You can always override the object's readObject() method (remember to first call super.readObject()) to reinitialize any important values after serialization.
RMI does some extra work to make sure that the objects serialized between a client and server (or maybe stored/read from a file) are reconstructed using the exact same class, and version of that class. It verifies the version with a 'long' integer uniquely generated when the class is compiled. This can sometimes be a pain when you improved some code in a class but then are unable to reconstruct a stored class instance using the fields serialized in a file. Assuming you haven't changed any field definitions, you may really want to get your hands on that stored data, but Java will complain since the "serialVersionUID" number is now different. You can "trick" Java by explicitly defining the field: "static final long serialVersionUID" In fact, Java provided a utility in the JDK/SDK to help: 'serialver'. It will generate a proper number for you, but actually any number will do. If its too late (you've already compiled over the old version), just read the exceptions's error message... it will tell what version number it was expecting. Note that you still have to be somewhat careful about the changes you make. Changing or adding methods is harmless. Removing methods or changing method signatures will only cause problems if other classes reference those methods.
Adding new fields (a.k.a. members) to a class means that instantiating the serialized older version of the class will leave those new fields in their "default" state. The default for numbers is zero, for objects it's null, and booleans are false. This means your new class code has to expect the possibility of coming across one of these default values. To supply better default values, you can override the object's readObject() method (remember to call super.readObject()) to reinitialize any important values after serialization, or set your defaults in the class's default constructor (the one with no parameters) since it is called before field values are deserialized.
Removing fields from a class definition will probably cause an exception when the old class data is reconstructed for the new class?... Not tested.
Objects carry a lot more serialization overhead when serialized than primitive fields, so use primitives whenever possible.
Serialization within an open ObjectOutputStream is optimized to only serialize an object instance once. If it sees the same object within the stream later (maybe part of some reference chain), it will just write an index code that will not only save bytes, but will also prevent unnecessary object instantiation when read, and will also preserve reference chains.
The Serializable marker is a workhorse... it tells the JVM to do all the hard work for you when it comes to serialization, but at a price... both performance and size overhead. Externalizable is an alternative that gives you all the control (and all the headaches) associated with serializing the value of your object instances. The good news is that you can really optimize the process. A Java Developer Connection "Tech Tip" (in 2000) covered Externalization and showed a 30% increase in performance over Serialization.
Be very aware of exactly what you're serializing and what the reference bounds are... e.g. serializing a single tree node will actually serialize the whole tree, due to the references to parents and children within each node.
[January 17, 2012] ARMI (Asynchronous Remote Method Invocation) is our our own RMI alternative. It avoids NAT/firewall headaches, and adds asynchronous (publish/subscribe) capabilities too. It is free and open-source. Learn more.