|
Scott Oaks is a Systems Engineer for Sun Microsystems, where he focuses on practical
applications of Java technology. He is the co-author, with Henry Wong, of Java Threads.
He can be contacted at [email protected]
THIS MONTH'S QUESTION is about how a Java client application can receive and respond to
asynchronous messages from a Java server.
The premise behind this question is something that you're probably already aware of: There is no
Java API that allows asynchronous I/O. In fact, there is no asychronicity in Java at all except
for by creating different threads; although each thread is completely synchronous, the interleaving
of threads leads to asynchronous behavior.
Hence, the standard answer to this problem is simply to open up a socket to the server and read
that socket in a new thread. The thread will block until the server sends it data, at which point
it can deliver that data to other threads in the program.
This standard approach is often inconvenient to program. There are a lot of low-level threading
and communications issues to deal with; and on the client, we'd usually rather deal with event
callbacks than worry about the specifics of the threads and their I/O.
There's another technique in our arsenal, however, and that is the RMI callback. Using an RMI
callback allows us to deal with asynchronous messages from the server in a style more suited to
client-side programming (although, as we'll see, our programming will still not be strictly event-driven).
To use an RMI callback, we must define two RMI interfaces: the usual interface that our server will
implement and the interface that our callback will implement. For example, if we expect the server
to send us back simple strings, we might define the callback interface like this:
public interface Callback
extends Remote {
public void msg(String s)
throws RemoteException;
}
Among other things, our server interface must provide a mechanism for clients to register
instances of this callback, e.g.:
public interface Server
extends Remote {
...
public void register(Callback c)
throws RemoteException;
}
The server interface might also provide a method by which clients can de-register a callback,
or a callback can be automatically de-registered when the client it represents no longer exists.
That's the approach that we'll take in our server, a skeleton implementation of which looks
like this:
public class ServerImpl
extends UnicastRemoteObject
implements Server {
Vector v = new Vector();
public void
register(Callback c) {
v.addElement(c);
}
private void
broadcast(String s) {
Enumeration e = v.elements();
while (e.hasMoreElements()) {
Callback c = (Callback)
e.nextElement();
try { c.msg(s); }
catch (RemoteException e) {
v.removeElement(c);
}
}
}
}
Whenever the server has a message to send, it broadcasts it to all registered clients, removing any
who have disconnected. A client that is interested in such messages need only construct a class
that implements the Callback interface and pass that object to
the server. A skeleton implementation of such a client might look like this:
class ClientCB
extends UnicastRemoteObject
implements Callback {
Client c;
ClientCB(Client c) {
this.c = c;
}
public void msg(String s)
throws RemoteException {
c.handleMessage(s);
}
}
public class Client
extends Applet {
public void init() {
Server s = (Server)
Naming.lookup(...);
ClientCB cb =
new ClientCB(this);
s.register(cb);
}
void handleMessage(String s) {
...
}
}
In this client, the handleMessage() method is called whenever the server
has an asynchronous message to send to us. There's a strong similarity between this technique and the
event callbacks that make up a typical GUI-based client. Things happenbuttons are pressed, servers
send messagesand the appropriate method to handle the event is called. For most purposes, this
technique is sufficient. However, we need to consider some of the details of what's going on here.
To begin, although it's not readily apparent, there are a number of threads that are involved in the
client callback. This isn't surprising because we know that starting a new thread is the only way to
obtain asynchronous behavior in Java.
The most important ramification of this is that the callback methodthe
handleMessage() method in our exampleis not called from the
event dispatching thread of a GUI-based applet. So even though we've gone to some effort to make
programming our callback look just like any other callback, we haven't quite succeeded: Because the
handleMessage() method is not called from the event dispatching
thread, it cannot directly manipulate any Swing component in the client.
Hence, if the callback needs to access Swing elements, we must
rewrite the msg() method of the callback class as follows:
public void msg(String s)
throws RemoteException {
SwingUtilities.invokeLater(
new Runnable() {
public void run() {
c.handleMessage(s);
}
}
);
}
This change is not needed if our client does not have a GUI or if its
handleMessage() method does not access any GUI components.
In Java 2, this implementation still isn't sufficient because the default Java 2 security manager
will interfere with this activity. When the invokeLater()
method attempts to post an event to the event queue (so that the
handleMessage() method can later be run by the event-dispatching thread),
the security manager will notice that an RMI stub method is on the stack. And stub methods by default
do not have permission to post an event to the event queue. Hence, on the Java 2 platform, the
client code must be written as follows:
public void msg(String s)
throws RemoteException {
AccessController.doPrivileged(
new PrivilegedAction() {
public Object run() {
SwingUtilities.
invokeLater(
...
);
return null;
}
}
);
}
This allows the invokeLater() method to post the necessary event
to the event queue so that its target object may be run (assuming, of course, that the client code
itself has the necessary permission to post the event, which is the default behavior).
Another important ramification of the fact that the callback method is called from a hidden thread
lies in the possibility that the callback method may attempt to invoke a synchronized method that
will result in an unusual type of deadlock. This typically happens when the other threads in the
client are in the process of a call to the server when the callback is invoked. If the methods
involved in the calls to the server are synchronized, and the callback method itself attempts
to grab the same synchronization lock, things will deadlock: The call to the server will not
complete until the callback completes, and the callback cannot complete because it cannot obtain
the correct lock. This can also happen if the methods in the server are synchronized, and the
client ends up calling the server from both its default thread and its callback thread.
The easy way to avoid this possibility is to use the code that we've just shown: The callback,
rather than actually performing any work, simply posts an event somewhere. Then the other threads
of the client can process that event when convenient. If all such processing occurs in the same
thread (e.g., the event-dispatching thread), this type of deadlock will be avoided.
With a little bit of care then, the RMI callback offers a convenient way to obtain asynchronous results
from a server without having to worry low-level threading or I/O interfaces.
Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by
email: [email protected].
|