Search the Catalog
Jini in a Nutshell

Jini in a Nutshell

By Scott Oaks & Henry Wong
1st edition March 2000
1-56592-759-1, Order Number: 7591
400 pages, $24.95

The O'Reilly Conference on Java is offering extensive tutorials and sessions on Jini, EJB, XML and Java, Servlets, Jakarta, SSL in Java, Java and PKI, and more. Register today!


Sample Chapter 4:
Basic Jini Programming

Contents:
The Jini Lookup Service
A Simple Service and Client
Leasing and the Lookup Service
Lookup and Discovery Support Classes
Attributes and the Entry Interface
Other Service Implementations
Summary

In the last chapter, we developed an understanding of RMI and how it behaves as an object-oriented distributed framework. With RMI, we have the capability for applications on any host to invoke methods on objects located on another host. An important feature about this framework is that the underlying infrastructure is well hidden.

That grounding in a distributed object model gives us the necessary background to begin our exploration of Jini programming, which we'll do in this chapter. We'll cover the implementation of a Jini service and a Jini client, and while our initial service will be based on RMI, we'll also show how to write a Jini service that uses other techniques to communicate between the client and server, and one that actually performs all of its work in the client itself. This ability to take advantage of different service implementations is one of the strong benefits of Jini.

4.1 The Jini Lookup Service

From now on, we'll be using the Jini lookup service in our examples. In order to understand the Jini lookup service, let's examine some of the more important differences between the RMI registry (and other distributed object registries) and the Jini lookup service:

4.2 A Simple Service and Client

We'll begin our example by showing a complete service, its implementation, and a client that can use the service. All the features of a well-written Jini service will not be present in our initial implementation; as we work through this book, we'll add those additional features. Following our standard practice, we'll develop source in two directories as shown in Example 4.1.

Example 4.1: An Initial Jini Service and Client

Common classes
    ConvertService (listed in )
Service classes
    ConvertServiceImpl (modified from )
    ConvertServiceProxy (listed in )
    ServiceListener (new)
Client classes
    ServiceFinder (new)
    ConvertClient (modified from )

4.2.1 The Jini Service

We'll start by converting the RMI server object that we developed in , into a Jini service. To turn our existing code into a Jini service, we'll modify it so that it participates in Jini's discovery and join protocols as we outlined in : the service will use multicast discovery to find all lookup services on the network, and then join (register with) each of those services. So we need to develop two things: a class that handles discovery and a new implementation of the service itself.

4.2.1.1 Performing discovery and join

The details of the discovery and join protocols are handled for us by the Jini APIS (the LookupDiscovery class that we'll examine a little further on). All we need to do is create a new class (a discovery listener) that will deal with the asynchronous behavior of Jini's discovery system. This class will be alerted when our service discovers a lookup service and when our service needs to discard a lookup service. When we're notified of a new lookup service, we'll join it.

What happens under the covers is this: when our service starts, the Jini classes will send out a special packet that lookup services will respond to; this is how we find the initial set of lookup services. When new lookup services are added to the network, they send out a special packet the Jini classes will see; this is how our program finds out that new lookup services have been added to the network. In both cases, our discovery listener is notified of the presence of the lookup services.

If we encounter an error while talking to a particular lookup service, we must discard that service (it will then be able to be rediscovered if the lookup service recovers). Additionally, if we change the set of groups that we're interested in, lookup services that do not support the new set of groups must be discarded. In both cases, our discovery listener will be informed that the lookup service has been discarded. Here's the implementation of our discovery listener, a new class with this example:

import java.rmi.*;
import java.util.*;
import net.jini.discovery.*;
import net.jini.core.lookup.*;

public class ServerListener implements DiscoveryListener {
    // Hashtable of registration leases keyed by the lookup service
    private Hashtable leases = new Hashtable();
    private ServiceItem item;        // Item to be registered with lookup
    private static final int ltime = 15*60*1000;  // 15 minutes

    public ServerListener(Object object) {
        item = new ServiceItem(null, object, null);
    }

    // Automatically called when new lookup service(s) are discovered
    public synchronized void discovered (DiscoveryEvent dev) {
        ServiceRegistrar[] lookup = dev.getRegistrars();
        // For each discovered service, see if we're already registered.
        // If not, register
        for (int i = 0; i < lookup.length; i++) {
            if (leases.containsKey(lookup[i]) == false) {
                // Not already registered
                try {
                    // Register
                    ServiceRegistration ret =
                                     lookup[i].register(item, ltime);
                    // You must assign the serviceID based on what the
                    // lookup service returns
                    if (item.serviceID == null) {
                        item.serviceID = ret.getServiceID();
                    }
                    // Save this registration
                    // Note that we don't actually renew the leases yet
                    leases.put (lookup[i], ret);
                } catch (RemoteException ex) {
                    System.out.println("ServerListener error: " + ex);
                }
            }
            // else we were already registered in this service
        }
    }


    // Automatically called when lookup service(s) are
    // no longer available
    public synchronized void discarded(DiscoveryEvent dev) {
        ServiceRegistrar[] lookup = dev.getRegistrars();
        for (int i = 0; i < lookup.length; i++) {
            if (leases.containsKey(lookup[i]) == true) {
                // Remove the registration. If the lookup service comes
                // back later, we'll re-register at that time.
                leases.remove(lookup[i]);
            }
        }
    }
}

The net.jini.discovery.DiscoveryListener interface supports two methods: discovered( ) and discarded( ). These methods will be called by the Jini lookup and discovery classes when new lookup services are found or a previously discovered lookup service is to be removed. They are passed a single parameter, a net.jini.discovery.DiscoveryEvent object.

When a new lookup service is detected, the discovered( ) method is called. An array of lookup services (net.jini.core.lookup.ServiceRegistrar objects) is returned from the getRegistrars( ) method of the DiscoveryEvent object. The first step is to determine if the lookup service that was found is unknown to us. This is why we have stored all lookup services along with their registrations into a hashtable: we can simply check the hashtable for the existence of the lookup service.

Once we've determined that we have a new lookup service, we must register our object with that service. To do that, we wrap it into a net.jini.core.lookup.ServiceItem object. To instantiate this service item, we need three things: the object that we will be placing into the lookup service, the unique service ID assigned to the object, and an array of attributes that can be used to further identify the object. In this case, we are passing null for both the service ID and the array of attributes. This means that there will be no attributes stored with this object, and the lookup service will provide the ID assigned to this service object. (We'll discuss the attributes in more detail later in this chapter.)

