Contents
Most applications will make some use of RMI (Remote Method Invocation). There are several ways in which this may be done. In addition, it is possible to avoid use of RMI proxies completely. This chapter looks at such RMI-related issues.
In the option 3 example of ``Simple Examples'', a proxy of our design was created. However, when it came to communicating back to the original service, a remote method call using RMI was used, and for this the proxy used an RMI stub (or RMI proxy) for the implementation. Thus we ended up with two proxies on the client!
This section investigates one of the two possibilities in reducing the number of proxies, and that is to reuse the RMI proxy as the only service proxy. To do this, export the implementation only. The Java runtime will then ensure that the exported object will in fact be the RMI proxy for that service, leaving the original service behind. There is then no need for an explicit proxy at all!
A version of the option 3 file classifier to do this is
package rmi;
import option3.FileClassifierImpl;
import option3.RemoteFileClassifier;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import java.rmi.RMISecurityManager;
/**
* FileClassifierServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.1
* added LeaseRenewalManager
* moved sleep() from constructor to main()
*/
public class FileClassifierServerRMI implements DiscoveryListener, LeaseListener {
protected FileClassifierImpl impl;
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new FileClassifierServerRMI();
// no need to keep server alive, RMI will do that
}
public FileClassifierServerRMI() {
try {
impl = new FileClassifierImpl();
} catch(Exception e) {
System.err.println("New impl: " + e.toString());
System.exit(1);
}
// install suitable security manager
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
RemoteFileClassifier service;
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
// export the proxy service
ServiceItem item = new ServiceItem(null,
impl,
null);
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.print("Register exception: ");
e.printStackTrace();
// System.exit(2);
continue;
}
try {
System.out.println("service registered at " +
registrar.getLocator().getHost());
} catch(Exception e) {
}
leaseManager.renewFor(reg.getLease(), Lease.FOREVER, this);
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServerRMI
The option 3 service and the last section exported an RMI proxy object directly. They were able to do this because the Jini protocol allows objects to be moved around to their destination, where the ``lookup'' to find the object has been done by the Jini discovery protocol. In more ``traditional' uses of RMI, a client wishing to use an RMI service has to first locate it using RMI lookup mechanisms. This is possible with Jini also, but it is a little more complex and probably not so useful.
An RMI exportable object can advertise itself in various ways, and
Jini registration is only one of these. RMI comes with a simple lookup
service, the Naming
service. This uses methods such as
Naming.bind()
and Naming.rebind()
to register a
name for the RMI object in an RMI registry, and Naming.lookup()
to retrieve it from there.
An RMI registry is started by the command rmiregistry
. This
registry must be run on the same machine as a service
trying to bind to it.
So if there are many services running on many machines, then there will
be many RMI registries running, one per machine.
A server will create an RMI object and then register it with its local
RMI naming service.
It will also still need to export a service
registration to the Jini lookup service. So this method will export
two services: an RMI service to an RMI naming registry, and
a Jini service to a Jini lookup service.
On the other side, a client wishing to locate an object using this method will still need to find the Jini service, and will follow the methods of option 3. When it gets its Jini proxy, it will then need to locate its RMI proxy to make calls on it. But this time, it has not carried the RMI proxy along, and this must be found from the RMI registry. In order to use this, it must know the internet address of the registry's machine. By the time it has reached the client it is too late to have this information, so the Jini proxy must be ``primed'' with the address of the registry and the name of the RMI service while it is still in the service and before it is exported to the Jini lookup service.
The implementation of the service remains unaltered as the
option3.FileClassifierImpl
.
The server must bind this implementation into the rmi naming registry
using bind()
or rebind()
. It must also prime the
proxy with the address of this rmi registry, and the name the implementation
is bound to.
package rmi;
import java.rmi.Naming;
import java.net.InetAddress;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import java.rmi.RMISecurityManager;
/**
* FileClassifierServerNaming.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.1
* added LeaseRenewalManager
* moved sleep() from constructor to main()
*/
public class FileClassifierServerNaming implements DiscoveryListener, LeaseListener {
// This is just a name for the service
// It can be anything, just needs to shared by both
// ends of the Naming service
static final String serviceName = "FileClassifier";
protected option3.FileClassifierImpl impl;
protected FileClassifierNamingProxy proxy;
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new FileClassifierServerNaming();
// no need to keep server alive, RMI will do that
}
public FileClassifierServerNaming() {
try {
impl = new option3.FileClassifierImpl();
} catch(Exception e) {
System.err.println("New impl: " + e.toString());
System.exit(1);
}
// register this with RMI registry
System.setSecurityManager(new RMISecurityManager());
try {
Naming.rebind("rmi://localhost/" + serviceName, impl);
} catch(java.net.MalformedURLException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
} catch(java.rmi.RemoteException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
}
System.out.println("bound");
// find where we are running
String address = null;
try {
address = InetAddress.getLocalHost().getHostName();
} catch(java.net.UnknownHostException e) {
System.err.println("Address: " + e.toString());
System.exit(1);
}
String registeredName = "//" + address + "/" + serviceName;
// make a proxy that knows the service address
proxy = new FileClassifierNamingProxy(registeredName);
// now continue as before
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
// export the proxy service
ServiceItem item = new ServiceItem(null,
proxy,
null);
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.print("Register exception: ");
e.printStackTrace();
// System.exit(2);
continue;
}
try {
System.out.println("service registered at " +
registrar.getLocator().getHost());
} catch(Exception e) {
}
leaseManager.renewFor(reg.getLease(), Lease.FOREVER, this);
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServerNaming
The proxy which uses the naming service can still continue to use the
``option 3'' implementation. The way it finds this is different, as it
uses the naming service lookup()
method.
package rmi;
import common.FileClassifier;
import common.MIMEType;
import option3.RemoteFileClassifier;
import java.io.Serializable;
import java.io.IOException;
import java.rmi.Naming;
/**
* FileClassifierNamingProxy
*
*
* Created: Thu Mar 18 14:32:32 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class FileClassifierNamingProxy implements FileClassifier, Serializable {
protected String serviceLocation;
transient RemoteFileClassifier server = null;
public FileClassifierNamingProxy(String serviceLocation) {
this.serviceLocation = serviceLocation;
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException {
stream.defaultReadObject();
try {
Object obj = Naming.lookup(serviceLocation);
server = (RemoteFileClassifier) obj;
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
}
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException {
return server.getMIMEType(fileName);
}
} // FileClassifierNamingProxy
The Jini service (or a proxy for the service) runs locally to the client. In the examples so far given it makes use of an RMI proxy object (itself or another object) to make calls back to the server it came from. But RMI is only one of many possible protocols that can be used to communicate between the client and the server. Any other protocol can be used, depending on the problem domain. An easy alternative is to use a direct socket connection from the service component running in the client to the service. This opens up a number of possibilities, including
We shall give an example of this last type. Recently an Australian, Pat Farmer,
attempted to set a world record for jogging the longest distance. While he was
running around, I became involved in a project to broadcast his heart beart live
to the Web: a heart monitor was attached to him, which talked on an RS232
link to a mobile he was carrying. This did a data transfer to a program
running at www.micromed.com.au
which forwarded the data to a machine at DSTC. This ran a Web
server delivering an applet, and the applet talked back to a server on this
DSTC machine which farmed out the data to each applet as it was received
from the heart monitor.
Now that the experiment is over, the broadcast data is sitting as a file at http://www.micromed.com.au/patfarmer/v2/patfhr.ecg , and it can be viewed on the applet from http://www.micromed.com.au/patfarmer/v2/heart.html. We can make it into a Jini service as follows
Name
entry
The heart monitor service can be regarded in a number of ways:
The Heart
interface only has one method, and that is to show()
the heart trace in some manner.
/**
* Heart.java
*
*
* Created: Thu Jul 15 16:11:56 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package heart;
public interface Heart extends java.io.Serializable {
public void show();
} // Heart
The HeartServer
is similar to the ``option 2'' method, where it exports
a complete service. This service, of type HeartImpl
is primed with a
URL of where the heart data is stored. This data will later be delivered by an HTTP
server. This implementation is enough to locate the service. However, rather than
just anyone's heart data, a client may wish to search for a particular persons data.
This can be done by adding a Name
entry as additional information
to the service.
package heart;
import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.Name;
/**
* HeartServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class HeartServer implements DiscoveryListener,
LeaseListener {
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new HeartServer();
// keep server running forever to
// - allow time for locator discovery and
// - keep re-registering the lease
try {
Thread.currentThread().sleep(Lease.FOREVER);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public HeartServer() {
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
ServiceItem item = new ServiceItem(null,
// new HeartImpl("file:/home/jan/projects/jini/doc/heart/TECG3.ecg"),
new HeartImpl("http://www.micromed.com.au/patfarmer/v2/patfhr.ecg"),
new Entry[] {new Name("Pat Farmer")});
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.println("Register exception: " + e.toString());
}
System.out.println("service registered");
// set lease renewal in place
leaseManager.renewUntil(reg.getLease(), Lease.FOREVER, this);
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // HeartServer
The client searches for a service implementing the Heart
interface,
with the additional requirement that it be for a particular person. Once it has
this, it just calls the method show()
to display this in some manner.
package heart;
import heart.Heart;
import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.Name;
/**
* HeartClient.java
*
*
* Created: Wed Mar 17 14:29:15 1999
*
* @author Jan Newmarch
* @version 1.3
* moved sleep() from constructor to main()
* moved to package client
* simplified Class.forName to Class.class
*/
public class HeartClient implements DiscoveryListener {
public static void main(String argv[]) {
new HeartClient();
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(1000000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public HeartClient() {
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class [] classes = new Class[] {Heart.class};
Entry [] entries = new Entry[] {new Name("Pat Farmer")};
Heart heart = null;
ServiceTemplate template = new ServiceTemplate(null, classes,
entries);
for (int n = 0; n < registrars.length; n++) {
System.out.println("Service found");
ServiceRegistrar registrar = registrars[n];
try {
heart = (Heart) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(2);
}
if (heart == null) {
System.out.println("Heart null");
continue;
}
heart.show();
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // HeartClient
The HeartImpl
class opens a connection to an HTTP server and requests
delivery of a file. For heart data it needs to display this at a reasonable rate,
so it reads, draws, sleeps, in a loop. It acts as a fat client to the HTTP server,
displaying the data in a suitable format (in this case, it uses HTTP as a transport
mechanism for data delivery).
As a ``client-aware'' service it customises this delivery to the characteristics
of the client platform, just occupying a ``reasonable'' amount of screen space
and using local colors and fonts.
/**
* HeartImpl.java
*
*
* Created: Thu Jul 15 15:57:47 1999
*
* @author Jan Newmarch
* @version 1.0
*/
package heart;
import java.io.*;
import java.net.*;
import java.awt.*;
public class HeartImpl implements Heart {
protected String url;
/*
* If we want to run it standalone we can use this
*/
public static void main(String argv[]) {
HeartImpl impl =
new HeartImpl("file:/home/jan/projects/jini/doc/heart/TECG3.ecg");
impl.show();
}
public HeartImpl(String u) {
url = u;
}
double[] points = null;
Painter painter = null;
String heartRate = "--";
public void setHeartRate(int rate) {
if (rate > 20 && rate <= 250) {
heartRate = "Heart Rate: " + rate;
} else {
heartRate = "Heart Rate: --";
}
// ? ask for repaint?
}
public void quit(Exception e, String s) {
System.err.println(s);
e.printStackTrace();
System.exit(1);
}
public void show() {
int SAMPLE_SIZE = 300 / Toolkit.getDefaultToolkit().
getScreenResolution();
Dimension size = Toolkit.getDefaultToolkit().
getScreenSize();
int width = (int) size.getWidth();
// capture points in an array, for redrawing in app if needed
points = new double[width * SAMPLE_SIZE];
for (int n = 0; n < width; n++) {
points[n] = -1;
}
URL dataUrl = null;
InputStream in = null;
try {
dataUrl = new URL(url);
in = dataUrl.openStream();
} catch (Exception ex) {
quit(ex, "connecting to ECG server");
return;
}
Frame frame = new Frame("Heart monitor");
frame.setSize((int) size.getWidth()/2, (int) size.getHeight()/2);
try {
painter = new Painter(this, frame, in);
painter.start();
} catch (Exception ex) {
quit(ex, "fetching data from ECG server");
return;
}
frame.setVisible(true);
}
/*
public void paint(Graphics g) {
if (points == null)
return;
int SAMPLE_SIZE = 300 / Toolkit.getDefaultToolkit().
getScreenResolution();
g.setColor(Color.red);
int min = 127;
int max = -128;
for (int n = 0; n < points.length; n++) {
int x = n + 1;
int magnitude = points[n];
if (magnitude == -1) {
return;
}
if (x % SAMPLE_SIZE == 0) {
// draw only on multiples of sample size
// Data is in the range -128 .. 127. Need to
// draw so -128 is at the bottom, 127 at the top
int x0 = x / SAMPLE_SIZE;
g.drawLine(x0, min, x0, max);
min = 127;
max = -128;
} else {
if (magnitude > max) max = magnitude;
if (magnitude < min) min = magnitude;
}
}
}
*/
} // HeartImpl
class Painter extends Thread {
static final int DEFAULT_SLEEP_TIME = 25; // milliseconds
static final int CLEAR_AHEAD = 15;
static final int MAX = 255;
static final int MIN = 0;
final int READ_SIZE = 10;
protected HeartImpl app;
protected Frame frame;
protected InputStream in;
protected final int RESOLUTION = Toolkit.getDefaultToolkit().
getScreenResolution();
protected final int UNITS_PER_INCH = 125;
protected final int SAMPLE_SIZE = 300 / RESOLUTION;
protected int sleepTime = DEFAULT_SLEEP_TIME;
public Painter(HeartImpl app, Frame frame, InputStream in) throws Exception {
this.app = app;
this.frame = frame;
this.in = in;
}
public void run() {
while (!frame.isVisible()) {
try {
Thread.sleep(1000);
} catch(Exception e) {
// ignore
}
}
int height = frame.getSize().height;
int width = frame.getSize().width;
int x = 1; // start at 1 rather than 0 to avoid drawing initial line
// from -128 .. 127
int magnitude;
int nread;
int max = MIN; // top bound of magnitude
int min = MAX; // bottom bound of magnitude
int oldMax = MAX + 1;
byte[] data = new byte[READ_SIZE];
Graphics g = frame.getGraphics();
g.setColor(Color.red);
try {
Font f = new Font("Serif", Font.BOLD, 20);
g.setFont(f);
} catch (Exception ex) {
// ....
}
try {
boolean expectHR = false; // true ==> next byte is heartrate
while ((nread = in.read(data)) != -1) {
for (int n = 0; n < nread; n++) {
int thisByte = data[n] & 0xFF;
if (expectHR) {
expectHR = false;
app.setHeartRate(thisByte);
continue;
} else if (thisByte == 255) {
expectHR = true;
continue;
}
// we are reading bytes, from -127..128
// conver to unsigned
magnitude = thisByte;
// then convert to correct scale
magnitude -= 128;
// scale and convert to window coord from the top downwards
int y = ((128 - magnitude) * RESOLUTION) / UNITS_PER_INCH;
app.points[x] = y;
// draw only on multiples of sample size
if (x % SAMPLE_SIZE == 0) {
// delay to draw at a reasonable rate
Thread.sleep(sleepTime);
int x0 = x / SAMPLE_SIZE;
g.clearRect(x0, 0, CLEAR_AHEAD, height);
if (oldMax != MAX + 1) {
g.drawLine(x0-1, oldMax, x0, min);
}
g.drawLine(x0, min, x0, max);
oldMax = max;
min = 1000;
max = -1000;
if (app.heartRate != null) {
g.setColor(Color.black);
g.clearRect(0, 180, 200, 100);
g.drawString(app.heartRate, 0, 220);
g.setColor(Color.red);
}
} else {
if (y > max) max = y;
if (y < min) min = y;
}
if (++x >= width * SAMPLE_SIZE) {
x = 0;
}
}
}
} catch(Exception ex) {
if (! (ex instanceof SocketException)) {
System.out.println("Applet quit -- got " + ex);
}
} finally {
try {
if (in != null) {
in.close();
in = null;
}
} catch (Exception ex) {
// hide it
}
}
}
}