The daytime server example from the last section demonstrated the nuts and bolts of using each of the three communication techniques for applet-servlet communication. It didn't take advantage, though, of the persistence gains when using socket communication. Nor did it show off the simplicity of RMI communication or the elegance of RMI callbacks (where the servlet can invoke methods of the applet). It also didn't provide a compelling reason for why one servlet should support all the communication techniques--there was no state to maintain or complicated code base to collect in one location. So, before we end our discussion of applet-servlet communication, let's look at a more sophisticated example: a chat server, implemented as a servlet, that supports clients connecting via HTTP, non-HTTP sockets, and RMI.
We'll build this chat server using all three communication techniques so that it can take advantage of the best, most efficient solution for each client. For example, when the client supports RMI, the servlet can be treated as a remote object, and (where possible) it can treat the applet as a remote object, too. When the client doesn't support RMI but can support direct socket communication, the chat server can utilize socket persistence and communicate with the client using a non-HTTP socket protocol. And, of course, when all else fails, the chat server can fall back to using HTTP. It would rather not fall back because HTTP, being stateless, requires that the client poll for updates. But for many clients, HTTP is the only choice.
The chat server is implemented as a single class with a single instantiation because it has a large amount of associated state and a fair amount of code that would otherwise have to be repeated. To separate it into three classes, one for each protocol, would demand excessive interserver communication and replicate the core chat server code three times. Implementing the chat server as a servlet provides a simple way for one object to make itself available via all three communication techniques. By being an HTTP servlet, it has built-in HTTP support. And by extending the RemoteDaemonHttpServlet class, it can also easily gain support for non-HTTP socket and RMI communication.
Note that although you'll see the code in its entirety, we won't be fully explaining each and every line. To do so would extend this chapter beyond a reasonable length, assuming we aren't there already. Therefore, we'll explain the issues as they concern applet-servlet communication and rely on you to examine the code to understand all the details.
Figure 10-3 shows the chat applet in action. Notice that it uses a large TextArea component to display the running conversation, with a small TextInput component underneath where the user can post a new single-line message. As each contributor composes a message, it's sent to the chat server and distributed to the other chat clients in various ways.
HTTP chat clients post their messages to the server using the HTTP POST method. The applet takes the new message from the TextInput component when the user hits Enter, URL-encodes the message, and posts it to the servlet as a message parameter. It's all very straightforward. What is a bit more complicated is how an HTTP chat client manages to get the other clients' messages. It uses the HTTP GET method to receive each message, but it has a problem: it doesn't know when exactly there's a new message to get. This is the problem with a unidirectional request/response communication paradigm. The client has to either periodically poll for updates or simulate bidirectional communication by making a series of blocking GET requests. By that we mean the chat client initiates a GET request that blocks until the server decides it's time to return something. For our example, we implement this simulated bidirectional communication.
Socket chat clients, for the sake of convenience, post their messages to the server the same way HTTP chat clients do, with the HTTP POST method. They could post their messages using raw socket connections, but only with a marginal gain in efficiency that, at least in this case, doesn't outweigh the increased complexity. These socket clients, however, do use raw sockets to get messages from the other clients, replacing the simulated bidirectional communication with actual bidirectional communication. As each new message comes in to the servlet, it's sent right away from the servlet to the socket chat clients across plain-text socket connections.
RMI chat clients perform their POSTs and their GETs using method invocations. To post each new message, the applet simply calls the remote servlet's broadcastMessage(String) method. To get new messages, it has two options. It can call the servlet's blocking getNextMessage() method or, through the use of callbacks, it can ask the servlet to call its own setNextMessage(String) method every time there's a new message broadcast. We've chosen to use the callback option in our example.
In front of all these applets is a dispatch servlet. It lets the user choose the applet-servlet communication technique (HTTP, socket, or RMI) he wants to use and, based on his choice, generates a page that contains the appropriate applet. It's true that a single applet could be written to support all three techniques and auto-select between them based on its runtime environment, but to do that here would unnecessarily complicate our example. The dispatch servlet also tells the applet the name of its user, but more on that later.
The full listings for the ChatServer interface and the ChatServlet class that implements it are given in Example 10-15 and Example 10-16.
import java.rmi.Remote; import java.rmi.RemoteException; public interface ChatServer extends Remote { public String getNextMessage() throws RemoteException; public void broadcastMessage(String message) throws RemoteException; public void addClient(ChatClient client) throws RemoteException; public void deleteClient(ChatClient client) throws RemoteException; }
import java.io.*; import java.net.*; import java.rmi.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.RemoteDaemonHttpServlet; public class ChatServlet extends RemoteDaemonHttpServlet implements ChatServer { // source acts as the distributor of new messages MessageSource source = new MessageSource(); // socketClients holds references to all the socket-connected clients Vector socketClients = new Vector(); // rmiClients holds references to all the RMI clients Vector rmiClients = new Vector(); // doGet() returns the next message. It blocks until there is one. public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); // Return the next message (blocking) out.println(getNextMessage()); } // doPost() accepts a new message and broadcasts it to all // the currently listening HTTP and socket clients. public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // Accept the new message as the "message" parameter String message = req.getParameter("message"); // Broadcast it to all listening clients if (message != null) broadcastMessage(message); // Set the status code to indicate there will be no response res.setStatus(res.SC_NO_CONTENT); } // getNextMessage() returns the next new message. // It blocks until there is one. public String getNextMessage() { // Create a message sink to wait for a new message from the // message source. return new MessageSink().getNextMessage(source); } // broadcastMessage() informs all currently listening clients that there // is a new message. Causes all calls to getNextMessage() to unblock. public void broadcastMessage(String message) { // Send the message to all the HTTP-connected clients by giving the // message to the message source source.sendMessage(message); // Directly send the message to all the socket-connected clients Enumeration enum = socketClients.elements(); while (enum.hasMoreElements()) { Socket client = null; try { client = (Socket)enum.nextElement(); PrintStream out = new PrintStream(client.getOutputStream()); out.println(message); } catch (IOException e) { // Problem with a client, close and remote it try { if (client != null) client.close(); } catch (IOException ignored) { } socketClients.removeElement(client); } } // Directly send the message to all RMI clients enum = rmiClients.elements(); while (enum.hasMoreElements()) { ChatClient chatClient = null; try { chatClient = (ChatClient)enum.nextElement(); chatClient.setNextMessage(message); } catch (RemoteException e) { // Problem communicating with a client, remove it deleteClient(chatClient); } } } protected int getSocketPort() { // We listen on port 2428 (look at a phone to see why) return 2428; } public void handleClient(Socket client) { // We have a new socket client. Add it to our list. socketClients.addElement(client); } public void addClient(ChatClient client) { // We have a new RMI client. Add it to our list. rmiClients.addElement(client); } public void deleteClient(ChatClient client) { // Remote the specified client from our list. rmiClients.removeElement(client); } } // MessageSource acts as the source for new messages. // Clients interested in receiving new messages can // observe this object. class MessageSource extends Observable { public void sendMessage(String message) { setChanged(); notifyObservers(message); } } // MessageSink acts as the receiver of new messages. // It listens to the source. class MessageSink implements Observer { String message = null; // set by update() and read by getNextMessage() // Called by the message source when it gets a new message synchronized public void update(Observable o, Object arg) { // Get the new message message = (String)arg; // Wake up our waiting thread notify(); } // Gets the next message sent out from the message source synchronized public String getNextMessage(MessageSource source) { // Tell source we want to be told about new messages source.addObserver(this); // Wait until our update() method receives a message while (message == null) { try { wait(); } catch (Exception ignored) { } } // Tell source to stop telling us about new messages source.deleteObserver(this); // Now return the message we received // But first set the message instance variable to null // so update() and getNextMessage() can be called again. String messageCopy = message; message = null; return messageCopy; } }
The getNextMessage() and broadcastMessage(String message) methods are most interesting portions of ChatServlet. The getNextMessage() method returns the next new message as it comes in, blocking until there is one. To enable this blocking, it uses the MessageSource and MessageSink classes. Without getting too deep into the details of these two classes, we'll just say this: the servlet constructs a new MessageSink and asks this sink to get the next message from the source. To accomplish this, the sink registers itself as an observer of source and calls wait() to block. When the source receives a new message, the sink (being an observer) is notified of the change with a call to its update() method. The sink's update() method saves the source's latest message in its message variable and then calls notify(). This causes its getNextMessage() method to unblock and return the message.
The broadcastMessage() method tells all listening clients when there's a new message. It notifies HTTP clients by sending the message to the MessageSource; other clients it notifies directly by looping through its client list. For each of its socket-connected clients, it prints the message to the client's socket. For each of its RMI clients, it calls the client's setNextMessage(String) method. This is the callback we've been talking about. If, at any point, there's a problem with a socket or RMI client, it removes that client from its list.
The two lists, socketClients and rmiClients, are populated as the servlet hears from clients. When a socket client connects, the servlet's handleClient(Socket) method is called and the new client is added to the socketClientsVector. RMI clients have to add themselves to the list by invoking the servlet's addClient(ChatClient) method.
The doGet() and doPost() methods of ChatServlet are essentially thin wrappers around the getNextMessage() and broadcastMessage() methods. The doGet() wrapper is so thin you can almost see through it: doGet() sends as its response whatever String is returned by getNextMessage(). The doPost() wrapper is a bit less transparent. It extracts the posted message from the POST form data's "message" parameter, broadcasts the message by passing it to the broadcastMessage() method, and sets its response's status code to SC_NO_CONTENT to indicate there is no content in the response. In a sense, making a GET request is equivalent to calling getNextMessage(), and making a POST request is equivalent to calling broadcastMessage().
Did you notice which socket port ChatServlet listens on? It's 2428. Overriding the getSocketPort() method as ChatServlet does is an easy way to set the socket port when you don't want to use an init parameter.
The code for our first applet, the HTTP chat applet, is shown in Example 10-17.
import java.applet.*; import java.awt.*; import java.io.*; import java.net.*; import java.util.*; import com.oreilly.servlet.HttpMessage; public class HttpChatApplet extends Applet implements Runnable { TextArea text; Label label; TextField input; Thread thread; String user; public void init() { // Check if this applet was loaded directly from the filesystem. // If so, explain to the user that this applet needs to be loaded // from a server in order to communicate with that server's servlets. URL codebase = getCodeBase(); if (!"http".equals(codebase.getProtocol())) { System.out.println(); System.out.println("*** Whoops! ***"); System.out.println("This applet must be loaded from a web server."); System.out.println("Please try again, this time fetching the HTML"); System.out.println("file containing this servlet as"); System.out.println("\"http://server:port/file.html\"."); System.out.println(); System.exit(1); // Works only from appletviewer // Browsers throw an exception and muddle on } // Get this user's name from an applet parameter set by the servlet // We could just ask the user, but this demonstrates a // form of servlet->applet communication. user = getParameter("user"); if (user == null) user = "anonymous"; // Set up the user interface... // On top, a large TextArea showing what everyone's saying. // Underneath, a labeled TextField to accept this user's input. text = new TextArea(); text.setEditable(false); label = new Label("Say something: "); input = new TextField(); input.setEditable(true); setLayout(new BorderLayout()); Panel panel = new Panel(); panel.setLayout(new BorderLayout()); add("Center", text); add("South", panel); panel.add("West", label); panel.add("Center", input); } public void start() { thread = new Thread(this); thread.start(); } String getNextMessage() { String nextMessage = null; while (nextMessage == null) { try { URL url = new URL(getCodeBase(), "/servlet/ChatServlet"); HttpMessage msg = new HttpMessage(url); InputStream in = msg.sendGetMessage(); DataInputStream data = new DataInputStream( new BufferedInputStream(in)); nextMessage = data.readLine(); } catch (SocketException e) { // Can't connect to host, report it and wait before trying again System.out.println("Can't connect to host: " + e.getMessage()); try { Thread.sleep(5000); } catch (InterruptedException ignored) { } } catch (FileNotFoundException e) { // Servlet doesn't exist, report it and wait before trying again System.out.println("Resource not found: " + e.getMessage()); try { Thread.sleep(5000); } catch (InterruptedException ignored) { } } catch (Exception e) { // Some other problem, report it and wait before trying again System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); try { Thread.sleep(1000); } catch (InterruptedException ignored) { } } } return nextMessage + "\n"; } public void run() { while (true) { text.appendText(getNextMessage()); } } public void stop() { thread.stop(); thread = null; } void broadcastMessage(String message) { message = user + ": " + message; // Pre-pend the speaker's name try { URL url = new URL(getCodeBase(), "/servlet/ChatServlet"); HttpMessage msg = new HttpMessage(url); Properties props = new Properties(); props.put("message", message); msg.sendPostMessage(props); } catch (SocketException e) { // Can't connect to host, report it and abandon the broadcast System.out.println("Can't connect to host: " + e.getMessage()); } catch (FileNotFoundException e) { // Servlet doesn't exist, report it and abandon the broadcast System.out.println("Resource not found: " + e.getMessage()); } catch (Exception e) { // Some other problem, report it and abandon the broadcast System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); } } public boolean handleEvent(Event event) { switch (event.id) { case Event.ACTION_EVENT: if (event.target == input) { broadcastMessage(input.getText()); input.setText(""); return true; } } return false; } }
This applet has the same two workhorse methods as ChatServlet: getNextMessage() and broadcastMessage(). Its getNextMessage() method gets the next message from the servlet. It's called repeatedly to update the TextArea. It operates using an HttpMessage to make a GET request to the servlet, then interprets the first line of the response as the next new message. Its broadcastMessage() method sends a message to the servlet for distribution to the other clients. This method is called in the applet's handleEvent() method every time the user hits Enter in the TextInput component. It works similarly to getNextMessage(). It uses an HttpMessage to perform a POST request, passing the TextInput's text as the "message" parameter, and it doesn't bother to read the response.
The only difference between the socket-based SocketChatApplet and the HTTP-based HttpChatApplet is a redesigned getNextMessage() method. This method is shown in Example 10-18.
static final int PORT = 2428; DataInputStream serverStream; String getNextMessage() { String nextMessage = null; while (nextMessage == null) { try { // Connect to the server if we haven't before if (serverStream == null) { Socket s = new Socket(getCodeBase().getHost(), PORT); serverStream = new DataInputStream( new BufferedInputStream( s.getInputStream())); } // Read a line nextMessage = serverStream.readLine(); } catch (SocketException e) { // Can't connect to host, report it and wait before trying again System.out.println("Can't connect to host: " + e.getMessage()); serverStream = null; try { Thread.sleep(5000); } catch (InterruptedException ignored) { } } catch (Exception e) { // Some other problem, report it and wait before trying again System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); try { Thread.sleep(1000); } catch (InterruptedException ignored) { } } } return nextMessage + "\n"; }
This method reads broadcast messages from a socket that's connected to the chat servlet. It uses a simple socket protocol: all content is plain text, one message per line. The first time this method is called, it establishes the socket connection and then uses the connection to get a DataInputStream, where it can read from the socket one line at a time. It reads the first line from this stream and returns the text as the next message. For each subsequent invocation, it reuses the same stream and simply returns the next line it reads. If there's ever a SocketException, it reestablishes the connection.
The code for the ChatClient interface is shown in Example 10-19; the RMI-based chat applet that implements it is shown in Example 10-20.
import java.rmi.Remote; import java.rmi.RemoteException; public interface ChatClient extends Remote { public void setNextMessage(String message) throws RemoteException; }
import java.applet.*; import java.awt.*; import java.io.*; import java.net.*; import java.rmi.*; import java.rmi.registry.*; import java.rmi.server.*; import java.util.*; public class RMIChatApplet extends Applet implements ChatClient { TextArea text; Label label; TextField input; Thread thread; String user; ChatServer chatServer; private int getRegistryPort() { try { return Integer.parseInt(getParameter("port")); } catch (NumberFormatException ignored) { return Registry.REGISTRY_PORT; } } private String getRegistryName() { String name = getParameter("name"); return (name == null ? "ChatServlet" : name); } // Returns a reference to the remote chat server/servlet // Tries to exit if there's a problem. private ChatServer getChatServer() { try { Registry registry = LocateRegistry.getRegistry(getCodeBase().getHost(), getRegistryPort()); Object obj = registry.lookup(getRegistryName()); return (ChatServer)obj; } catch (java.rmi.UnknownHostException e) { // Don't know the registry host, try to exit System.out.println("Host unknown in url: " + e.getMessage()); System.exit(1); } catch (NotBoundException e) { // Can't find our object, try to exit System.out.println("Name not bound: " + e.getMessage()); System.exit(1); } catch (ClassCastException e) { // The object wasn't a ChatServer, try to exit System.out.println(getRegistryName() + " was not a ChatServer:" + e.getMessage()); System.exit(1); } catch (RemoteException e) { // General RMI problem, try to exit System.out.println("Remote exception: " + e.getMessage()); System.exit(1); } catch (Exception e) { // Some other problem, try to exit System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); System.exit(1); } return null; // return null if the exit() doesn't work } // Add ourselves as a client of the chat server // Notice there's no need for an RMI registry private void registerWithChatServer(ChatServer server) { try { UnicastRemoteObject.exportObject(this); server.addClient(this); } catch (RemoteException e) { // General RMI problem, try to exit System.out.println("Remote exception: " + e.getMessage()); System.exit(1); } catch (Exception e) { // Some other problem, try to exit System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); System.exit(1); } } public void init() { // Check if this applet was loaded directly from the filesystem. // If so, explain to the user that this applet needs to be loaded // from a server in order to communicate with that server's servlets. URL codebase = getCodeBase(); if (!"http".equals(codebase.getProtocol())) { System.out.println(); System.out.println("*** Whoops! ***"); System.out.println("This applet must be loaded from a web server."); System.out.println("Please try again, this time fetching the HTML"); System.out.println("file containing this servlet as"); System.out.println("\"http://server:port/file.html\"."); System.out.println(); System.exit(1); // Works only from appletviewer // Browsers throw an exception and muddle on } // Get the remote chat server chatServer = getChatServer(); // Register ourselves as one of its clients registerWithChatServer(chatServer); // Get this user's name from an applet parameter set by the dispatch servlet // We could just ask the user, but this demonstrates a // form of servlet->applet communication. user = getParameter("user"); if (user == null) user = "anonymous"; // Set up the user interface... // On top, a large TextArea showing what everyone's saying. // Underneath, a labeled TextField to accept this user's input. text = new TextArea(); text.setEditable(false); label = new Label("Say something: "); input = new TextField(); input.setEditable(true); setLayout(new BorderLayout()); Panel panel = new Panel(); panel.setLayout(new BorderLayout()); add("Center", text); add("South", panel); panel.add("West", label); panel.add("Center", input); } String getNextMessage() { String nextMessage = null; while (nextMessage == null) { try { nextMessage = chatServer.getNextMessage(); } catch (RemoteException e) { // Remote exception, report and wait before trying again System.out.println("Remote Exception:" + e.getMessage()); try { Thread.sleep(1000); } catch (InterruptedException ignored) { } } } return nextMessage + "\n"; } public void setNextMessage(String message) { text.appendText(message + "\n"); } void broadcastMessage(String message) { message = user + ": " + message; // Pre-pend the speaker's name try { chatServer.broadcastMessage(message); } catch (RemoteException e) { // Remote exception, report it and abandon the broadcast System.out.println("Remote exception: " + e.getMessage()); } catch (Exception e) { // Some other exception, report it and abandon the broadcast System.out.println("General exception: " + e.getClass().getName() + ": " + e.getMessage()); } } public boolean handleEvent(Event event) { switch (event.id) { case Event.ACTION_EVENT: if (event.target == input) { broadcastMessage(input.getText()); input.setText(""); return true; } } return false; } }
This applet's getNextMessage() and broadcastMessage() implementations are as simple as any we've seen. They need only call the remote servlet's methods of the same name. But their simplicity comes with a cost: more complicated set-up code. Specifically, the init() method now has to call the lengthy (but by now understandable) getChatServer() method to obtain a reference to the remote chat servlet.
If you look closely at RMIChatApplet, you'll notice that it doesn't actually use its getNextMessage() method. Instead, it asks the servlet to call its setNextMessage() method each time there's a new message being broadcast. It makes this request in its init() method when it calls registerWithChatSer-ver(ChatServer). This method exports the applet as a remote object, then invokes the servlet's addClient() method passing a reference to itself. After this, the servlet's broadcastMessage() method sends a callback to the applet each time there's a new message.
If you try using callbacks on your own, don't forget the basics we covered earlier. You need to run the rmic RMI compiler on your remote applet to generate its stub and skeleton classes. And you need to be sure your server has the RMIChatApplet_Stub.class and ChatClient.class files somewhere in its classpath.
Now, for this chapter's last code example, the ChatDispatch servlet is shown in Example 10-21. This servlet performs two duties. First, when this servlet is accessed without any request parameters, it prints a friendly welcome page asking the user which applet version he is interested in using, as shown in Figure 10-4. Second, when it's accessed with a request parameter, it prints a page that contains the appropriate applet, as you saw in Figure 10-3. Be aware that the URL used to access this dispatch servlet should contain the server's true name, not localhost, so as to avoid RMI security problems.
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class ChatDispatch extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { res.setContentType("text/html"); if (!req.getParameterNames().hasMoreElements()) { // There were no request parameters. Print a welcome page. printWelcomePage(req, res); } else { // There was at least one request parameter. // Print a page containing the applet. printAppletPage(req, res); } } // The welcome page greets the reader and has a form where the user // can choose an applet-servlet communication method. private void printWelcomePage(HttpServletRequest req, HttpServletResponse res) throws IOException { PrintWriter out = res.getWriter(); String me = req.getServletPath(); out.println("<HTML>"); out.println("<HEAD><TITLE>"); out.println("Welcome to an Absurdly Simple Chat"); out.println("</TITLE></HEAD>"); out.println(); out.println("<BODY>"); out.println("<H1>Welcome to an Absurdly Simple Chat</H1>"); out.println(); out.println("Would you like to communicate via:"); out.println("<UL>"); out.println(" <LI><A href=\"" + me + "?method=http\">http</A>"); out.println(" <LI><A href=\"" + me + "?method=socket\">socket</A>"); out.println(" <LI><A href=\"" + me + "?method=rmi\">rmi</A>"); out.println("</UL>"); out.println("</BODY></HTML>"); } // The applet page displays the chat applet. private void printAppletPage(HttpServletRequest req, HttpServletResponse res) throws IOException { PrintWriter out = res.getWriter(); out.println("<HTML>"); out.println("<HEAD><TITLE>An Absurdly Simple Chat</TITLE></HEAD>"); out.println("<BODY>"); out.println("<H1>An Absurdly Simple Chat</H1>"); String method = req.getParameter("method"); String user = req.getRemoteUser(); String applet = null; if ("http".equals(method)) { applet = "HttpChatApplet"; } else if ("socket".equals(method)) { applet = "SocketChatApplet"; } else if ("rmi".equals(method)) { applet = "RMIChatApplet"; } else { // No method given, or an invalid method given. // Explain to the user what we expect. out.println("Sorry, this servlet requires a <TT>method</TT> " + "parameter with one of these values: " + "http, socket, rmi"); return; } // Print the HTML code to generate the applet. // Choose the applet code based on the method parameter. // Provide a user parameter if we know the remote user. out.println("<APPLET CODE=" + applet + " CODEBASE=/ " + "WIDTH=500 HEIGHT=170>"); if (user != null) out.println("<PARAM NAME=user VALUE=\"" + user + "\">"); out.println("</APPLET>"); out.println("</BODY></HTML>"); } }
Nothing here should surprise you. In fact, we expect this code to appear refreshingly simple after the ChatServlet example. Still, this example does demonstrate one last form of applet-servlet communication: servlet-generated applet parameters. Using this technique, a servlet generates a page that contains an applet and passes information to the applet by manipulating the applet's <PARAM> tags. Any information the servlet wants to send to a new applet can be sent this way. In this example, the servlet sends the name returned by req.getRemoteUser(). In another example, a servlet could tell the applet its browser type by sending it the string returned by req.getHeader("User-Agent"). Or, to be more helpful, the servlet could use a database to determine the capabilities of the browser and tell the applet exactly what it needs to know. It could even tell the applet whether the browser supports RMI communication.
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |