Skip to main content

The New RMI

October 6, 2005

{cs.r.title}









Contents
RMI Security
   Security Overview
   Securing RMI
RMI as Service
Stubless RMI
Conclusion
Resources

Java Remote Method
Invocation
(RMI) introduced a powerful mechanism for distributing
application logic across different machines. Instead of having to perform tasks in one monolithic central
system, RMI made it possible to build
modular and manageable applications where the computing logic could be
distributed. RMI has been the primary communication mechanism for various
server-side component architectures, including Enterprise JavaBeans
(EJB). Introduced with Java 1.1, RMI has been steadily
evolving with every major release and has seen the introduction of three
new important features with the release of Java 5.0. The new features
include support for dynamic stub generation, RMI over SSL, and the
ability to launch a Java RMI sever as an extended Internet service
(xinetd) daemon in Unix systems. In this article we will
cover these additions.

RMI Security

The default RMI communication mechanism—the Java Remote Method
Protocol
(JRMP)—is not secure. It is possible to secure
the communication by writing custom socket factories using Java Secure
Socket Extension (JSSE). But this approach puts the burden of writing additional code
on the developers, who must take care of securing data exchange using
cryptography. Java 5.0 alleviates this issue by introducing two new
classes, javax.rmi.ssl.SslRMIClientSocketFactory and
javax.rmi.ssl.SslRMIServerSocketFactory, that provide the
ability to secure the communication channel between the client and the
server using the Secure Sockets Layer (SSL)/Transport Layer Security
(TLS) protocols. These socket factory classes provide a simple and
elegant way to use JSSE for secure Java RMI communication, which enables
enforcement of data integrity, data confidentiality (through
encryption), server authentication, and (optionally) client
authentication for remote method invocations. This means developers can
focus on the business logic of the distributed application instead of
dealing with security-related plumbing.

In this section we will go over the details of using the new href="http://en.wikipedia.org/wiki/Secure_Sockets_Layer">SSL socket
factories for RMI communication. But before we delve into
details, we will provide a brief introduction on transport level
security. Secure Sockets Layer (SSL) is the most widely used protocol
for implementing cryptography over a distributed communication
protocol such as HTTP. The primary purpose of SSL is to provide privacy,
data integrity, authenticity, and non-repudiation. This is achieved by
using symmetric key cryptography for data encryption between the client
and server, and asymmetric key cryptography (or public/private key
cryptography) to authenticate the identities of communicating parties as
well as to encrypt the shared encryption key that is used during
establishing SSL session.

Security
Overview

A client and server that are about to exchange information via the
SSL protocol first establish an SSL session after exchanging a series of
messages, a process known as an SSL handshake. This is a multi-step
process, so we will take a look at a few important messages that are
sent between the client and server. The messages in the following
sections are from the SSL debug output from the sample application that
is provided along with this article (see href="#resources">Resources). The client starts the handshake
process by sending a "Hello" message to the server with a list of cipher
suites that it supports. This message is as follows:

*** ClientHello, TLSv1
RandomCookie: GMT: 1101964544 bytes = { 99,...,232 }
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5,
SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA,
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, ...
]

The server responds with a "Hello" message along with a decision on
the cipher suite and the compression method. In this particular case, the
server chose SSL_RSA_WITH_RC4_128_MD5 for
cipher method and 0 for compression method.

*** ServerHello, TLSv1
RandomCookie:  GMT: 1101964544 bytes = { 78,...,209}
Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
Compression Method: 0
***
%% Created:  [Session-1, SSL_RSA_WITH_RC4_128_MD5]
** SSL_RSA_WITH_RC4_128_MD5

What this means is that the client-server communication is going to
perform the following:

  • Use RSA (Rivest Shamir Adleman) public/private key
    cryptography for key exchange.
  • Use RC4 (Rivest Cipher Version 4) with 128-bit
    encryption for data exchange.
  • Use MD5 (Message Digest Version 5) for hashing to
    ensure message integrity during the exchange.

