This chapter looks at what is involved in discovering a lookup service/service locator. This is common to both services and clients.
A client locates a service by querying a lookup service (service locator). In order to do this, it must first locate such a service. On the other hand, a service must register itself with the lookup service, and in order to do so it must also first locate a service.
The initial phase of both a client and a service is thus discovering a lookup service. Such a service (or set of services) will usually have been started by some independent mechanism. The search for a lookup service can be done either by unicast or by multicast. In fact, the lookup service is just another Jini service, but it is one that is specialised to store services and pass them on to clients looking for them.
Sun supplies a lookup service called reggie
as
part of the standard Jini distribution. The specification of a
lookup service is public, and in future we may expect to see
other implementations of lookup services. There may be any
number of these lookup services running in a network. A LAN
may run many lookup services to provide redundancy in case one
of them crashes. Across the internet, people may run lookup
services for a variety of reasons: sometimes a public lookup
service is running on
http://www.jini.monash.edu.au
to aid people
trying Jini clients and services without needing to also set
up a lookup service. Other lookup services may act as
co-ordination centres, for example by acting as a repository
of locations for all of the atomic clock servers in the world.
Anybody can start a lookup service (depending on access permissions), but it will usually be started by an administrator, or started at boot time.
Starting a lookup service used to be the hardest part of getting Jini
working for the beginner. It could take hours, or even days of playing
with configuration files and network settings.
It has now been made substantially easier -
just run a DOS batch file or Unix shell script. At the top level of the Jini
distribution is a directory installverify
.
Change to this directory and run the program
LaunchAll
and it will start an HTTP server, a lookup
service reggie
and several other useful services.
For the curious, LaunchAll
uses the ServiceStarter
described in a later chapter, and this uses a configuration file
startAll.config
. Configuration files will also be described
in a later chapter.
Unicast discovery can be used when you already know the machine on which the lookup service resides, so you can ask for it directly. This is expected to be used for a lookup service that is outside of your local network, which you know the address of anyway (your home network while you are at work, given in some newsgroup or email message, or maybe even advertised on TV!).
The class LookupLocator
in package
net.jini.core.discovery
is used for this. There
are two constructors,
package net.jini.core.discovery;
public class LookupLocator {
LookupLocator(java.lang.String url) throws
java.net.MalformedURLException;
LookupLocator(java.lang.String host,int port);
}
For the first constructor, the URL must be
of the form jini://host/
or
jini://host:port/
. If no port is given, it
defaults to 4160. The host should be a valid DNS name (such as
www.jini.monash.edu.au
or an IP address (such as
137.92.11.13
). No unicast discovery is performed
at this stage, though, so any rubbish could be entered. Only a
check for syntactic validity of the URL is performed. This
syntantic check is not even done for the second constructor.
The following program creates some objects with valid and invalid host/URLs. They are only checked for syntactic validity rather than existence as URLs:
package basic;
import net.jini.core.discovery.LookupLocator;
/**
* InvalidLookupLocator.java
*/
public class InvalidLookupLocator {
static public void main(String argv[]) {
new InvalidLookupLocator();
}
public InvalidLookupLocator() {
LookupLocator lookup;
// this is valid
try {
lookup = new LookupLocator("jini://localhost");
System.out.println("First lookup creation succeeded");
} catch(java.net.MalformedURLException e) {
System.err.println("First lookup failed: " + e.toString());
}
// this is probably an invalid URL,
// but the URL is syntactically okay
try {
lookup = new LookupLocator("jini://ABCDEFG.org");
System.out.println("Second lookup creation succeeded");
} catch(java.net.MalformedURLException e) {
System.err.println("Second lookup failed: " + e.toString());
}
// this IS a malformed URL, and should throw an exception
try {
lookup = new LookupLocator("A:B:C://ABCDEFG.org");
System.out.println("Third lookup creation succeeded");
} catch(java.net.MalformedURLException e) {
System.err.println("Third lookup failed: " + e.toString());
}
// this is valid, but no check is made anyway
lookup = new LookupLocator("localhost", 80);
System.out.println("Fourth lookup creation succeeded");
}
} // InvalidLookupLocator
All programs in this book can be compiled using the JDK 1.2
compiler. Jini programs will not compile or run under JDK 1.1
(any versions). The Java 1.5 compiler can be used, although
the Jini class libraries do not
use any of the 1.5 features. The following program defines the class
InvalidLookupLocator
in package
basic
. The source code will in the file
InvalidLookupLocator.java
in the
basic
subdirectory. From the parent directory,
this can be compiled by a command such as
javac basic/InvalidLookupLocator.java
to leave the class file also in the basic
subdirectory.
The CLASSPATH
will need to include some Jini jar files.
In versions 2.0 and earlier the
jini-core.jar
jar was required.
This has changed for Jini 2.1 - the preferred files are
jsk-platform.jar
and jsk-lib.jar
for compilation of the source code. These files are in the
lib
subdirectory of the Jini distribution.
When a service is run, these Jini files will need to be in its
CLASSPATH
. Similarly, when a client runs, it will
also need these files in its CLASSPATH
. The reason
for this repetition is that the service and the client are two
separate applications, running in two separate Java Virtual
Machines, and quite likely will be on two separate computers.
The InvalidLookupLocator
has no additional
requirements. It does not perform any network calls, and does
not require any additional service to be running. So it can be
run simply by
java -Djava.security.policy=policy.all -classpath ... basic.InvalidLookupLocator
where the
policy file could be the permisssive security policy file
grant { permission
java.security.AllPermission;
};
An Ant file to build, deploy and run this class is
A LookupLocator
has methods
String getHost();
int getPort();
which
will return information about the hostname that the locator
will use, and the port it will connect on or is already
connect on. This is just the information fed into the
constructor or left to default values, though. It doesn't give
anything new for unicasting. This information
will be useful in the multicast
situation, though, if you need to find out where the lookup
service is.
Search and lookup is performed by the method
getRegistrar()
of the LookupLocator
which returns an object of class
ServiceRegistrar
.
public ServiceRegistrar getRegistrar() throws
java.io.IOException,
java.lang.ClassNotFoundException
The
ServiceRegistrar
is discussed in detail later. This performs network lookup on
the URL given in the LookupLocator
constructor.
UML sequence diagrams are useful for showing the timelines of object
existence and the method calls that are made from one object
to another. The timeline reads down, and method calls and
their returns read across. A UML sequence diagram augmented
with a jagged arrow showing the network connection is shown in
figure 4.1. The
UnicastRegister
object makes a new()
call to create a LookupLocator
and this call
returns a lookup
object. The method call
getRegistrar()
is then made on the
lookup
object, and this causes network activity.
As a result of this, a ServiceRegistrar
object is
created in some manner by the lookup
object, and
this is returned from the method as the
registrar
.
package basic;
import net.jini.core.discovery.LookupLocator;
import net.jini.core.lookup.ServiceRegistrar;
import java.rmi.RMISecurityManager;
/**
* UnicastRegistrar.java
*/
public class UnicastRegister {
static public void main(String argv[]) {
new UnicastRegister();
}
public UnicastRegister() {
LookupLocator lookup = null;
ServiceRegistrar registrar = null;
System.setSecurityManager(new RMISecurityManager());
try {
lookup = new LookupLocator("jini://localhost");
} catch(java.net.MalformedURLException e) {
System.err.println("Lookup failed: " + e.toString());
System.exit(1);
}
try {
registrar = lookup.getRegistrar();
} catch (java.io.IOException e) {
System.err.println("Registrar search failed: " + e.toString());
System.exit(1);
} catch (java.lang.ClassNotFoundException e) {
System.err.println("Registrar search failed: " + e.toString());
System.exit(1);
}
System.out.println("Registrar found");
// the code takes separate routes from here for client or service
}
} // UnicastRegister
The registrar
object will be used in different
ways for clients and services: the services will use it to
register themselves, and the clients will use it to locate
services.
The program needs to be compiled and run with
jsk-platform.jar
and jsk-lib.jar
in its CLASSPATH
.
javac -classpath ... basic/UnicastRegister.java
When
run, it will attempt to connect to the service locator, so
obviously one needs to be running on the machine specified in
order for this to happen. Otherwise, the program will throw an
exception and terminate. Here the host specified is
localhost
. It could, however, be any machine
accessible on the local or remote network (as long as it is
running a service locator). For example, sometimes a
service locator is running on my current workstation,
jan.newmarch.name
. The
parameter to LookupLocator
would be
jini://jan.newmarch.name
.
This program will receive a ServiceRegistrar
from
the service locator. However, it does so by a simple
readObject()
on a socket connected to the service
locator, and so does not need any additional support services
such as rmiregistry
or rmid
. It can
be run by
java -Djava.security.policy=policy.all -classpath ... basic.UnicastRegister
An Ant file to build, deploy and run this class is
If the location of a lookup service is unknown, it is necessary to make a broadcast search for one. UDP supports a multicast mechanism which the current implementations of Jini use. Because multicast is expensive in terms of network requirements, most routers block multicast packets. This usually restricts broadcast to a local area network, although this depends on the network configuration and the time-to-live (TTL) of the multicast packets.
There may be any number of lookup services running on the network accessible to broadcast search. On a small network, such as a home network, there may be just a single lookup service, but in a large network there may be many - perhaps one or two per department. Each one of these may choose to reply to a broadcast request.
Some services may be meant for anyone to use, but some may be more restricted in applicability. For example, the Engineering Dept may wish to keep lists of services specific to that department. This may include a departmental diary service, a deparmental inventory, etc. The services themselves may be running anywhere in the organisation, but the department would like to be able to store information about them and to locate them from their own lookup service. Of course, this lookup service may be running anywhere too!
So there could be lookup services specifically for a particular group of services such as the Engineering Dept services, and others for the Publicity Dept services. Some lookup services may cater for more than one group - for example a company lookup service may want to hold information about all services running for all groups.
When a lookup service is started, it can be given a list of groups to act for as a command line parameter. A service may include such group information by giving a list of groups that it belongs too. This is an array of strings, such as
String [] groups = {"Engineering dept"};
The class LookupDiscovery
in package
net.jini.discovery
is used for broadcast
discovery. There are two constructors
LookupDiscovery(java.lang.String[] groups)
LookupDiscovery(java.lang.String[] groups, Configuration config)
We shall only look at the first
one for now. The second one is new to Jini 2.0
The parameter to the first LookupDiscovery
constructor
can take three cases
null
, or
LookupDiscovery.ALL_GROUPS
,
means to attempt to discover all reachable lookup services
no matter which group they belong to.
This will be the normal case.
An empty list of strings, or
LookupDiscovery.NO_GROUPS
,
means that the object is created, but no search is performed. In this
case, the method setGroups()
will need to be
called
in order to perform a search.
A non-empty array of strings can be given. This will attempt to discover all lookup services in that set of groups.
A broadcast is a multicast call across the network, expecting
lookup services to reply as they receive it. Doing so may take
time, and there will generally be an unknown number of lookup
services that can reply. To be notified of lookup services as
they are discovered, the application must register a listener
with the LookupDiscovery
object.
public void addDiscoveryListener(DiscoveryListener l)
The listener must implement the
DiscoveryListener
interface:
package net.jini.discovery;
public abstract interface DiscoveryListener {
public void discovered(DiscoveryEvent e);
public void discarded(DiscoveryEvent e);
}
The discovered()
method is invoked whenever a
lookup service has been discovered. The API recommends that
this method should return quickly, and not make any remote
calls. However, for a service it is the natural place to
register the service, and for a client it is the natural place
to ask if there is a service available and to invoke this
service. It may be better to perform these lengthy operations
in a separate thread.
There are other timing issues involved: when the
DiscoveryListener
is created, the broadcast is
made. After this, a listener is added to this discovery
object. What happens if replies come in very quickly, before
the listener is added? The ``Jini Discovery Utilities
Specification'' guarantees that these replies will be buffered
and delivered when a listener is added. Conversely, no replies
may come in for a long time - what is the application supposed
to do in the meantime? It cannot simply exit, because then
there would be no object to reply to! it has to be made
persistent enough to last till replies come in. One way of
handling this is if the application has a GUI interface - then
it will stay till the user dismisses it. Another possibility
is that the application may be prepared to wait for a while
before giving up. In that case the main could sleep for, say,
ten seconds and then exit. This will depend on what the
application should do if no lookup service is discovered.
The discarded()
method is invoked whenever the
application discards a lookup service by calling
discard()
on the registrar object.
The parameter to the discovered()
method of the
DiscoveryListener
interface is a
DiscoveryEvent
object.
package net.jini.discovery;
public Class DiscoveryEvent {
public net.jini.core.lookup.ServiceRegistrar[] getRegistrars();
}
This has one public method,
getRegistrars()
which returns an array of
ServiceRegistrar
objects. Each one of these
implements the ServiceRegistrar
interface, just
like the object returned from a unicast search for a lookup
service. More than one can be returned if a set of replies
have come in before the listener was registered - they are
collected in an array and returned in a single call to the
listener. A UML sequence diagram augmented with jagged arrows
showing the network broadcast and replies is shown in figure
4.2.
In this figure, creation of a LookupDiscovery
object
starts the broadcast search, and it returns the
discover
object. The
MulticastRegister
adds itself as listener to the
discover
object. The search continues in a
separate thread, and when a new lookup service replies, the
discover
object invokes the
discovered()
method in the
MulticastRegister
, passing it a newly created
DiscoveryEvent
. The
MulticastRegister
object can then make calls on
the DiscoveryEvent
such as
getRegistrars()
, which will return suitable
ServiceRegistrar
objects.
By this stage the program looks like
package basic;
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.discovery.LookupLocator;
import java.rmi.RemoteException;
/**
* MulticastRegister.java
*/
public class MulticastRegister implements DiscoveryListener {
static public void main(String argv[]) {
new MulticastRegister();
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(10000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public MulticastRegister() {
System.setSecurityManager(new java.rmi.RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
e.printStackTrace();
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];
// the code takes separate routes from here for client or service
try {
System.out.println("found a service locator at " +
registrar.getLocator().getHost() +
" at port " +
registrar.getLocator().getPort());
} catch(RemoteException e) {
e.printStackTrace();
}
}
}
public void discarded(DiscoveryEvent evt) {
}
} // MulticastRegister
In the constructor we create a LookupDiscovery
object, add a DiscoveryListener
and then the
constructor terminates. The main()
method, having
called this constructor, promptly goes to sleep. What is going
on here? The constructor to LookupDiscovery
actually starts up a number of threads, to broadcast the
service and to listen for replies. (See the chapter on Architecture). When replies come
in, the listener thread will call the
discovered()
method of the
MulticastRegister
. However, these threads are
daemon threads. Java has two types of
threads: daemon and user threads, and at least one user thread
must be running or the application will terminate. All these
other threads are not enough to keep the application alive,
and it keep a user thread running in order to continue to
exist.
The sleep()
ensures that a user thread continues
to run, even though it apparently does nothing! This will keep
the application alive so that the daemon threads (running in
the ``background'') can discover some lookup locators. Ten
seconds (10,000 milliseconds) is long enough for that. To stay
alive after this ten seconds expires will
depend on either increasing the sleep time or creating another
user thread in the discovered()
method. Later in
this book, use is made in ``leasing'' of a useful constant
Lease.FOREVER
. While the ``leasing'' system
understands a special meaning for this constant, the standard
Java sleep()
merely uses its value
Long.MAX_VALUE
and just sleeps for a lengthy
period.
I have placed the sleep()
in the
main()
method. It is perfectly reasonable to
place it in the application constructor, and some examples do
this. However, it looks a bit strange there, so I prefer this
placement. Note that although the constructor to
MulticastRegister
will have terminated without us
assigning its object reference, a live reference has been
passed into the discover
object as a
DiscoveryListener
and it will keep the reference
alive in its own daemon threads. This means that the
application object will still exist for its
discovered()
method to be called.
Any other method that results in a user thread continuing to exist will do just as well. For example, a client that has an AWT or Swing user interface will stay alive because there are many user threads created by any of these GUI objects.
For services, which typically will not have a GUI interface
running, another simple way is to create an object and then
wait for another thread to notify()
it. Since
nothing will, the thread (and hence the application) stays
alive. Essentially, this is an unsatisfied wait that will
never terminate - usually an erroneous thing to do, but here
it is deliberate.
Object keepAlive = new Object();
synchronized(keepAlive) {
try {
keepAlive.wait();
} catch(InterruptedException e) {
// do nothing
}
}
This will keep the service alive
indefinitely long, and will not terminate unless interrupted.
This is unlike sleep()
which will terminate
eventually.
The program needs to be compiled and run with
jsk-platform.jar
and jsk-lib.jar
in its CLASSPATH
.
javac -classpath ... basic/MulticastRegister.java
When run, the program
will attempt to find all service locators that it can. If
there are none, it will find none - pretty boring. So one or
more should be set running in the near network or on the local
machine.
java -Djava.security.policy=policy.all -classpath ... basic.MulticastRegister
This program will receive ServiceRegistrar
's from
the service locators. However, it does so by a simple
readObject()
on a socket connected to a service
locator, and so does not need any additional support services
such as rmiregistry
.
An Ant file to build, deploy and run this class is
Services and clients search for lookup locators using the multicast protocol, by sending out packets as UDP datagrams. It makes announcements on UDP 224.0.1.84 on port 4160. How far do these announcements reach? This is controlled by two things:
the ``time to live'' (TTL) field on the packets
network administrator settings on routers and gateways
LookupDiscovery
sets the TTL to be 15. Common
network administrative settings restrict such packets to the
local network. However, the TTL may be changed by giving the
system property net.jini.discovery.ttl
a
different value. But be careful about setting this: many
people will get irate if you flood the networks with multicast
packets.
The ServiceRegistrar
is an abstract class which is
implemented by each lookup service. The actual details of this
implementation are not relevant here. The
role of a ServiceRegistrar
is
to act as a proxy for the lookup service.
This proxy runs in the application, which may be a service or a
client.
This is the first object that is moved from one Java process to
another in Jini. It is shipped from the lookup service to the
application looking for the lookup service, using a socket
connection. From then it runs as an object in the application's
address space, and the application makes normal method calls to
it. When needed, it communicates back to its lookup service. The
implementation used by Sun's reggie
uses RMI to
communicate, but the application does not need to know this, and
anyway, it could be done in different ways. This proxy object
should not cache any information on the application side, but
get ``live'' information from the lookup service as needed. The
implementation of the lookup service supplied by Sun does
exactly this.
This object has two major methods, one used by a service attempting to register
public
ServiceRegistration register(ServiceItem item, long leaseDuration)
throws java.rmi.RemoteException
and the other(s) by a client trying to locate
a particular service
public java.lang.Object lookup(ServiceTemplate tmpl)
throws java.rmi.RemoteException;
public ServiceMatches lookup(ServiceTemplate tmpl, int maxMatches)
throws java.rmi.RemoteException;
The details of these are given in Service Registration
and in Client Search .
For now, an overview will suffice.
A service provider will register a service object (that is, an
instance of a class), and a set of attributes for that object.
For example, a printer may specify that it can handle Postscript
documents, or a toaster that it can deal with frozen slices of
bread. The service provider may register a singleton object that
completely implements the service, but more likely it will
register a service proxy that will communicate back to other
objects in the service provider. Note carefully: the
registered object will be shipped around the network. When it
finally gets to run, it may be a long way away from where it
was originally created. It will have been created
in the service's JVM, transferred to the lookup locator by
register()
and then to the client's JVM by
lookup()
.
A client is trying to find a service, using some properties of
the service that it knows about. Whereas the service can export
a live object, the client cannot use a service object as a
property - because then it would already have the thing, and
wouldn't need to try to find one! What it can do is to use a
class object, and try to find if there are
any instances of this class lying around in
service locators. As discussed later in Client Search, it is best if
the client asks for an interface
class object. In
addition to this class specification, the client may specify a
set of attribute values that it requires from the service.
The next step is to look at the possible forms of attribute
values, and how matching will be performed. This is done using
Jini Entry objects. The
simplest services, and the least demanding clients, will not
require any attributes: the Entry[]
array will be
null
. You may wish to skip ahead to service registration
or to client search and
come back to entries later.
The ServiceRegistrar
is returned after a
successful discovery has been made. This object has a number
of methods that will return useful information about the
lookup service itself. So in addition to using this object to
register a service or lookup a service, you can use it to find
out about the lookup locator. The major methods are
String[] getGroups();
LookupLocator getLocator();
ServiceID getServiceID();
The method getGroups()
will return a list of the
groups that the locator is a member of.
The second method, getLocator()
is more
interesting. This is exactly the same object as is used in the
unicast lookup, but now its fields are filled in by the
discovery process. So you can find which host the locator is
running on, and its hostname by
registrar.getLocator().getHost();
This can
be used to find a list of which locators are running. From
there information such as host can be found - this will be
information filled in by the discovery process, rather than
being preset as in unicast lookup.
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
System.out.println("Service locator at " +
registrar.getLocator().getHost());
}
}
You
could use this to find where a service locator is so that the
next time this program runs it could
connect directly by unicast.
The third method getServiceID()
is unlikely to be
of much use to you. In general, service ID's are used to give
a globally identifier for the service (no different services
should have the same ID), and a service should have the same
ID with all service locators. However, this is the service ID
of the lookup service, not of any services registered with it.
Both services and clients need to find lookup services. Discovering a lookup service may be done using unicast or multicast protocols. Unicast discovery is a synchronous mechanism. Multicast discovery is an asynchronous mechanism that requires use of a listener to respond when a new service locator is discovered.
When a service locator is discovered, it sends a
ServiceRegistrar
object to run in the client or
service. This acts as a proxy for the locator. This object may
be queried for information, such as the host the service locator
is on.
If you found this chapter of value, the full book "Foundations of Jini 2 Programming" is available from APress or Amazon .
This work is licensed under a
Creative Commons License, the replacement for the earlier Open Content License.