The service ID is used to distinguish the object when it appears in different lookup services. Our service object will be registered with multiple lookup servers, and other service objects with the same interface may also be registered in those lookup services. Clients need a way to distinguish all of these. If a client retrieves services of the same type from different lookup services, the client can tell whether the objects represent the same instance of the service by comparing the service IDS. In addition, the service ID provides a way for the client to ensure that it uses the same service: a client that wants to select a particular print service as its default printer needs to save only the service ID for that print service. Later, the client knows which print service to use by retrieving the correct service ID.

This means that we must register the same service ID with all lookup services. The actual registration with the lookup service is accomplished with the register( ) method. The two parameters that this method requires are the ServiceItem that we created earlier and an integer which represents the number of milliseconds for which we want the lookup service to grant our lease. We use 15 minutes here, but we'll discuss the notion of leasing a little bit later.

For now, what's important is the register( ) method will return a service registration object that contains, among other things, the service ID assigned to our service. We save that service ID into our service item so future registrations will use the same service ID. When we write a really robust Jini service, we'll actually save this service ID to a file, so if our service crashes, it will come back up with the same service ID (so clients that have saved the service ID of our service will still be able to find us); we'll show that process in .

When a lookup service is to be discarded, the discarded( ) method of the discovery listener is called. An array of lookup services (ServiceRegistrar objects) is returned from the getRegistrars( ) method of the DiscoveryEvent object. We check the hashtable for the existence of each lookup service in the array and remove it.

4.2.2 The Service Implementation

Here's an implementation of our basic service, modified from :

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;

public class ConvertServiceImpl extends UnicastRemoteObject
                                implements ConvertServiceProxy {

    public ConvertServiceImpl() throws RemoteException {
    }

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());

        // Start listening for lookup services
        String[] groups = new String[] { "" };
        LookupDiscovery reg = new LookupDiscovery(groups);
        // Create the instance of the service and register it
        // with all discovered lookup services
        ConvertServiceImpl csi =
                  (ConvertServiceImpl) new ConvertServiceImpl();
        ServerListener sl = new ServerListener(csi);
        reg.addDiscoveryListener(sl);
    }
}

We have not made any changes to the operational behavior of the ConvertServiceImpl class: it is still using RMI as its transport and is supporting the same ConvertServiceProxy interface. The difference is how it is being registered with the lookup service. We now declare an array of groups, which in this example is a single group specified by a blank string. This is the default group for a Jini lookup service. Note that when Sun's tools like reggie specify "public" as the name of a group, they convert that string internally to a blank string so that those tools use the default lookup group.

The registration process starts by instantiating a net.jini.discovery.LookupDiscovery object with an instance of the ServerListener class that we have previously defined. The lookup discovery object is the Jini class that handles the discovery protocol and calls the server listener when it finds new or existing lookup groups.

4.2.3 The Jini Client

Unlike the server, creating the client is a little more complex than just using the Jini lookup service. On the server, the task of registering the object with the lookup service is well defined, and moving from a synchronous registration process to an asynchronous one was handled by a single ServerListener class. On the client, we could conceivably perform a lot more work.

In effect, we must redesign the client to take advantage of the Jini infrastructure. Whether a service is registered with one or many lookup services should have no effect on the server. But the client has many options: should the client use only the first service it finds, or load balance requests across all server objects found? Should the client worry about discarding lookup services that have failed and switch to alternate lookup services?

Those issues are beyond the scope of this quick-reference book. For now, we will write a class that simply returns the first matching service that it finds:

import java.io.*;
import java.rmi.*;
import java.util.*;
import net.jini.discovery.*;
import net.jini.core.lookup.*;
import net.jini.core.entry.*;

public class ServiceFinder implements DiscoveryListener {
    private static String[] publicGroup = new String[] { "" };
    private Vector returnObject = new Vector();

    private LookupDiscovery reg;
    private ServiceTemplate template;

    public ServiceFinder(Class serviceInterface) throws IOException {
        this(publicGroup, serviceInterface, (Entry[])null);
    }

    public ServiceFinder(Class serviceInterface, Entry attribute)
                         throws IOException {
        this(publicGroup, serviceInterface, new Entry[] { attribute });
    }

    public ServiceFinder(Class serviceInterface, Entry[] attributes)
                         throws IOException {
        this(publicGroup, serviceInterface, attributes);
    }

    public ServiceFinder(String[] groups, Class serviceInterface,
                         Entry[] attributes) throws IOException {
        // Construct the template here for matching in the lookup service
        // We don't use the template until we actually discover a service
        Class[] name = new Class[] { serviceInterface };
        template = new ServiceTemplate(null, name, attributes);

        // Create the facility to perform multicast discovery for all
        // lookup services
        reg = new LookupDiscovery(groups);
        reg.addDiscoveryListener(this);
    }

    // Automatically called when a lookup service is discovered
    // (the listener callback of the addDiscoveryListener method)
    public synchronized void discovered(DiscoveryEvent dev) {
        ServiceRegistrar[] lookup = dev.getRegistrars();
        // We may have discovered one or more lookup services
        for (int i = 0; i < lookup.length; i++) {
            try {
                ServiceMatches items =
                    lookup[i].lookup(template, Integer.MAX_VALUE);
                // Each lookup service may have zero or more registered
                // servers that implement our desired template
                for (int j = 0; j < items.items.length; j++) {
                    if (items.items[j].service != null)
                        // Put each matching service into our vector
                        returnObject.addElement(items.items[j]);
                    // else the service item couldn't be deserialized
                    // so the lookup() method skipped it
                }
                notifyAll();
            } catch (RemoteException ex) {
                System.out.println("ServiceFinder Error: " + ex);
            }
        }
    }

    public synchronized void discarded(DiscoveryEvent dev) {
    }

    // This class is to be used by the client. It will return only
    // the first service object that satisfies the template request.
    public synchronized Object getObject() {
        while (returnObject.size() == 0) {
            try {
                wait();
            } catch (InterruptedException ex) {};
        }
        return ((ServiceItem)returnObject.elementAt(0)).service;
    }

    // If an error is encountered when using a service object, the client
    // should call this method.
    // A new object can then be retrieved from the getObject() method.
    public synchronized void errored(Object obj) {
        if ((obj != null) && (returnObject.size() != 0)) {
            if (obj.equals(
                    ((ServiceItem)returnObject.elementAt(0)).service)) {
                returnObject.removeElementAt(0);
            }
        }
    }
}