After sending the "Hello" message, the server sends its certificate and
the client verifies it. The certificate that is sent to the client is served
up from the server's keystore, which is
essentially a flat file that serves as a database for the contents, such
as key pairs and their associated certificates or certificate chains.
The client that receives the certificate from the server will pursue
establishing the SSL connection only when it can authenticate the
server. This trust can be established only when the client can verify
the digital signature on the server's certificate. This is possible when
the client trusts the server or it trusts at least one of the signers in
the certificate chain provided by the server. This trust assertion is
based on the certificates present in the truststore that
the client is configured with. Essentially, truststore is a
database of certificates or certificate chains that the client can trust.
The default truststore is cacerts, which can be found in
$JAVA_HOME\jre\lib\security.

Once the server certificate verification process is complete, the
client uses the public key of the server to send a
ClientKeyExchange message to the server. This message
contains some random information that will be used for the generation of
a symmetric key. This key will be used for encrypting the
content during data exchange. The server checks to see if any other client
already uses the symmetric key; if so, it will ask the client to
regenerate another random key.

*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
Random Secret:  { 3, 1, 59, 151, 102, 56, 204, 6,
249, 100, 25, 171, 9, 221, 105, 97, 0, 106, 77,
10, 180, 237, 222, 165, 9, 116, 216, 10, 181, 54,
32, 244, 46, 158, 73, 18, 17, 249, 32, 254, 10,
249, 196, 196, 185, 139, 70, 17 }

Once the client and server agree on a symmetric key, the client sends
a ChangeCipherSpec message indicating that it is now ready
to communicate; this message is followed by a Finished
message. The server responds by sending its
ChangeCipherSpec message and a Finished
message:

main, WRITE: TLSv1 Change Cipher Spec, length = 1
[Raw write]: length = 6
0000: 14 03 01 00 01 01
*** Finished

The setup configuration that an application developer needs to provide
for the above client-server exchange is to specify a
keystore for the server application and a
truststore for the client application. We will go
over how to create a keystore and a truststore and once we are done with
infrastructure-related aspects, we will write RMI code and integrate it
in order to establish secure communication. The keystore and truststore
can be created using the keytool utility that is bundled
with the JDK distribution. This utility can be found under
$JAVA_HOME/bin. If the JDK's bin
directory is in the PATH, then the keytool utility can be
executed from a command window or shell.

  • First, we will generate a keystore that has a key pair (public
    and private key) along with a self-signed certificate. This is
    accomplished by executing the following command, which will create a
    file called Server_Keystore. The key algorithm is RSA, and the
    keysize is 1024 bits.

    $keytool -genkey -alias SecureServer
               -keyalg RSA -keystore Server_Keystore
    Enter keystore password:  password
    What is your first and last name?
      [Unknown]:  kv
    What is the name of your organizational unit?
      [Unknown]:  IT
    What is the name of your organization?
      [Unknown]:  ABC
    What is the name of your City or Locality?
      [Unknown]:  KC
    What is the name of your State or Province?
      [Unknown]:  MO
    What is the two-letter country code for this unit?
      [Unknown]:  US
    Is CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US correct?
      [no]:  y

    Enter key password for <SecureServer>
        (RETURN if same as keystore password):
           
  • Next, we will examine the contents of the generated Server Keystore, which is accomplished by the following command.
    $keytool -list -v  -keystore Server_Keystore

    Enter keystore password:  password

    Keystore type: jks
    Keystore provider: SUN

    Your keystore contains 1 entry

    Alias name: secureserver
    Creation date: Aug 12, 2005
    Entry type: keyEntry
    Certificate chain length: 1
    Certificate[1]:
    Owner: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
          until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
      MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:
            B2:43:CB
      SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:
            C0:DD:51:4F:B0:E8:97

    *******************************************
    *******************************************

    As it can be seen from the output of the above command that there is a
    "keyEntry" which is the private key for corresponding self signed
    certificate.
  • We are done creating a key entry for the server, so we
    will shift our focus to the client side. The next step is to create a
    self-signed certificate and this is accomplished by executing the
    following commands.

    $keytool -export -alias SecureServer -keystore
                 Server_Keystore -rfc -file Server.cer
    Enter keystore password:  password
    Certificate stored in file <Server.cer>

    Just to see what the certificate looks like, we'll print to the console with the following:

    $cat Server.cer

    This will print a byte string that starts with -----BEGIN CERTIFICATE----- and ends with -----END CERTIFICATE-----

  • Now that we have created a self-signed certificate, the next logical
    step is to import this certificate into a truststore, which then can be
    used by the client. This is accomplished by the following command.

    $keytool -import -alias SecureServer -file Server.cer \
            -keystore Client_Truststore
    Enter keystore password:  passsword
    Owner: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
          until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
       MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:
             B2:43:CB
       SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:
             C0:DD:51:4F:B0:E8:97
    Trust this certificate? [no]:  y
    Certificate was added to keystore
  • To verify the contents of the truststore that we created, we issue
    the following command. As can be seen from the output, the contents
    of the truststore contain a trustedCertEntry, which means
    that a private key is not available and should not be.

    keytool -list -v  -keystore Client_Truststore
    Enter keystore password:  password

    Keystore type: jks
    Keystore provider: SUN

    Your keystore contains 1 entry

    Alias name: secureserver
    Creation date: Aug 12, 2005
    Entry type: trustedCertEntry

    Owner: CN=kv, OU=IT, O=ABC, L=Kansas City, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=Kansas City, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
        until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
      MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:B2:43:CB
      SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:C0:DD:51:4F:B0:E8:97

    *******************************************
    *******************************************

