RMI: Distributed Java For the Rest Of Us
J2EE seems to be the part of Java that receives the most press these days.
When you think J2EE, you tend to think in terms of costly
but cool
application
servers and large enterprises. But, what about the rest of us? What about the
small and medium sized businesses that need to do distributed processing and
either can't afford or don't want to spend the capital on brand-name app servers?
You know the type: guys like me who are looking for "good enough"
technologies Yes, we could use JBoss, and probably will at some point, but for
right now plain old Tomcat works great. It's is easy to set up, and it meets
our needs. "But Tomcat doesn't support EJB...and we need to do distributed
processing," you say. Well I say, "Fine. Let's use RMI to accomplish
our distributed tasks." In this article, I'll discuss a real-world example
using RMI and Tomcat to run a distributed application. The examples referenced
in this article were developed using Jbuilder 5 and Jbuilder 9, Sun's J2SE 1.4,
Tomcat 4.127, Microsoft Access, Red Hat Linux 7.3, and Microsoft Windows NT
4.0.
A Simple Client/Server Need
A few years ago a friend asked me to provide some technology consulting to
a company he owns. The company provides psychological evaluations for pre-employment
screening and job-role fit. My friend asked me to help them take their evaluation
testing service to the web. The first hurdle was to convert their current test
administration software from CBASIC running on DOS
yes I said DOS
to
Windows Visual Basic 6.0. I contracted a VB programmer who worked with the original
developer in converting the code. They ended up with a Windows based administration
module utilizing a Microsoft Access database and TCP/IP sockets for evaluation
processing.
With the administration module in place, we began reviewing the underlying
technologies needed for a website front-end. Like many small enterprises, the
company couldn't afford much in the way of web servers and application servers.
So, it didn't take long to see the value in using JSP and Tomcat.
The first iteration of their web-based system consisted of running Tomcat 3.3
on the same Windows NT 4.0 server handling the test administration. A simple
JavaBean was created to handle socket connections between the JSP client and
the VB server program.
Using Sockets in Java
As you may already know, sockets are an abstraction for network connections.
Java provides some relatively simple interfaces and classes for utilizing sockets.
Java's socket interfaces and class definitions are found in the java.net package.
The following example illustrates using a socket to connect to a server:
public void setprocessEval(boolean process) {
try {
// establish a socket connection
SocketPermission p = new SocketPermission(ipAddress + ":" + port + ","connect,resolve");
InetAddress address = InetAddress.getByName(ipAddress);
// receive a socket
Socket socket = new Socket(address, port);
// establish an OutputStream for writing data
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
PrintWriter pw = new PrintWriter(osw);
// write the information
pw.println(evaluationString.substring(0, evaluationString.length()));
pw.flush();
// establish an InputStream for reading the socket
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int c;
StringBuffer instr = new StringBuffer();
// read the data coming across the socket
while ((c=isr.read()) != -1) instr.append((char) c);
processEval = true;
}
catch (IOException f) {
processEval = false;
}
catch (Exception g) {
processEval = false;
}
}
The basic steps for using sockets are: establish a connection with a server,
receive a socket from the server, establish a stream for writing data, write
the data, and receive a response from the host system. As far as the port is
concerned, it is typically assigned by the server. The client needs to know
the port that the server is listening on. Unlike some other languages, VB for
example, there is no need to keep querying the host to determine whether all
the information has been sent/received. The underlying Java classes handle this
for you.
Back to The System
The VB administration program we talked about earlier processes evaluation
input, creates PDF evaluation reports, and stores the results in a MS Access
database. To view the results on-line, I built a secure area with then JSP site.
I used the JDBC-ODBC bridge provided with Java,(sun.jdbc.odbc.JdbcOdbcDriver),
to access the database. While it's not necessarily the most efficient way to
access ODBC databases, especially when there are several good commercial drivers
available, it does work...and remember this particular company didn't have much
money to invest in technology.
The Windows NT 4.0 server worked well for a year or so. But as the number of
customers using the website grew, it became apparent that the application server
needed to be separated from the administration server. After some technology
discussions with the company's board, a decision was made to pursue running
the entire process on Linux. I set up a Linux server for them and began the
process of moving the website to Linux.
An Introduction to Remote Method Invocation
Obviously, moving JSP from Windows to Linux is not a problem. The only challenge
I had was how to access the MS Access databases. I didn't have an ODBC administrator
on the Linux server
and really didn't want to set one up because the long-term
plan is to move the company away from Access. After looking over the possibilities,
I decided to use RMI (Remote Method Invocation) to handle data access between
the servers.
For the remainder of this article I'm going to show basic RMI concepts and
some code examples. For a more complete discussion of RMI you may want refer
Sun's white paper "Java Remote Method Invocation - Distributed Computing
for Java http://java.sun.com/marketing/collateral/javarmi.html
and Builder's tutorial "Getting Started with RMI".
Basically, the RMI model works as follows:

In the above example, class1 on the client need to call class2.method() from
the server. Using RMI, the client obtains an instance of class2_stub class.
The stub class (class2_stub) is automatically generated from the server class
(class2Impl). You use the rmic (RMI Compiler) command to create a stub class
(y the way JBuilder does a great job of managing this for you). Internally the
stub class creates a socket connection to the server or it can use a pre-existing
connection. It marshalls the information associated with method call and sends
this information to the server. The server demarshalls the data and makes the
method call on the actual server object. The server marshalls the return value
and sends it back to the client. As complicated as all this sounds, Java is
doing most of the work for you.
With RMI, you can pass values to and from remote methods either by reference
or by value depending on need. Values that are passed by reference extend the
"Remote" interface. You extend the Serializable interface to pass
objects by value. In the following examples, I'll be extending the Remote interface.
An Introduction to CachedRowSets
After determining that I would use RMI to communicate between the servers,
I needed to find a way to return a RowSet from a remote method. RowSets are
useful for returning scrollable result sets from a table. A RowSet can either
be connected or disconnected. To use RowSets you'll need to download the sun.jdbc.rowset
package from Sun.
If you are familiar with JDBC, you know that to work with a database you'll
need to do the following:
· Load and register a driver
· Establish a connection
· Create a statement
· Then do something like executeQuery().
Once you've done this, you then have an open connection to the database until
you close it. All of this works well if your program can establish a direct
connection to the database. But, what happens when you want to pass the RowSet
as remote object? You could parse the data and pass it as a vector or array
of objects or such. Or, you could use the CachedRowSet. CachedRowSet was introduced
as part of JDBC 2.0.
CachedRowSets are essentially the same as RowSets but they create a cache of
the data that can be passed around remotely. Think of CachedRowSets as disconnected
ResultSets. Big caution here, if you are planning on passing CachedRowSets you'll
need to think about how you want to pass data around because data = network
traffic = bandwidth...something you want to assess before actually doing it.
Getting back to my example, I now had a solution for accessing the MS Access
data from my Linux server. Basically I created a simple RMI server program that
receives an input string containing the SQL statement from the client and returns
a CachedRowSet from the server.
RMI Code Examples
The Client JavaServer Page
<%@ page contentType="text/html; charset=iso-8859-1" language="java" import="java.sql.*, java.rmi.*, javax.sql.*" %>
//CachedRowSets are beans and are instantiated as such
<jsp:useBean id="Results" class="sun.jdbc.rowset.CachedRowSet" scope="session">
</jsp:useBean>
<%
String sqlStmt = "select something ";
String serverIP = "some ip address ";
//establish a RMI connection with the server
getData gData = (getData)Naming.lookup("rmi://" + serverIp + "/getData");
RowSet Recordset1 = gData.callSql(sqlStmt);
While (!Recordset1.eof()){
Recordset1.next();
String someField = Recordset1.getString("someColumn");
/* some html and JSP stuff would follow
...
*/
}
Recordset1.close();
%>
The above example illustrates the basics of using a JavaServer page as an RMI
client that receives a CachedRowSet. While I've not included all of the HTML
components, this should give you an idea of CachedRowSets work. Notice that
I've treated the CachedRowSet like any other javabean (ie. using the jsp:usebean
tags). Pay attention also to the RMI connection. Following normal Java conventions,
the JSP instantiates the getData object. In order to create a new instance
of getData called gData you have to locate the getData
interface and getData_stub files in the classpath of the JSP. Once instantiated,
the client referenced the callSql() method passing the appropriate SQL
statement. When the RowSet is returned, the client JSP can then scroll through
the data, retrieving the necessary information.
The Server Interface:
import java.sql.*;
import javax.sql.*;
public interface getData extends java.rmi.Remote {
public RowSet callSql(String instring)
throws java.rmi.RemoteException;
}
Notice that the getData interface extends java.rmi.Remote. It defines
one public method callSql(), and because it extended java.rmi.Remote
the interface must throw a java.rmi.RemoteException.
The Server Implementation:
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.sql.*;
import javax.sql.*;
import sun.jdbc.rowset.*;
public class getDataImpl extends UnicastRemoteObject implements getData {
static Connection con;
static Statement stmt;
static String Connection;
static String Driver;
static String User;
static String Password;
static ResultSet rs;
static String hostIp;
public getDataImpl() throws RemoteException {
super();
}
public RowSet callSql(String s) throws RemoteException {
hostIp = "some IP address";
Driver = "sun.jdbc.odbc.JdbcOdbcDriver";
Connection = "jdbc:odbc:someDatabase";
User = "";
Password = "";
try {
CachedRowSet rset = new CachedRowSet();
// Load and register the driver
Class.forName(Driver);
// Create the connection
con = DriverManager.getConnection (Connection, User, Password);
// Create the Statement
stmt = con.createStatement();
// put the results of the executeQuery into the ResultSet object rs
rs = stmt.executeQuery(s);
// populate the CachedRowSet object rset with the data from the ResultSet object rs
rset.populate(rs);
// return to the client the CachedRowSet object rset
return(rset);
}
catch (ClassNotFoundException e) {
System.out.println(e.getMessage());
}
catch (SQLException e) {
System.out.println("SQL Msg1:" + e);
}
return(null);
}
public static void main(String[] arg) {
try {
getDataImpl gData = new getDataImpl();
Naming.rebind("rmi://"+ hostIp + "/getData",gData);
}
catch(Exception e) {
System.out.println("getDataImpl: " + e);
}
}
}
In getDataImpl, notice that under the main() method there is a Naming.Rebind()
method call. This method registers the RMI server program with the RMI Registry.
If you look back at the client code, you'll see that it calls the Naming.lookup()
method using the name "getData" that was registered by the server
program. The Naming.lookup() method invokes the RMI Registry on the server
(as indicated by the IP and/or DNS) and looks to see if "getData"
class is registered. Assuming a successful lookup, the client can now make method
calls. You'll notice that there is only one public method call in the getDataImpl
program
callSql(). Looking closely at the line the program, you
can see that the getData class expects a String and returns a RowSet RecordSet1.
The returned RowSet can then be queried using familiar methods: .next(), .getString(),
etc.
In Summary
I hope that the above examples have given you a taste of providing small scale
distributed processing with RMI. The investment you make in RMI will not be
lost if you decide to move to J2EE and EJB. RMI is a part of the J2EE spec and
understanding how it works will help when you decide to make the leap. The next
step for my friend's company is to convert his VB system to a Java system and
switch from MS Access to a more robust database. Now at least, he has a foundation
and some breathing room to grow as his business does.
Connect with Us