Just like the ServerListener class, our ServiceFinder class will search for lookup services. However, we don't actually need to keep track of the lookup services: once a lookup service is found, we just ask the lookup service for all services that match the client's interest. This matching is something that we will discuss throughout the remainder of this chapter.

If an object that matches the criteria is found, it is stored into a vector, and we wake up all threads that are waiting for the object. The threads that are waiting for the object will have called the getObject( ) method. So a client uses this class by instantiating it, calling its getObject( ) method to get the matching service, and invoking methods on that service.

The constructor(s) of this class build a net.jini.core.lookup.ServiceTemplate object. This is very similar to the ServiceItem object that we created on the server side. This template is used to search for the particular services. The parameters to the service template are for the three search criteria that we can use:

The service ID

With this object, we can specify the exact service that we want to find. When we specify null (as we have here), we'll match any service ID.

An array of Class objects

This parameter specifies the classes and interfaces that the service must extend or implement.

An array of attributes

This parameter allows us to specify more details about the service we are looking for.

When we construct a client lookup object, we can also specify which groups the returned services must belong to; by default, we search in the default public group.

In addition to being a discovery listener, this class will also manage the LookupDiscovery object. This allows the client to instantiate and use a ServiceFinder object without dealing with the particulars of the Jini lookup system.

Once we have the template of the object that we are looking for, we can find the service object by using this template and calling the lookup( ) method of the lookup service (the ServiceRegistrar object). There are two signatures for this method. In the simpler form, only the first service is found -- if no matching service is found, a null value will be returned. With the method that we are using (which contains an addition parameter that specifies the maximum number of matches), all the matching objects will be retrieved (up to the maximum number specified). This version returns a ServiceMatches object that contains an array of ServiceItem objects. The Jini API ensures that the lookup( ) method will not return a null array and that the length of that array will not be 0 (unless maxMatches was 0). But the individual elements of the array may contain null values, which indicates that a lookup service was found but that we can't deserialize the instance of the lookup service (which usually means that we forgot to install a security manager in the client, though it could also mean that the lookup service isn't correctly supplying its class definitions). Once we find a valid service objects, we store it in the returnObject vector.

Implementing the client is pretty simple. We provided a getObject( ) method that returns the first service object found. This is the object at the beginning of the vector. There will be no load balancing or testing to see if the object is still valid. Instead, we provide the errored( ) method. If the client catches a remote exception, it should call this method with the offending service object. The method will simply remove the object from the first position of the vector so that the client method can call the getObject( ) method to obtain an alternate service object. The client, based on , now looks like this:

import java.rmi.*;
import net.jini.discovery.*;

public class ConvertClient {

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());

        // Find the service by its interface type
        ServiceFinder sf = new ServiceFinder(ConvertService.class);
        ConvertService cs = (ConvertService) sf.getObject();

        // Now invoke methods on the service object
        System.out.println(cs.convert(5));
    }
}

Our only changes here are related to finding the service object. In this simple example, we'll block until the ServiceFinder class discovers and returns a service object, and then either succeed or fail in executing a method on that object. That's good as far as it goes, but a better implementation of the client is for it to handle services that fail. A simple way to achieve that is:

ConvertService cs;
boolean done = false;
while (!done) {
    cs  = (ConvertService) sf.getObject();
    try {
        System.out.println(cs.convert(5));
        done = true;
    } catch (RemoteException re) {
        cl.errored(s);
    }
}

For simplicity, we'll just use the original version in our future examples.

4.2.4 Running the Basic Example

We run this example just as we did all the examples in : we need two separate directories, an HTTP-server to download code from the server to the client, and so on. However, we no longer need to run rmiregistry, since that functionality is now handled by the Jini lookup service. We do, however, need to run some of the basic Jini services as we showed in ; in particular, we must run the HTTP server, rmid, and reggie.

To recap, here are the steps to run the example (assuming that the Jini services are already up and running):

  1. Compile the source files:

    client% javac ConvertService.java ConvertClient.java ServiceFinder.java
    server% javac ConvertService.java ConvertServiceProxy.java
                  ConvertServiceImpl.java ServerListener.java 
  2. On the server, create the necessary stubs and skeletons:

    server% rmic -v1.2 ConvertServiceImpl
  3. Start the HTTP server that we'll use to download code. As usual, we'll use the HTTP server that comes with Jini and run it on port 8086:

    server% java -jar /files/jini1_0/lib/tools.jar -dir . -port 8086

    Make sure that the server class files are in the current directory (.) -- or alternately give a different -dir argument. Remember to start this command in a new window or, if your OS supports it, in the background.

  4. Start the service, specifying the correct property from which code may be downloaded and the appropriate security policy file:

    server% java -Djava.rmi.server.codebase=http://server:8086/
                 -Djava.security.policy=/files/jini1_0/java.policy.all
                 ConvertServiceImpl
  5. Now we can run the client and get its output:

    client% java -Djava.security.policy=/files/jini1_0/java.policy.all 
                 ConvertService
    5

    In this example, it's very important to run the server before the client. Our client is presently written so that it will find all servers that are running when it starts, but it won't be able to find any servers that are started after it is. That's a problem that we won't be able to fix until we learn about remote events in .

4.3 Leasing and the Lookup Service

If you ran the last example, you'll notice that the application does not work after a certain amount of time. That's because we haven't yet dealt with a key aspect of the Jini framework: leasing.

We discuss leasing more in depth in the next chapter. For now, we'll simply point out that leasing is how Jini services keep track of whether their clients are still alive. When you register a service with the lookup service, that registration is valid for only a short period of time. A lease represents that period of time, and the service is responsible for renewing that lease before it expires. If it fails to renew the lease, the lookup service automatically unregisters the service object. The purpose of this feature is that it makes the system more robust. If our application terminates prematurely, the lookup service deletes our entry after the lease period, and new clients don't waste time attempting to contact our service.

In our previous example, when we registered the service object, we asked for a lease time of 15 minutes. This lease time is only a request. We did not check to see how much time was granted, nor did we handle the renewal of the lease. This means that as soon as the lease has expired, new clients will not be able to find the service.

In this section, we'll develop an example that renews the lease provided by the lookup service. The necessary files to run this example are listed in Example 4.2.

Example 4.2: A Jini Service with Leasing

Common classes
    ConvertService (used in Example 4.1; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.1)
    ConvertServiceProxy (used in Example 4.1; listed in )
    ServiceListener (modified from Example 4.1)
Client classes
    ServiceFinder (used in Example 4.1)
    ConvertClient (used in Example 4.1)

Let's add a thread to our service that will renew the lookup service leases. We modify the ServerListener class from Example 4.1 as follows:

import java.rmi.*;
import java.util.*;
import net.jini.discovery.*;
import net.jini.core.lookup.*;
import net.jini.core.lease.*;


public class ServerListener implements DiscoveryListener, Runnable {
    // Hashtable of registration leases keyed by the lookup service
    private Hashtable leases = new Hashtable();
    private ServiceItem item;        // Item to be registered with lookup
    private static final long ltime = Lease.FOREVER;
    private static final int mtime = 30*1000;  // 30 seconds
                                               // (minimum renewal)

    private LookupDiscovery ld;      // The discovery object
                                     // we're listening to

    public ServerListener(LookupDiscovery ld, Object object) {
        item = new ServiceItem(null, object, null);
        this.ld = ld;
        // Start the new thread to renew the leases
        new Thread(this).start();
    }

    // Automatically called when new lookup service(s) are discovered
    public synchronized void discovered(DiscoveryEvent dev) {
        ServiceRegistrar[] lookup = dev.getRegistrars();
        // For each discovered service, see if we're already registered.
        // If not, register
        for (int i = 0; i < lookup.length; i++) {
            if (leases.containsKey(lookup[i]) == false) {
                // Not already registered
                try {
                    // Register
                    ServiceRegistration ret =
                                 lookup[i].register(item, ltime);
                    // You must assign the serviceID based on what the
                    // lookup service returns
                    if (item.serviceID == null) {
                        item.serviceID = ret.getServiceID();
                    }
                    // Save this registration
                    leases.put(lookup[i], ret);
                    // There's a new lease, notify the renewal thread
                    notify();
                } catch (RemoteException ex) {
                    System.out.println("ServerListener error: " + ex);
                }
            }
            // else we were already registered in this service
        }
    }

    // Automatically called when lookup service(s)
    // are no longer available
    public synchronized void discarded(DiscoveryEvent dev) {
        ServiceRegistrar[] lookup = dev.getRegistrars();
        for (int i = 0; i < lookup.length; i++) {
            if (leases.containsKey(lookup[i]) == true) {
                // Remove the registration. If the lookup service comes
                // back later, we'll re-register at that time.
                leases.remove(lookup[i]);
            }
        }
    }

    public synchronized void run() {
        while (true) {
            long nextRenewal = Long.MAX_VALUE;
            long now = System.currentTimeMillis();
            Enumeration e = leases.keys();
            // Loop to renew all leases that are about to expire
            // and also to find the time when the next lease will
            // expire so we know when to run the loop again.
            while (e.hasMoreElements()) {
                ServiceRegistrar lookup =
                           (ServiceRegistrar) e.nextElement();
                ServiceRegistration sr =
                           (ServiceRegistration) leases.get(lookup);
                Lease l = sr.getLease();
                long expire = l.getExpiration();
                // See if the current lease has the minimum time.
                // If we can't renew it, discard that lookup service.
                // That will generate an event to the discarded()
                // method, which will actually remove the lease from
                // our list.
                try {
                    if (expire <= now + mtime) {
                        l.renew(ltime);
                        expire = l.getExpiration();
                    } 
                    if (nextRenewal > expire - mtime) {
                        nextRenewal = expire - mtime;
                    }
                } catch (LeaseDeniedException lex) {
                } catch (UnknownLeaseException lex) {
                    ld.discard(lookup);
                } catch (RemoteException ex) {
                    ld.discard(lookup);
                }
            }
            try {
                // Wait until the next renewal time. A new lease
                // will notify us in case the new
                // lease has a smaller time until it must be renewed
                wait(nextRenewal - now);
            } catch (InterruptedException ex) {};
        }
    }
}

In this new version, we've added a thread to renew our leases and made some changes to support that thread. The constructor must now save the LookupDiscovery object that is needed by the new thread to handle error conditions. It must also start the new thread. And the discovered( ) method adds a single call to notify the new thread that there is now another lease that it needs to renew.

The implementation of the new thread is straightforward. We iterate through the hashtable and check all the leases, which were found in the ServiceRegistration object that was returned by the register( ) method and saved in the hashtable. If the time remaining on the lease is less than the minimum time, we renew the lease by calling the renew( ) method. Then, we adjust the nextRenewal instance variable to represent the earliest time that a lease will expire. Upon completion of the iteration through the leases, we call the wait( ) method to sleep until this time.

In this example, we also added exception handling code for the renew( ) method. If the lease that we requested is denied, then we simply continue with the old lease. If the lease is an unknown lease, it means that the lease expired and the server object has been removed from the lookup service. In this case we discard the lookup service; we'll reregister with it when the lookup service is rediscovered. If a remote exception occurs, we perform the same operation.

To use this new class with our server, we need change only the way in which the ServerListener class from Example 4.1 is constructed:

 ServerListener sl = new ServerListener(reg, s);

The client application does not change in this example.

At this point, we have a complete example server. You can convert any RMI service (or RMI client) to a Jini service (or Jini client) using the previous examples. However, to realize fully the advantages of the Jini infrastructure, the services should be modified to use the more advanced features of the Jini framework. Services can use leases to keep track of the existence of their clients. Services can use events to provide a more asynchronous API to their clients. And services and clients can use the provided Jini support services -- the Jini transaction manager service and the Jini JavaSpaces service -- to take advantage of the self-repairing nature of the Jini infrastructure. We will examine the details of these services in later chapters.

4.4 Lookup and Discovery Support Classes

The Jini Starter Kit provides classes that simplify working with the Jini lookup service. In Jini 1.0, there are classes in the com.sun.jini package that enable a service to discover and join lookup services very simply. In Jini 1.1, there are classes that enable a client to discovery and search lookup services.

4.4.1 The JoinManager and Other Service Classes

We'll look first at the classes that help a service to discover and join lookup services. In 1.0, most of these classes are part of the com.sun.jini package. This means that while they ship with the Jini Starter Kit, they are not official APIS and hence may change at any time. In fact, the most important of these classes -- the JoinManager class -- has moved from the com.sun.jini package to the net.jini package as part of the Jini 1.1 release; its interface also changed between these releases. Other classes in this category also moved packages between 1.0 and 1.1.