In this particular case, we are working with a self-signed
certificate instead of certificates signed by Certification Authority
(CA). If there is a need to get the certificate signed by a CA then a
Certificate Signing Request(CSR) needs to be generated. The generated
CSR, then, should to be submitted along with other pertinent information
to a Certification Authority such as VeriSign or USPS, who will then
digitally sign the certificate. The aspects of creating a CSR and
getting it signed by a CA is beyond the scope of this article.







Securing RMI

We have covered all the security-related aspects. Now we will go
about writing a simple secure server and a client. The server in this
case is a simplified version of an RMI service that performs credit card
authorization for the client. Of course, in the real world this will involve
an EDI or Web service call to a credit card authorization gateway, made
available by banking providers. In our case, we will just mimic this
transaction by performing a simple href="http://en.wikipedia.org/wiki/Luhn_formula">mod 10 check and,
if the check passes, we will return a good return-code; otherwise, we will
return a failure return-code. This simple exercise proves the point that
we need to secure the transaction because of sensitive data
exchange.

The class diagram in Figure 1 provides a high-level view of the
classes involved and follows the standard RMI programming semantics.

Typical RMI programming arrangement
Figure 1. Typical RMI programming arrangement

The CreditCardAuthorizer describes the core business
operation and also extends the java.rmi.Remote interface.
The CreditCardAuthImpl provides an implementation for the
business method authorizeCreditCard(). In addition, there
is a bind() method, which takes care of the RMI-specific
plumbing. Three important things happen in bind(). First,
the remote object is exported, so it can receive incoming calls using a
transport that is specified by the socket factory parameters. Next, a
Registry object is created on the specified port. Finally, the remote
object is bound to the registry. As can be seen, a standard RMI
programming paradigm is followed, and the only difference is the
exportObject() method, where the SSL version of the client
and server socket factory are used as method parameters.

javax.rmi.ssl.SslRMIClientSocketFactory;
javax.rmi.ssl.SslRMIServerSocketFactory;

....
RMIClientSocketFactory rmiClientSocketFactory =
                    new SslRMIClientSocketFactory();
RMIServerSocketFactory rmiServerSockeyFactory =
                    new SslRMIServerSocketFactory();

CreditCardAuthorizer ccAuth = (CreditCardAuthorizer)
      UnicastRemoteObject.exportObject(this, 0,
      rmiClientSocketFactory, rmiServerSockeyFactory);
Registry registry = LocateRegistry.createRegistry(2004);
registry.rebind(name, ccAuth);

Having covered the key programming aspects, the next
logical step is to test the server and the client. This is where the
keystore and truststore will be used. The server needs to use the
keystore, and this can be accomplished in either of two ways: It
can be specified as a command-line argument to the JVM, or the System
property can be explicit in the application code. The following code
snippets demonstrate both approaches.

  • Setting Keystore - Programmatically:
    System.setProperty("javax.net.ssl.keyStore",
    "./resources/Server_Keystore");
    System.setProperty("javax.net.ssl.keyStorePassword", "password");
  • Setting Keystore - Command Line:
    java
    -Djavax.net.ssl.keyStore=./resources/Server_Keystore
    -Djavax.net.ssl.keyStorePassword=password
    com.article.jn.securermi.CreditCardAuthServer
  • Setting Truststore - Programmatically:
    System.setProperty("javax.net.ssl.trustStore",
    "./resources/Client_Truststore");
  • Setting Truststore - Command Line:
    java
    -Djavax.net.ssl.trustStore= ./resources/Client_Truststore

The client specifies the truststore in a similar fashion, except
that with the truststore there is no need to specify the truststore
password.

That's it! Now we can start the server and then start the client to
watch the transaction run in a secure manner. To get a complete
idea of what is happening under the hood, we could turn the
SSL debug on and watch the SSL messages exchange between the server and the
client, including some of the messages presented earlier in this
article. This can be accomplishing by passing
-Djavax.net.debug=all as a JVM parameter.

RMI as Service

In systems like Linux or FreeBSD, Internet services such as FTP and
Telnet are set up to listen in on standard, well-known ports. For
instance, FTP uses port 20 and 21, and remote access using the TELNET
protocol is established over port 23. Any incoming TCP connection request
targeted at one of these well-known port numbers is understood to be a
request for a particular service. In order to establish a Telnet
session to a remote host, the client requires a hostname lookup to find
the remote IP address and then implicitly connect to that host's
standard telnet port: 23.

In the operating systems that are hosting the service, a program
called Internet Daemon—inetd—is started at boot time.
This program takes a list of services it has to manage from a startup
configuration file. The daemon creates sockets on behalf of the
services and listens in on all of them simultaneously. When an incoming
connection is requested for any of these sockets, inetd
accepts the connection and spawns the service as a child process,
passing the socket to it. inetd, also called the "super
server," returns to listening in on the socket for new connections after
performing the task. However, inetd has a few shortcomings. It is unable to: police access control on services,
prevent denial of service attacks, perform detailed
logging, and distinguish virtual hosts. This has resulted
in the development of a better version of Internet Service deamon as a replacement for inetd:
xinetd (eXtended inetd).

With the release of Java 5.0, it is possible to expose Java services
via the (x)inetd deamon and start Java RMI services on
demand. To make this happen, an application must use specific
programming semantics to ensure that the application and its constituent
services can be started from xinetd. In this section, we
will build a simple Date server that is based on the sample echo server
that ships with the JDK distribution and is documented in the
API documentation. This server is diagrammed in Figure 2.

alt="Class diagram for Date server" />

Figure 2. Class diagram
for Date server

The DateServer follows typical RMI-related programming
semantics: The DateService interface specifies the business
methods, and the DateServer provides implementation details.
However. in this case, since the server process will be started by the
inetd process, setting up the registry will use the new
Java 5.0 feature.

The DateServer delegates setting up the RMI Registry to
the initializeWithInheritedChannel() method of the
InitializeRegistry object. At a high level, this method
obtains a channel that was inherited from the entity that spawned the
Java VM, and then creates a registry where the remote proxy is bound for
the clients to look up. The following code snippet demonstrates how to
obtain the server socket that is inherited from the process that
launched this virtual machine:

Channel channel = System.inheritedChannel();
ServerSocket serverSocket = null;

if (channel instanceof ServerSocketChannel) {
    serverSocket =
      ((ServerSocketChannel) channel).socket();
}