Here are the more useful of these classes that pertain to the lookup service:

net.jini.core.discovery.LookupLocator

This class is used to locate the lookup service using a unicast protocol. An object of this class type can be obtained by using the getLocator( ) method of the ServiceRegistrar object. A LookupLocator object can also be created using a URL to define the hostname and network port -- much the same way as the Naming class in accessing an RMI registry.

Since an object of this class type uses a unicast protocol to obtain the ServiceRegistrar object, it can be used to access lookup services that are beyond the range that can be discovered by the LookupDiscovery class.

net.jini.discovery.DiscoveryManagment (1.1 only)

This interface defines a number of operations related to discovery of the lookup service. Objects that implement this interface are used to perform discovery according to a variety of rules. In Jini 1.1, the LookupDiscovery class implements this interface, as do many of the following classes. A given discovery object may perform unicast, multicast, or a combination of both types of discovery, but since they all implement this interface, they may be operated on in the same way.

com.sun.jini.discovery.LookupLocatorDiscovery (1.0)
net.jini.discovery.LookupLocatorDiscovery (1.1)

This class is very similar to the LookupDiscovery class. However, instead of using a multicast protocol to search for lookup services, an object of this class is provided with an array of LookupLocator objects and hence uses only the unicast discovery protocol. Each locator is used to obtain lookup services. Discovery of lookup services is accomplished by using the LookupLocator objects and notification of found ServiceRegistrars is as before -- using DiscoveryListener objects. In 1.1, this class implements the DiscoveryManagement interface.

net.jini.discovery.LookupDiscoveryManager (1.1 only)

This class manages both multicast and unicast discovery. It will perform multicast discovery within your network's multicast radius, and it will also allow you to provide a set of LookupLocator objects and perform unicast discovery to those services. This class implements the DiscoveryManagement interface.

com.sun.jini.lease.LeaseRenewalManager (1.0)
net.jini.lease.LeaseRenewalManager (1.1)

This is a support class that is used in the management of leases. When you register a lease with an object of this class, the lease renewal manager will take the responsibility of renewing leases until you ask it to stop or until a predetermined time has passed.

You can also provide a com.sun.jini.lease.LeaseListener object (net.jini.lease.LeaseListener in 1.1) to this class. The implementation of the lease listener interface requires one method -- the notify( ) method, which is called by the lease renewal manager if it fails to renew a lease.

com.sun.jini.lookup.JoinManager (1.0)
net.jini.lookup.JoinManager (1.1)

The JoinManager class is a "do-it-all" class that accomplishes all of the tasks necessary for a service to interface with the lookup service. It will instantiate a LookupDiscovery object, optionally a LookupLocatorDiscovery object, or a LookupDiscoveryManager object (in 1.1), and install the DiscoveryListener object necessary to handle the discovery objects.

You may also specify a service ID, or -- just as with our ServerListener class -- the JoinManager can obtain the service ID from the first lookup service. The service ID can be delivered back to the application using a com.sun.jini.lookup.ServiceIDListener object (net.jini.lookup.Service-IDListener in 1.1) so that you can save it to persistent store. You can specify the groups to join and all the components of the ServiceItem object. The join manager will handle all the lease renewals with the lookup services, even if no LeaseRenewalManger object is explicitly provided.

With the JoinManager class, implementing our example becomes easy. We need only the files listed in Example 4.3, and only the service implementation changes.

Example 4.3: A Jini Service with a 1.0 Join Manager

Common classes
    ConvertService (used in Example 4.2; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.2)
    ConvertServiceProxy (used in Example 4.2; listed in )
Client classes
    ServiceFinder (used in Example 4.2; listed in Example 4.1)
    ConvertClient (used in Example 4.2; listed in Example 4.1)

The only change for using the JoinManager is in the service implementation:

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;
import com.sun.jini.lookup.*;

public class ConvertServiceImpl extends UnicastRemoteObject
                                implements ConvertServiceProxy {

    public ConvertServiceImpl() throws RemoteException {
    }

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());
        String[] groups = new String[] { "" };
       
        // Create the instance of the service; the JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
                           (ConvertServiceImpl) new ConvertServiceImpl();
        JoinManager manager = new JoinManager(csi, null, groups,
                                              null, null, null);
    }
}

We no longer need to create the ServerListener or LookupDiscovery objects, since everything that we need is handled by the JoinManager object.

The parameters to the constructor of the join manager are:

There is also another constructor that allows us to specify a service ID -- when we have a saved service ID, we must use that format of the constructor. Otherwise, as in this case, the service ID will be provided by the first lookup service that is discovered. We'll show the former facility later in the book. This example is run with the same command line we've used all along.

In 1.1, the code is slightly different, because the interface to the JoinManager class has changed. To use the net.jini.lookup.JoinManager class, we need to modify the ConvertServiceImpl class from Example 4.3 as shown in Example 4.4.

Example 4.4: A Jini Service with a 1.1 Join Manager

Common classes
    ConvertService (used in Example 4.3; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.3)
    ConvertServiceProxy (used in Example 4.3; listed in )
Client classes
    ServiceFinder (used in Example 4.3; listed in Example 4.1)
    ConvertClient (used in Example 4.3; listed in Example 4.1)

Here's the 1.1-based implementation of our service:

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.core.lookup.*;
import net.jini.discovery.*;
import net.jini.lookup.*;

public class ConvertServiceImpl extends UnicastRemoteObject
                   implements ConvertServiceProxy, ServiceIDListener {

    public ConvertServiceImpl() throws RemoteException {
    }

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }
 
    public void serviceIDNotify(ServiceID id) {
        // For now, this is just a required method
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());
        String[] groups = new String[] { "" };

        // Create the instance of the service; the JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
                 (ConvertServiceImpl) new ConvertServiceImpl();
        LookupDiscoveryManager mgr =
                 new LookupDiscoveryManager(groups, null, null);
        JoinManager manager = new JoinManager(csi, null,
                                              csi, mgr, null);
    }
}

In this case, the arguments to the constructor of the join manager are:

Note that which join manager you use is defined by the import statement in the code: the com.sun.jini.lookup.JoinManager class exists in both 1.0 and 1.1, and its interface in that package is unchanged in 1.1. In the rest of our examples, we'll use the 1.0-based code, because at the time of this writing the 1.1 code is still in alpha and subject to change. Once 1.1 FCS has been released, however, you should migrate your code to the new join manager as soon as practical.

4.4.2 The ClientLookupManager Class

In Jini 1.1, there is a new set of classes that helps clients discover lookup services and find registered services in those lookup services. The most important are:

net.jini.discovery.ClientLookupManager

This class uses an object that implements the DiscoveryManagement interface and a lease renewal manager to discover all lookup services on the network. It provides a variety of ways for a client to find services in the discovered lookup services through various implementations of the lookup( ) method, which is used to find a service that matches a particular template. This method can block until a matching service is found, return an array of matching services, and filter the matching services with a ServiceItemFilter object.

The ClientLookupManager class performs the same function as our ServiceFinder class: it allows the client to find a service that implements a particular interface. In Example 4.5, we use the ClientLookupManager to replace the ServiceFinder class.

Example 4.5: A Client with the ClientLookupManager Class.

Common classes
    ConvertService (used in Example 4.4; listed in )
Server classes
    ConvertServiceImpl (used in Example 4.4)
    ConvertServiceProxy (used in Example 4.4; listed in )
Client classes
    ConvertClient (modified from Example 4.4; listed in Example 4.1)

Here's what the new client looks like:

import java.rmi.*;
import net.jini.core.lookup.*;
import net.jini.lookup.*;
import net.jini.discovery.*;

public class ConvertClient {

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());

        // Create the lookup manager to discover lookup services
        ClientLookupManager clm =
                    new ClientLookupManager(null, null);
        Class[] name = new Class[] { ConvertService.class };
        ServiceTemplate st = new ServiceTemplate(null, name, null);
        
        // Block until a matching service is found
        ServiceItem[] si =
                    clm.lookup(st, 1, 5, null, Long.MAX_VALUE);
        if (si == null || si.length == 0)
            throw new Exception("Can't find service");
        ConvertService cs = (ConvertService) si[0].service;

        // Now invoke methods on the service object
        System.out.println(cs.convert(5));
        clm.terminate();
    }
}

When we construct the client lookup manager, we pass null for each parameter. That means the client lookup manager will use a new instance of the LookupDiscoveryManager class as the discovery manager and a new instance of the LeaseRenewalManager to renew any leases. Then, just as we did in the ServiceFinder class, we create a ServiceTemplate and use that to look up services. In this case, the parameters to the lookup( ) method specify:

The alpha version of the ClientLookupManager class is fairly buggy (for example, the code above will hang if there are two matching services running when the client starts); it is subject to change before FCS. In the remainder of our examples, we'll continue to use the ServiceFinder class. However, the ClientLookupManager class does have the advantage that it will discover services that are started after the client starts, which is something that our ServiceFinder class cannot do until we learn about remote events in .

4.5 Attributes and the Entry Interface

In our examination of the ServiceItem class and the ServiceTemplate class, we briefly touched on the concept of attributes. Attributes are used to specify the particulars of the service. For example, while the service may be a printer service, its attributes could be information such as the location of the printer, or configuration information such as whether the printer supports two-sided printing or color, or status information like whether the printer is out of toner or paper. In Jini, attributes are not name/value pairs such as you may be used to working with; they are classes that implement the net.jini.core.entry.Entry interface.

Entry objects are serializable objects that are treated in a special fashion when they are serialized or reconstituted. They are distinguished by the implementation of the Entry interface, which is used purely as a type identifier; it has no methods. Each field of the entry object must be a public object reference. Instance variables of primitive types are not permitted. References to object types that are static, transient, final, or not public are ignored when entry objects are stored or retrieved. Furthermore, these objects must have a default (no argument) constructor. The use of primitive types or the omission of a default constructor will cause an IllegalArgumentException object to be thrown when the object is serialized.

Storing an entry object is accomplished as follows. The fields of an entry object are serialized separately and stored as MarshalledObject objects. This means that if two fields refer to the same object (even indirectly), two copies of the object will be stored in their serialized form. The reason MarshalledObject objects are used is because they can be checked for equality without deserializing the field; this is faster than comparing the deserialized objects, and it means that the lookup service doesn't need to load the class definitions for the object contained within the marshalled object. This allows them to be managed and matched quickly. Fields that are static, transient, final, or not public do not get serialized and are initialized by the default constructor during deserialization.

Matching an entry object requires an additional entry object used as a template, specifying the field values to be searched for. A template entry object matches an entry object if the type of the template object is the same as or a superclass of the type of the entry object. The fields within the objects must also have the same values. A template with a null value for a field is treated as a wildcard and matches any value for that field. Any fields in the entry object that don't exist in the template object (because of subtyping) match. This may sound complicated, but it is quite simple. In Example 4.6, we extend our basic example to allow attribute matching (we've gone back to Example 4.3 as the basis for the code in this example so it will run under 1.0).

Example 4.6: A Jini Service with Standard Attributes

Common classes
    ConvertService (used in Example 4.3; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.3)
    ConvertServiceProxy (used in Example 4.3; listed in )
Client classes
    ServiceFinder (used in Example 4.3; listed in Example 4.1)
    ConvertClient (modified from Example 4.3; listed in Example 4.1)

Here's our new service implementation:

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;
import net.jini.core.entry.*;
import net.jini.lookup.entry.*;
import com.sun.jini.lookup.*;

public class ConvertServiceImpl extends UnicastRemoteObject
                                implements ConvertServiceProxy {

    public ConvertServiceImpl() throws RemoteException {
    }

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());
        String[] groups = new String[] { "" };

        Entry[] attributes = new Entry[2];
        attributes[0] = new Name("Marketing Converter");
        attributes[1] = new Location("4", "4101", "NY/02");

        // Create the instance of the service; the JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
                  (ConvertServiceImpl) new ConvertServiceImpl();
        JoinManager manager = new JoinManager(csi, attributes, groups,
                                              null, null, null);
    }
}

In our new version of the ConvertServiceImpl class, we are adding two attributes to our service: a name attribute and a location attribute. The name attribute gives our service a name -- in this case, the service is an integer converter to support conversion requests in the marketing department. The location attribute is used to inform its users of its location -- in this case, the 4th floor, in room #4101, in building NY/02. These attributes are entry objects and are created and provided to the join manager to be published in the lookup service.