As the code snippet shows, the static
inheritedChannel() method of the System class
enables the code to obtain the Channel that was inherited
from the process that spawned the Java service. This method returns an
instance of java.nio.channels.SocketChannel or
java.nio.channels.ServerSocketChannel, depending on how
xinetd is configured. A SocketChannel is used
to service a single incoming connection, and a
ServerSocketChannel is used to service multiple incoming
connections. We will discuss the xinetd configuration
later.

Now that we have an instance of a server socket, the next step is to
create a registry and bind the remote object to the registry. This is
accomplished as follows:

RMIServerSocketFactory ssf =
    new RegistryServerSocketFactory(serverSocket);
Registry registry =
   LocateRegistry.createRegistry(port, null, ssf);
try {
    registry.bind(name, proxy);
} catch (...) {...}

The registry that is created listens for incoming requests on a given
port using an instance of ServerSocket created from the
custom RMIServerSocketFactory. This custom server
socket factory creates and returns an instance of
DelayedAcceptServerSocket, which is a subclass of
ServerSocket. The core idea in doing this is to ensure that
this instance blocks while accepting requests from the inherited
ServerSocketChannel until the specified proxy is completely
bound in the registry. This is accomplished by overriding the
accept() method of the
DelayedAcceptServerSocket. This method blocks until the
thread that binds the proxy to the registry notifies the blocked thread
that it is okay to accept the incoming socket request. Essentially, this
operation prevents the clients from getting a
java.rmi.NotBoundException while trying to look up a proxy
that is not yet bound or may be in the process of binding to the
registry.

The following code snippet demonstrates the overridden
accept() method that blocks until the thread completes
the bind, which is provided in the code snippet that follows:

public Socket accept() throws IOException {
  synchronized (lock) {
    try {
       while (!serviceAvailable) {
        lock.wait();

       }
    }catch (InterruptedException e) {
       throw (IOException)
        (new InterruptedIOException()).initCause(e);
    }
  }
  return serverSocket.accept();
}

Here, once the bind operation finishes successfully, all the blocked threads are notified:

Registry registry =
LocateRegistry.createRegistry(port, null, ssf);
try {
   registry.bind(name, proxy);
} catch (...){
  ...
}

synchronized (lock) {
serviceAvailable = true;
  lock.notifyAll();

}







Now that we have defined our RMI code, the next step in the process
is to set it up as an RMI Service. This can be accomplished two ways:
editing the appropriate xinetd/inetd configuration files
directly, or using a GUI editor that is used for manipulating the
configuration files, which is typically supplied along with the OS. The
GUI tool that is bundled along is dependent on the flavor of Unix or
Linux in use. In this particular case, we will be using YaST,
which is bundled with Novell's SuSE Linux.

Figure 3 shows the SuSE Linux Control Center, and we are interested in
Network Services.

YaST Control<br />
Center GUI

Figure 3. YaST Control Center GUI

Opening "Network Services" will result in the configuration screen
shown in Figures 4 and 5, which enables you to add, edit, or delete
network services, as well as turn services on or off individually.

alt="YaST list of services" />

Figure 4. YaST list of services

height="385" alt="Editing a service in YaST" />

Figure 5. Editing a service in YaST

We add our RMI Service by providing details for the following
fields.

  • Service name - The name of a valid service.
  • Socket type - The choices are stream,
    dgram, raw, rdm (reliably
    delivered message), or seqpacket (sequenced packet
    socket).
  • Protocol - A protocol listed in /etc/protocols, which is
    some type of network protocol such as IP, ICMP, TCP, and UDP.
  • Flags - wait/nowait - Wait applies to
    datagram sockets only. All other socket types should have the "nowait"
    option in this entry. Nowait entries are used for multithreaded servers
    that free their sockets after each request so they can continue receiving
    more requests on the same socket.
  • User - The name of the user the server will run as.
  • Group - Specify the group so that the server can run with a
    different group ID than the one specified in the password file for that
    user.
  • Server - The path and name of the program to be
    executed.
  • Server Arguments - Command-line arguments for the
    server program that is being run.
  • Comment - Any relevant comment that describes what this
    service does.