While the lookup service will support any attribute that is an Entry object, the following attributes are provided by the Jini framework. Using these attributes will save you from creating a new class type:

net.jini.lookup.entry.Address

An attribute used to determine the address of the service. Fields include the organization name, the street address, city, state, zip code, and country.

net.jini.lookup.entry.Comment

An attribute that holds a generic comment. The stored data is a single string object.

net.jini.lookup.entry.Location

An attribute used to determine the location of the service. Unlike the Address entry, which can be used to provide contact information of the service provider or owner, this attribute provides the location of the service by specifying the floor, the room number, and the name of the building where the service is running.

net.jini.lookup.entry.Name

An attribute used to provide the name of the service. The stored data is a single string object.

net.jini.lookup.entry.ServiceInfo

An attribute used to provide generic information about the service. Fields include the name of the manufacturer, the name of the vendor, the version of the service, the model number, and the serial number.

net.jini.lookup.entry.ServiceType

An abstract attribute used to provide human readable information. Subclasses of this type should provide a name for the service, a short description, and icons that represent the service.

net.jini.lookup.entry.Status

An abstract attribute used to represent the state of the service. Services should use this base type to represent the status and error conditions of the service.

In order better to understand the use of entry objects, let's examine the client side of the application. Since our ServiceFinder class already supports the use of attributes, we just need to use a different constructor in our ConvertClient class:

import java.rmi.*;
import net.jini.discovery.*;
import net.jini.core.entry.*;
import net.jini.lookup.entry.*;

public class ConvertClient {

    public static void main (String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());

        // Find the service by its interface type
        Entry attribute = new Location("4", null, null);
        ServiceFinder sf =
                 new ServiceFinder(ConvertService.class, attribute);
        ConvertService cs = (ConvertService) sf.getObject();

        // Now invoke methods on the service object
        System.out.println(cs.convert(5));
    }
}

In our new client example, we now specify a single location attribute as part of our search template. The client is still looking for objects that support the ConvertService interface, except that there is now an additional requirement that it must satisfy: the service must be located on the fourth floor. We are not specifying a room number or building name, so we use a null value for those fields when we construct a location template; those fields will be treated as a wildcard. Furthermore, we are not specifying a name attribute, so the name attribute of the service object will not be checked.

4.5.1 Service-Defined Attributes

While the attributes that are defined by the Jini API suffice for many cases, there will be cases in which we will be required to write our own attributes. We'll do that in Example 4.7.

Example 4.7: A Jini Service with Service-Defined Attributes

Common classes
    ConvertService (used in Example 4.6; listed in )
    Copyright (new)
Server classes
    ConvertServiceImpl (modified from Example 4.6)
    ConvertServiceProxy (used in Example 4.6; listed in )
Client classes
    ServiceFinder (used in Example 4.6; listed in Example 4.1)
    ConvertClient (modified from Example 4.6)

Since attributes are simply entry objects (which themselves are simply data objects that follow a design pattern), it is quite easy to implement our own attributes:

import net.jini.entry.*;
import net.jini.lookup.entry.*;

public class Copyright extends AbstractEntry
                       implements ServiceControlled {
    // The fields of the Entry --
    // each must be a public, serializable object
    public String owner;
    public String date;
    public String lawfirm;

    public Copyright() {
        this(null, null, null);
    }

    public Copyright (String owner, String date, String lawfirm) {
        this.owner = owner;
        this.date = date;
        this.lawfirm = lawfirm;
    }
}

Let's introduce a new attribute -- a Copyright attribute. This attribute contains three fields, which represent the owner of the copyright, the date that the copyright was established, and the law firm that will sue whoever violates the copyright. Following the pattern for entry classes, the fields are all public object references, there is a default constructor, and this class implements the Entry interface. It implements the Entry interface by subclassing from the net.jini.entry.AbstractEntry class. While subclassing from the AbstractEntry class is not necessary, it does help, since that class provides a correct implementation of the methods that help in the comparison of entry objects, including the equals( ) and hashCode( ) methods.

We have also implemented the net.jini.lookup.entry.ServiceControlled interface. This interface is also used only for type identification. Implementation of this interface marks our entry object as read-only to our clients. The service may change this entry object (attribute) as it likes, but clients may only read this attribute. (It is possible for clients to ask a service to change the value of an attribute; for a discussion of this, see the administration interfaces in .)

The server implementation uses this new attribute as follows:

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;
import net.jini.core.entry.*;
import net.jini.lookup.entry.*;
import com.sun.jini.lookup.*;

public class ConvertServiceImpl extends UnicastRemoteObject
                                implements ConvertServiceProxy {

    public ConvertServiceImpl() throws RemoteException {
    }

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new RMISecurityManager());
        String[] groups = new String[] { "" };

        Entry[] attributes = new Entry[3];
        attributes[0] = new Name("Marketing Converter");
        attributes[1] = new Location("4", "4101", "NY/02");
        attributes[2] = new Copyright("AJC Marketing", "9/1999",
                                      "Oaks, Wong, Adler, and Mar");

        // Create the instance of the service; JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
                       (ConvertServiceImpl) new ConvertServiceImpl();
        JoinManager manager = new JoinManager(csi, attributes, groups,
                                              null, null, null);
    }
}

Once we have our new attribute, using it is almost like using any other attribute. There is only one difference. Since the attribute is not part of the Jini libraries, it must be placed on the web server in order for the client to find it. Furthermore, if the client is actually to use it as part of its template, it will need the class file during the compilation phase also. In our online example code, the client uses the Copyright attribute to lookup its service, which is why we listed the Copyright class as common between client and server.

4.6 Other Service Implementations