After entering appropriate data and clicking the Accept button, a
file called rmi-date-server will be created in the
/etc/xinetd.d directory. The content of this file looks
like:

#RMI Date Server service rmi-date-server
{
   socket_type  = stream
   protocol     = tcp
   wait         = no
   user         = root
   group        = users
   server       = /opt/jdk1.5.0_01/bin/java
   server_args  = -Djava.rmi.server.hostname=192.168.0.2
    -classpath /home/Article/RMI/bin
                        com.article.jn.service.DateServer
}

Next, the rmi-date-server needs to be listed as a
service in the /etc/services configuration file. This is
accomplished by editing (using any text editor) the file to add the
following entry. Note that editing this file will require root
access. The format is:

rmi-date-server <port>/<protocol>,

In this entry, port is the port number for the service's
local registry.

Now that the configuration of xinetd has been modified,
the service needs to be restarted to read the new configuration changes.
To do this, send the HUP (hangup) signal to the
xinetd/inetd process. This is accomplished by determining
the process ID with following command:

$ ps -ef | grep xinetd
root 2332 1 0 Jun 30 ? 0:02 /usr/sbin/xinetd -s

In this case, the process ID for xinetd is 2332. Now, to
send the hangup signal for the xinetd process, issue the
following command:

$su root
$ kill -HUP 2332

Now xinetd is all set to launch the Java 5 RMI service
when a client attempts to connect to the port configured in the
/etc/services file. To test this out, we can run a simple
client that does the following:

Registry registry =
                LocateRegistry.getRegistry(host, port);
DateService proxy =  (DateService)
                registry.lookup("ServiceInterface");
System.out.println("received message from proxy: "
                                     + proxy.getDate());

This will result in an output to the client's standard out. As Figure
6 confirms, a java process is spawned by xinetd, which
listens in on a specified port (9900 in this case) for client
connections.

alt="Active listening ports" />

Figure 6. Active listening ports

Stubless RMI

One of the best features of the new Java 5.0 RMI is that static
generation of stubs using rmic is not required
anymore. This feat is accomplished by the dynamic generation of stub
classes at runtime. However, if the client is not running Java 5.0, then
rmic must still be used to pre-generate stub classes for
remote objects. Dynamic stub generation is made possible by two
different changes that were part of previous JDK releases. First, the
revision of the JRMP protocol in Java 2 enabled RMI to work without
skeletons. Second, the introduction of Dynamic Proxies in Java 1.3
obviated the need for client-side stubs.

The href="http://java.sun.com/j2se/1.5.0/docs/guide/rmi/relnotes.html">JDK
release notes provide the following explanation for stubless RMI:
When an application exports a remote object (using the constructors or
the static exportObject methods of the classes
java.rmi.server.UnicastRemoteObject or
java.rmi.activation.Activatable) and a pre-generated stub
class for the remote object's class cannot be loaded, the remote
object's stub will be a java.lang.reflect.Proxy instance
(whose class is dynamically generated) with a
java.rmi.server.RemoteObjectInvocationHandler as its
invocation handler. An existing application can be deployed to use
dynamically generated stub classes whether or not pre-generated stub
classes exist by setting the system property
java.rmi.server.ignoreStubClasses to "true".
If this property is set to "true," then pre-generated stub classes are never
used.

Conclusion

We zipped through the three most important and powerful features of
RMI in Java 5.0. SSL security makes RMI client-server communication
truly secure; the stubless RMI makes deployment a breeze; and the
ability to expose RMI applications as a Unix service allows RMI services
to be launched on demand.

Over a period of ten years, RMI has evolved incrementally to become
a robust distributed computing technology. These additions will
push forth RMI as the de facto solution when there is a need for
developing distributed, loosely-coupled, object-oriented
applications.

Resources

width="1" height="1" border="0" alt=" " />
Krishnan Viswanath is currently working for JPMorgan Chase & Co. in Kansas City, MO.