We started this chapter by taking an RMI service and converting it into a Jini service, and we've used RMI ever since as this basis of our services. However, we don't mean to give the impression that Jini is simply an enhancement to RMI or that RMI is the only way to write a Jini service (even though we do feel it's one of the better ways). So before concluding our initial look at Jini, we'll present two more examples. These examples show that you can use a variety of transports with Jini and that you can architect Jini communities in a variety of ways.

4.6.1 Using Local Execution

Jini allows your service to take advantage of Java's movable code facility so that the service logic can be executed anywhere -- and in particular, the service logic can be executed on the client. If you have a device with limited resources, this is an important benefit. In Example 4.8, we develop such a service. Note that since the service interface remains unchanged, so too does the client code. We've also gone back to Example 4.3 as a simpler basis for this example.

Example 4.8: A Jini Service That Executes Locally

Common classes
    ConvertService (used in Example 4.3; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.3)
Client classes
    ServiceFinder (used in Example 4.3; listed in Example 4.1)
    ConvertClient (used in Example 4.3; listed in Example 4.1)

Because this is not an RMI service, it no longer needs the ConvertServiceProxy class. Instead, we'll return to our basic implementation with a 1.0 join manager and implement the service as follows:

import java.io.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;
import com.sun.jini.lookup.*;

public class ConvertServiceImpl implements ConvertService, Serializable {

    public String convert(int i) throws RemoteException {
        return new Integer(i).toString();
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new SecurityManager());
        String[] groups = new String[] { "" };

        // Create the instance of the service; the JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
                   (ConvertServiceImpl) new ConvertServiceImpl();
        JoinManager manager = new JoinManager(csi, null, groups,
                                              null, null, null);
        while (true)
            Thread.sleep(1000000);
    }
}

As long as an object is serializable, it can join the Jini lookup service. So this service registers itself (rather than its stub as in previous examples). When a client loads the object from the Jini lookup service, it will get an instance of this class locally, and the convert( ) method will then be executed on the client. The difference comes because the ConvertServiceImpl class is no longer a remote object, so when the object is serialized, a copy of the object itself is transferred to the client.

The server itself must do something to make sure that it keeps running. In the case of an RMI server, the RMI infrastructure starts threads that listen for requests and continue to run after the main thread exits. In this case, while the join manager has started some additional threads, they are all daemon threads, and the server will exit if the main thread exits. So this server simply sleeps forever, which allows its background threads to handle discovery and lease renewals with the lookup service.

Note that while this implementation has nothing to do with RMI, when we run the server we must still specify a java.rmi.server.codebase property. We must place the ConvertServiceImpl class itself in this codebase (rather than generating a stub class and placing that in the codebase) because now the client must load that class definition -- and even though the class itself has no connection to RMI, the Jini infrastructure will still use RMI to transfer the object (and class definition) to the client. So this server is run with exactly the same command line as all our other servers (as is the client).

4.6.2 Protocol Wrapping

What if you have an existing service that you want to expose as a Jini service? It's a simple matter of writing a Jini wrapper that calls the existing service, as we show in Example 4.9.

Example 4.9: A Jini Wrapper for an Existing Service

Common classes
    ConvertService (used in Example 4.3; listed in )
Server classes
    ConvertServiceImpl (modified from Example 4.3)
Client classes
    ServiceFinder (used in Example 4.3; listed in Example 4.1)
    ConvertClient (used in Example 4.3; listed in Example 4.1)

In this example, we'll mimic an existing conversion service that simply uses sockets for its communication. Once again, all the changes are on the server side, which has this implementation (since the changes are extensive, we won't highlight them):

import java.io.*;
import java.net.*;
import java.rmi.*;
import java.rmi.server.*;
import net.jini.discovery.*;
import com.sun.jini.lookup.*;

public class ConvertServiceImpl implements ConvertService,
                                           Serializable, Runnable {

    // Make this transient, so on all clients it will be false
    // (the default serialization value)
    transient boolean isServer;
    String remoteHost;            // Host where server socket is running
    int remotePort;               // Port of server socket

    ConvertServiceImpl(String host, int port) {
        remoteHost = host;
        remotePort = port;
        isServer = true;
    }

    public String convert(int i) throws RemoteException {
        Socket sock;
        DataOutputStream dos;
        BufferedReader br;
        String s;

        try {
            // Construct the socket to the remote server. Send it the
            // integer and read the returned string. This should only
            // be called by the client.
            if (isServer)
                throw new IllegalArgumentException(
                                     "Can't call from server");

            sock = new Socket(remoteHost, remotePort);
            dos = new DataOutputStream(sock.getOutputStream());
            br = new BufferedReader(
                     new InputStreamReader(sock.getInputStream()));
            dos.writeInt(i);
            dos.flush();
            s = br.readLine();
            sock.close();
            return s;
        } catch (Exception e) {
            throw new RemoteException("Convert failed", e);
        }
    }

    public void run() {
        try {
            // On the server, start the server socket and process requests
            if (!isServer)
                throw new IllegalArgumentException(
                                 "Should only run on server");
            ServerSocket ss = new ServerSocket(remotePort);
            while (true) {
                Socket s;
                try {
                    s = ss.accept();
                } catch (Exception e) {
                    // Resource error on the server; can't recover
                    return;
                }
                DataInputStream dis = new DataInputStream(
                                             s.getInputStream());
                PrintWriter pw = new PrintWriter(s.getOutputStream());
                Integer I = new Integer(dis.readInt());
                pw.println(I);
                pw.flush();
                s.close();
            }
        } catch (Exception e) {
            // Client disconnected
        }
    }

    public static void main(String[] args) throws Exception {
        System.setSecurityManager(new SecurityManager());
        String[] groups = new String[] { "" };

        // Create the instance of the service; the JoinManager will
        // register it and renew its leases with the lookup service
        ConvertServiceImpl csi =
             (ConvertServiceImpl) new ConvertServiceImpl(
                                InetAddress.getLocalHost().getHostName(),
                                3333);
        new Thread(csi).start();

        JoinManager manager = new JoinManager(csi, null, groups,
                                              null, null, null);
    }
}

Once again, a ConvertServiceImpl object will be transferred to clients that use this service. When the client calls the convert( ) method, a socket will be constructed back to the original server and data will be transmitted on that socket. All of this is transparent to the client, which simply calls the same method it always has. And even though the Jini infrastructure uses RMI to transfer the object to the client, once the client has the object, the client and server communicate using their own proprietary protocol.

The commands to run the client and server remain unchanged.

4.7 Summary

With the examples in this chapter, we have a fully functional Jini server and Jini client. Both of these programs can find and interact with the Jini lookup service, whether by using standard Jini APIS or additional APIS that are part of Sun's com.sun.jini package. And we've seen examples of how the service can be implemented using RMI, proprietary protocols, and even local execution.

In the next chapter, we'll look into more details about leasing. While leasing is the basis of the contract between the lookup service and Jini services, it is quite useful for services themselves. Understanding leasing is a prerequisite to the techniques needed to complete our basic service.

Back to: Jini in a Nutshell


O'Reilly Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies

© 2001, O'Reilly & Associates, Inc.
webmaster@oreilly.com