10.2. Daytime ServerFor a simple demonstration of each communication technique, we're going to write an applet that asks its server for the current time of day. The applet first uses an HTTP connection, then a non-HTTP socket connection, and finally an RMI connection. Of course, an applet can normally get the current time from the system on which it's running. To give this example an air of practicality, let's assume the applet needs an approximate time stamp for some event and cannot rely on the client machine to have a correctly set clock. 10.2.1. The AppletWe're going to be using the same example applet throughout this section. The skeleton code for this applet, DaytimeApplet, is shown in Example 10-1. Right now, the applet just creates a user interface where the times it retrieves can be displayed, as shown in Figure 10-1. As we proceed with this example, we'll implement its getDateUsingHttpText(), getDateUsingHttpObject(), getDateUsingSocketText(), getDateUsingSocketObject(), and getDateUsingRMIObject() methods. Note that the examples in this chapter use several JDK 1.0 methods that are deprecated in JDK 1.1. This is to maximize portability. Figure 10-1. The DaytimeApplet user interfaceExample 10-1. DaytimeApplet, without all the good stuffimport java.applet.*; import java.awt.*; import java.io.*; import java.util.*; public class DaytimeApplet extends Applet { TextField httpText, httpObject, socketText, socketObject, RMIObject; Button refresh; public void init() { // Construct the user interface setLayout(new BorderLayout()); // On the left create labels for the various communication // mechanisms Panel west = new Panel(); west.setLayout(new GridLayout(5, 1)); west.add(new Label("HTTP text: ", Label.RIGHT)); west.add(new Label("HTTP object: ", Label.RIGHT)); west.add(new Label("Socket text: ", Label.RIGHT)); west.add(new Label("Socket object: ", Label.RIGHT)); west.add(new Label("RMI object: ", Label.RIGHT)); add("West", west); // On the right create text fields to display the retrieved time values Panel center = new Panel(); center.setLayout(new GridLayout(5, 1)); httpText = new TextField(); httpText.setEditable(false); center.add(httpText); httpObject = new TextField(); httpObject.setEditable(false); center.add(httpObject); socketText = new TextField(); socketText.setEditable(false); center.add(socketText); socketObject = new TextField(); socketObject.setEditable(false); center.add(socketObject); RMIObject = new TextField(); RMIObject.setEditable(false); center.add(RMIObject); add("Center", center); // On the bottom create a button to update the times Panel south = new Panel(); refresh = new Button("Refresh"); south.add(refresh); add("South", south); } public void start() { refresh(); } private void refresh() { // Fetch and display the time values httpText.setText(getDateUsingHttpText()); httpObject.setText(getDateUsingHttpObject()); socketText.setText(getDateUsingSocketText()); socketObject.setText(getDateUsingSocketObject()); RMIObject.setText(getDateUsingRMIObject()); } private String getDateUsingHttpText() { // Retrieve the current time using an HTTP text-based connection return "unavailable"; } private String getDateUsingHttpObject() { // Retrieve the current time using an HTTP object-based connection return "unavailable"; } private String getDateUsingSocketText() { // Retrieve the current time using a non-HTTP text-based socket // connection return "unavailable"; } private String getDateUsingSocketObject() { // Retrieve the current time using a non-HTTP object-based socket // connection return "unavailable"; } private String getDateUsingRMIObject() { // Retrieve the current time using RMI communication return "unavailable"; } public boolean handleEvent(Event event) { // When the refresh button is pushed, refresh the display // Use JDK 1.0 events for maximum portability switch (event.id) { case Event.ACTION_EVENT: if (event.target == refresh) { refresh(); return true; } } return false; } } For this applet to be available for downloading to the client browser, it has to be placed under the server's document root, along with an HTML file referring to it. The HTML might look like this: <HTML> <HEAD><TITLE>Daytime Applet</TITLE></HEAD> <BODY> <CENTER><H1>Daytime Applet</H1></CENTER> <CENTER><APPLET CODE=DaytimeApplet CODEBASE=/ WIDTH=300 HEIGHT=180> </APPLET></CENTER> </BODY></HTML> The CODEBASE parameter indicates the directory where the applet's class file has been placed. The parameter is relative to the document root, which for the Java Web Server is generally server_root/public_html. Assuming the HTML file was named daytime.html, this applet can be viewed at the URL http://server:port/daytime.html. 10.2.2. Text-based HTTP CommunicationLet's start by implementing the lowest-common-denominator approach-- text-based HTTP communication. 10.2.2.1. The servletFor the DaytimeApplet to retrieve the current time from the server, it has to communicate with a servlet that returns the current time. Example 10-2 shows such a servlet. It responds to all GET and POST requests with a textual representation of the current time. Example 10-2. The DaytimeServlet supporting basic HTTP accessimport java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class DaytimeServlet extends HttpServlet { public Date getDate() { return new Date(); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { res.setContentType("text/plain"); PrintWriter out = res.getWriter(); out.println(getDate().toString()); } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doGet(req, res); } } This servlet's class files should be placed in the standard location for servlets, typically server_root/servlets. Once you place them there, they can be accessed by any web browser using the URL http://server:port/servlet/DaytimeServlet. 10.2.2.2. Back to the appletNow, for our DaytimeApplet to access this servlet, it must behave just like a browser and make an HTTP connection to the servlet URL, as the implementation of getDateUsingHttpText() in Example 10-3 shows. Example 10-3. DaytimeApplet getting the time using HTTPimport java.net.URL; // New addition import com.oreilly.servlet.HttpMessage; // A support class, shown later private String getDateUsingHttpText() { try { // Construct a URL referring to the servlet URL url = new URL(getCodeBase(), "/servlet/DaytimeServlet"); // Create a com.oreilly.servlet.HttpMessage to communicate with that URL HttpMessage msg = new HttpMessage(url); // Send a GET message to the servlet, with no query string // Get the response as an InputStream InputStream in = msg.sendGetMessage(); // Wrap the InputStream with a DataInputStream DataInputStream result = new DataInputStream(new BufferedInputStream(in)); // Read the first line of the response, which should be // a string representation of the current time String date = result.readLine(); // Close the InputStream in.close(); // Return the retrieved time return date; } catch (Exception e) { // If there was a problem, print to System.out // (typically the Java console) and return null e.printStackTrace(); return null; } } This method retrieves the current time on the server using a text-based HTTP connection. First, it creates a URL object that refers to the DaytimeServlet running on the server. The server host and port for this URL come from the applet's own getCodeBase() method. This guarantees that it matches the host and port from which the applet was downloaded. Then, the method creates an HttpMessage object to communicate with that URL. This object does all the dirty work involved in making the connection. The applet asks it to make a GET request of the DaytimeServlet and then reads the response from the returned InputStream. The code for HttpMessage is shown in Example 10-4. It is loosely modeled after the ServletMessage class written by Rod McChesney of Sun Microsystems. Example 10-4. The HttpMessage support classpackage com.oreilly.servlet; import java.io.*; import java.net.*; import java.util.*; public class HttpMessage { URL servlet = null; String args = null; public HttpMessage(URL servlet) { this.servlet = servlet; } // Performs a GET request to the previously given servlet // with no query string. public InputStream sendGetMessage() throws IOException { return sendGetMessage(null); } // Performs a GET request to the previously given servlet. // Builds a query string from the supplied Properties list. public InputStream sendGetMessage(Properties args) throws IOException { String argString = ""; // default if (args != null) { argString = "?" + toEncodedString(args); } URL url = new URL(servlet.toExternalForm() + argString); // Turn off caching URLConnection con = url.openConnection(); con.setUseCaches(false); return con.getInputStream(); } // Performs a POST request to the previously given servlet // with no query string. public InputStream sendPostMessage() throws IOException { return sendPostMessage(null); } // Performs a POST request to the previously given servlet. // Builds post data from the supplied Properties list. public InputStream sendPostMessage(Properties args) throws IOException { String argString = ""; // default if (args != null) { argString = toEncodedString(args); // notice no "?" } URLConnection con = servlet.openConnection(); // Prepare for both input and output con.setDoInput(true); con.setDoOutput(true); // Turn off caching con.setUseCaches(false); // Work around a Netscape bug con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // Write the arguments as post data DataOutputStream out = new DataOutputStream(con.getOutputStream()); out.writeBytes(argString); out.flush(); out.close(); return con.getInputStream(); } // Converts a Properties list to a URL-encoded query string private String toEncodedString(Properties args) { StringBuffer buf = new StringBuffer(); Enumeration names = args.propertyNames(); while (names.hasMoreElements()) { String name = (String) names.nextElement(); String value = args.getProperty(name); buf.append(URLEncoder.encode(name) + "=" + URLEncoder.encode(value)); if (names.hasMoreElements()) buf.append("&"); } return buf.toString(); } } Some of you may have been expecting the HttpMessage class to establish a raw socket connection to the server and proceed to speak HTTP. This approach would certainly work, but it isn't necessary. The higher-level java.net.URL and java.net.URLConnection classes already provide this functionality in a convenient abstraction. Let's do a quick walk-through of HttpMessage. HttpMessage is designed to communicate with just one URL, the URL given in its constructor. It can send multiple GET and/or POST requests to that URL, but it always communicates with just the one URL. The code HttpMessage uses to send a GET message is fairly simple. First, sendGetMessage() creates a URL-encoded query string from the passed-in java.util.Properties list. Then, it appends this query string to the saved URL, creating a new URL object. At this point, it could elect to use this new URL (named url) to communicate with the servlet. A call to url.openStream() would return an InputStream that contains the response. But, unfortunately for our purposes, by default all connections made using a URL object are cached. We don't want this--we want the current time, not the time of the last request. So HttpMessage has to turn caching off.[3] The URL class doesn't directly support this low-level control, so HttpMessage gets the URL object's URLConnection and instructs it not to use caching. Finally, HttpMessage returns the URLConnection object's InputStream, which contains the servlet's response.
The code HttpMessage uses to send a POST request (sendPostMessage()) is similar. The major difference is that it directly writes the URL-encoded parameter information in the body of the request. This follows the protocol for how POST requests submit their information. The other difference is that HttpMessage manually sets the request's content type to "application/x-www-form-urlencoded". This should be set automatically by Java, but setting it manually works around a bug in some versions of Netscape's browser. We should mention that HttpMessage is a general-purpose class for HTTP communication. It doesn't have to be used by applets, and it doesn't have to connect to servlets. It's usable by any Java client that needs to connect to an HTTP resource. It's included in the com.oreilly.servlet package, though, because it's often useful for applet-servlet communication. For the HttpMessage class to be usable by applets, it has to be made available for downloading along with the applet classes. This means it must be placed in the proper location under the web server's document root. For the Java Web Server, this location is server_root/public_html/com/oreilly/servlet. We recommend you copy the class there from wherever you originally installed the com.oreilly.servlet package (probably server_root/classes/com/oreilly/servlet). Note that HttpMessage as currently written does not provide a mechanism for an applet to either set or get the HTTP headers associated with its request and response. The URLConnection class, however, supports HTTP header access with its setRequestProperty() and getHeaderField() methods. You can add this functionality if you need it. Now, with all this code working together, we have an applet that retrieves the current time from its server using text-based HTTP applet-servlet communication. If you try it yourself, you should see the "HTTP text" date filled in, while the rest of the dates are still marked "unavailable." 10.2.3. Object-based HTTP CommunicationWith a few modifications, we can have the DaytimeApplet receive the current time as a serialized Date object. 10.2.3.1. The servletFor backward compatibility, let's change our DaytimeServlet to return a serialized Date only if the request asks for it by passing a "format" parameter with the value "object". The code is given in Example 10-5. Example 10-5. The DaytimeServlet using HTTP to serve an objectimport java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public class DaytimeServlet extends HttpServlet { public Date getDate() { return new Date(); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // If the client says "format=object" then // return the Date as a serialized object if ("object".equals(req.getParameter("format"))) { ObjectOutputStream out = new ObjectOutputStream(res.getOutputStream()); out.writeObject(getDate()); } // Otherwise send the Date as a normal string else { PrintWriter out = res.getWriter(); out.println(getDate().toString()); } } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doGet(req, res); } } As the code shows, sending a serialized Java object is quite simple. This technique can be used to send any primitive types and/or any Java objects that implement the Serializable interface, including a Vector that contains Serializable objects. Multiple objects can also be written to the same ObjectOutputStream, as long as the class receiving the objects reads them in the same order and casts them to the same types. You may notice that the servlet didn't set the content type of the response to indicate it contained a serialized Java object. The reason is that currently there are no standard MIME types to represent serialized objects. This doesn't really matter, though. A content type acts solely as an indication to the client of how to handle or display the response. If an applet already assumes it's receiving a specific serialized Java object, everything works fine. Sometimes, though, it's useful to use a custom MIME type (specific to your application), so that a servlet can indicate to an applet the contents of its response. 10.2.3.2. The appletThe applet code to retrieve the serialized Date object is very similar to the code to retrieve plain text. The getDateUsingHttpObject() method is shown in Example 10-6. Example 10-6. The DaytimeApplet using HTTP to retrieve an objectprivate String getDateUsingHttpObject() { try { // Construct a URL referring to the servlet URL url = new URL(getCodeBase(), "/servlet/DaytimeServlet"); // Create a com.oreilly.servlet.HttpMessage to communicate with that URL HttpMessage msg = new HttpMessage(url); // Construct a Properties list to say format=object Properties props = new Properties(); props.put("format", "object"); // Send a GET message to the servlet, passing "props" as a query string // Get the response as an ObjectInputStream InputStream in = msg.sendGetMessage(props); ObjectInputStream result = new ObjectInputStream(in); // Read the Date object from the stream Object obj = result.readObject(); Date date = (Date)obj; // Return the string representation of the Date return date.toString(); } catch (Exception e) { // If there was a problem, print to System.out // (typically the Java console) and return null e.printStackTrace(); return null; } } There are two differences between this method and the getDateUsingHttpText() method. First, this method creates a Properties list to set the "format" parameter to the value "object". This tells DaytimeServlet to return a serialized object. Second, the new method reads the returned content as an Object, using an ObjectInputStream and its readObject() method. If the class being serialized is not part of the Java Core API (and therefore isn't already available to the applet), it too has to be made available in the proper location under the web server's document root. An applet can always receive an object's serialized contents, but it needs to download its class file to fully reconstruct the object. Now the applet can retrieve the current time using both text-based and object-based HTTP communication. If you try it yourself now (with a web browser or applet viewer that supports JDK 1.1), you should see both the "HTTP text" and "HTTP object" fields filled in. 10.2.3.3. Posting a serialized objectBefore we go on, we should look at one more (hitherto unmentioned) method from the HttpMessage class: sendPostMessage(Serializable) . This method helps an applet upload a serialized object to a servlet using the POST method. This object transfer isn't particularly useful to our daytime server example (and is kind of out of place here), but we mention it because it can come in handy when an applet needs to upload complicated data structures to its server. Example 10-7 contains the code for this method. Example 10-7. Posting a serialized object// Uploads a serialized object with a POST request. // Sets the content type to java-internal/classname. public InputStream sendPostMessage(Serializable obj) throws IOException { URLConnection con = servlet.openConnection(); // Prepare for both input and output con.setDoInput(true); con.setDoOutput(true); // Turn off caching con.setUseCaches(false); // Set the content type to be java-internal/classname con.setRequestProperty("Content-Type", "java-internal/" + obj.getClass().getName()); // Write the serialized object as post data ObjectOutputStream out = new ObjectOutputStream(con.getOutputStream()); out.writeObject(obj); out.flush(); out.close(); return con.getInputStream(); } An applet uses sendPostMessage(Serializable) just as it uses sendPostMessage(Properties). Here is the code for an applet that uploads any exceptions it encounters to a servlet: catch (Exception e) { URL url = new URL(getCodeBase(), "/servlet/ExceptionLogger"); HttpMessage msg = new HttpMessage(url); InputStream in = msg.sendPostMessage(e); } The servlet, meanwhile, receives the Exception in its doPost() method like this: ObjectInputStream objin = new ObjectInputStream(req.getInputStream()); Object obj = objin.readObject(); Exception e = (Exception) obj; The servlet can receive the type of the uploaded object as the subtype (second half) of the content type. Note that this sendPostMessage(Serializable) method uploads just one object at a time and uploads only serializable objects (that is, no primitive types). 10.2.4. Socket CommunicationNow let's take a look at how an applet and servlet can communicate using non-HTTP socket communication. 10.2.4.1. The servletThe servlet's role in this communication technique is that of a passive listener. Due to security restrictions, only the applet can initiate a socket connection. A servlet must be content to listen on a socket port and wait for an applet to connect. Generally speaking, a servlet should begin listening for applet connections in its init() method and stop listening in its destroy() method. In between, for every connection it receives, it should spawn a handler thread to communicate with the client. With HTTP socket connections, these nitty-gritty details are managed by the web server. The server listens for incoming HTTP requests and dispatches them as appropriate, calling a servlet's service(), doGet(), or doPost() methods as necessary. But when a servlet opts not to use HTTP communication, the web server can't provide any help. The servlet acts, in essence, like its own server and thus has to manage the socket connections itself. Okay, maybe we scared you a bit more than we had to there. The truth is that we can write a servlet superclass that abstracts away the details involved in managing socket connections. This class, which we call DaemonHttpServlet, can be extended by any servlet wanting to make itself available via non-HTTP socket communication.[4]
DaemonHttpServlet starts listening for client requests in its init() method and stops listening in its destroy() method. In between, for every connection it receives, it calls the abstract handleClient(Socket) method. This method should be implemented by any servlet that subclasses DaemonHttpServlet. Example 10-8 shows how DaytimeServlet extends DaemonHttpServlet and implements handleClient() to make itself available via non-HTTP socket communication. Example 10-8. The DaytimeServlet acting as a non-HTTP serverimport java.io.*; import java.net.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.DaemonHttpServlet; public class DaytimeServlet extends DaemonHttpServlet { public Date getDate() { return new Date(); } public void init(ServletConfig config) throws ServletException { // As before, if you override init() you have to call super.init() super.init(config); } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // If the client says "format=object" then // send the Date as a serialized object if ("object".equals(req.getParameter("format"))) { ObjectOutputStream out = new ObjectOutputStream(res.getOutputStream()); out.writeObject(getDate()); } // Otherwise send the Date as a normal ASCII string else { PrintWriter out = res.getWriter(); out.println(getDate().toString()); } } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doGet(req, res); } public void destroy() { // Now, unlike before, if you override destroy() you also have to call // super.destroy() super.destroy(); } // Handle a client's socket connection by spawning a DaytimeConnection // thread. public void handleClient(Socket client) { new DaytimeConnection(this, client).start(); } } class DaytimeConnection extends Thread { DaytimeServlet servlet; Socket client; DaytimeConnection(DaytimeServlet servlet, Socket client) { this.servlet = servlet; this.client = client; setPriority(NORM_PRIORITY - 1); } public void run() { try { // Read the first line sent by the client DataInputStream in = new DataInputStream( new BufferedInputStream( client.getInputStream())); String line = in.readLine(); // If it was "object" then return the Date as a serialized object if ("object".equals(line)) { ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); out.writeObject(servlet.getDate()); out.close(); } // Otherwise, send the Date as a normal string else { // Wrap a PrintStream around the Socket's OutputStream PrintStream out = new PrintStream(client.getOutputStream()); out.println(servlet.getDate().toString()); out.close(); } // Be sure to close the connection client.close(); } catch (IOException e) { servlet.getServletContext() .log(e, "IOException while handling client request"); } catch (Exception e) { servlet.getServletContext() .log("Exception while handling client request"); } } } The DaytimeServlet class remains largely unchanged from its previous form. The major difference is that it extends DaemonHttpServlet and implements a handleClient(Socket) method that spawns a new DaytimeConnection thread. This DaytimeConnection instance bears the responsibility for handling a specific socket connection. DaytimeConnection works as follows. When it is created, it saves a reference to the DaytimeServlet, so that it can call the servlet's getDate() method, and a reference to the Socket, so that it can communicate with the client. DaytimeConnection also sets its running priority to one less than normal, to indicate that this communication can wait if necessary while other threads perform more time-critical work. Immediately after it creates the DaytimeConnection thread, DaytimeServlet starts the thread, causing its run() method to be called. In this method, the DaytimeConnection communicates with the client using some unnamed (but definitely not HTTP) protocol. It begins by reading the first line sent by the client. If the line is "object", it returns the current time as a serialized Date object. If the line is anything else, it returns the current time as a normal string. When it is done, it closes the connection. 10.2.4.2. The superclassThe low-level socket management is done in the DaemonHttpServlet class. Generally, this class can be used without modification, but it is useful to understand the internals. The code is shown in Example 10-9. Example 10-9. The DaemonHttpServlet superclasspackage com.oreilly.servlet; import java.io.*; import java.net.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public abstract class DaemonHttpServlet extends HttpServlet { protected int DEFAULT_PORT = 1313; // not static or final private Thread daemonThread; public void init(ServletConfig config) throws ServletException { super.init(config); // Start a daemon thread try { daemonThread = new Daemon(this); daemonThread.start(); } catch (Exception e) { getServletContext().log(e, "Problem starting socket server daemon thread"); } } // Returns the socket port on which this servlet will listen. // A servlet can specify the port in three ways: by using the socketPort // init parameter, by setting the DEFAULT_PORT variable before calling // super.init(), or by overriding this method's implementation protected int getSocketPort() { try { return Integer.parseInt(getInitParameter("socketPort")); } catch (NumberFormatException e) { return DEFAULT_PORT; } } abstract public void handleClient(Socket client); public void destroy() { // Stop the daemon thread try { daemonThread.stop(); daemonThread = null; } catch (Exception e) { getServletContext().log(e, "Problem stopping server socket daemon thread"); } } } // This work is broken into a helper class so that subclasses of // DaemonHttpServlet can define their own run() method without problems. class Daemon extends Thread { private ServerSocket serverSocket; private DaemonHttpServlet servlet; public Daemon(DaemonHttpServlet servlet) { this.servlet = servlet; } public void run() { try { // Create a server socket to accept connections serverSocket = new ServerSocket(servlet.getSocketPort()); } catch (Exception e) { servlet.getServletContext().log(e, "Problem establishing server socket"); return; } try { while (true) { // As each connection comes in, call the servlet's handleClient(). // Note this method is blocking. It's the servlet's responsibility // to spawn a handler thread for long-running connections. try { servlet.handleClient(serverSocket.accept()); } catch (IOException ioe) { servlet.getServletContext() .log(ioe, "Problem accepting client's socket connection"); } } } catch (ThreadDeath e) { // When the thread is killed, close the server socket try { serverSocket.close(); } catch (IOException ioe) { servlet.getServletContext().log(ioe, "Problem closing server socket"); } } } } The init() method of DaemonHttpServlet creates and starts a new Daemon thread that is in charge of listening for incoming connections. The destroy() method stops the thread. This makes it imperative that any servlet subclassing DaemonHttpServlet call super.init(config) and super.destroy() if the servlet implements its own init() and destroy() methods. The Daemon thread begins by establishing a ServerSocket to listen on some specific socket port. Which socket port is determined with a call to the servlet's getSocketPort() method. The value returned is either the value of the init parameter "socketPort", or, if that init parameter doesn't exist, the current value of the variable DEFAULT_PORT. A servlet may choose to override the getSocketPort() implementation if it so desires. After establishing the ServerSocket, the Daemon thread waits for incoming requests with a call to serverSocket.accept() . This method is blocking--it stops this thread's execution until a client attaches to the server socket. When this happens, the accept() method returns a Socket object that the Daemon thread passes immediately to the servlet's handleClient() method. This handleClient() method usually spawns a handler thread and returns immediately, leaving the Daemon thread ready to accept another connection. The server socket clean-up is equally as important as its set-up. We have to be sure the server socket lives as long as the servlet, but no longer. To this end, the destroy() method of DaemonHttpServlet calls the Daemon thread's stop() method. This call doesn't immediately stop the Daemon thread, however. It just causes a ThreadDeath exception to be thrown in the Daemon thread at its current point of execution. The Daemon thread catches this exception and closes the server socket. There are two caveats in writing a servlet that acts like a non-HTTP server. First, only one servlet at a time can listen to any particular socket port. This makes it vital that each daemon servlet choose its own socket port--by setting its socketPort init parameter, setting the DEFAULT_PORT variable before calling super.init(config), or overriding getSocketPort() directly. Second, a daemon servlet must be loaded into its server and have its init() method called before it can accept incoming non-HTTP connections. Thus, you should either tell your server to load it at start-up or be sure it is always accessed via HTTP before it is accessed directly. 10.2.4.3. The appletThe applet code to connect to the servlet using non-HTTP communication, primarily the getDateUsingSocketText() and getDateUsingSocketObject() methods, is shown in Example 10-10. Example 10-10. The DaytimeApplet getting the time using a socket connectionimport java.net.Socket; // New addition static final int DEFAULT_PORT = 1313; // New addition private int getSocketPort() { try { return Integer.parseInt(getParameter("socketPort")); } catch (NumberFormatException e) { return DEFAULT_PORT; } } private String getDateUsingSocketText() { InputStream in = null; try { // Establish a socket connection with the servlet Socket socket = new Socket(getCodeBase().getHost(), getSocketPort()); // Print an empty line, indicating we want the time as plain text PrintStream out = new PrintStream(socket.getOutputStream()); out.println(); out.flush(); // Read the first line of the response // It should contain the current time in = socket.getInputStream(); DataInputStream result = new DataInputStream(new BufferedInputStream(in)); String date = result.readLine(); // Return the retrieved string return date; } catch (Exception e) { // If there was a problem, print to System.out // (typically the Java console) and return null e.printStackTrace(); return null; } finally { // Always close the connection // This code executes no matter how the try block completes if (in != null) { try { in.close(); } catch (IOException ignored) { } } } } private String getDateUsingSocketObject() { InputStream in = null; try { // Establish a socket connection with the servlet Socket socket = new Socket(getCodeBase().getHost(), getSocketPort()); // Print a line saying "object", indicating we want the time as // a serialized Date object PrintStream out = new PrintStream(socket.getOutputStream()); out.println("object"); out.flush(); // Create an ObjectInputStream to read the response in = socket.getInputStream(); ObjectInputStream result = new ObjectInputStream(new BufferedInputStream(in)); // Read an object, and cast it to be a Date Object obj = result.readObject(); Date date = (Date)obj; // Return a string representation of the retrieved Date return date.toString(); } catch (Exception e) { // If there was a problem, print to System.out // (typically the Java console) and return null e.printStackTrace(); return null; } finally { // Always close the connection // This code executes no matter how the try block completes if (in != null) { try { in.close(); } catch (IOException ignored) { } } } } For both these methods, the applet begins by creating a Socket that is used to communicate with the servlet. To do this, it needs to know both the host name and the port number on which the servlet is listening. Determining the host is easy--it has to be the same host from which it was downloaded, accessible with a call to getCodeBase().getHost(). The port is harder, as it depends entirely on the servlet to which this applet is connecting. This applet uses the getSocketPort() method to make this determination. The implementation of getSocketPort() shown here returns the value of the applet's socketPort parameter, or (if that parameter isn't given) returns the value of the DEFAULT_PORT variable. Once it has established a socket connection, the applet follows an unnamed protocol to communicate with the servlet. This protocol requires that the applet send one line to indicate whether it wants the current time as text or as an object. If the line says "object", it receives an object. If it says anything else, it receives plain text. After sending this line, the applet can read the response as appropriate. The applet and servlet could continue to communicate using this socket. That's one of the major advantages of not using HTTP communication. But, in this case, the applet got what it wanted and just needs to close the connection. It performs this close in a finally block. Putting the close here guarantees that the connection is closed whether the try throws an exception or not. With the addition of these two methods our applet is nearly complete. If you run it now, you should see that all of the fields except "RMI object" contain dates. 10.2.5. RMI CommunicationEarlier in this chapter, we pointed out that one of the reasons not to use RMI communication is that it's complicated. Although that's true, it's also true that with the help of another servlet superclass, the code required for a servlet to make itself available via RMI communication can be ridiculously simple. First, we'll lead you through the step-by-step instructions on how to make a servlet a remote object. Then, after you've seen how simple and easy that is, we'll explain all the work going on behind the scenes. 10.2.5.1. The servletTo begin with, all RMI remote objects must implement a specific interface. This interface does two things: it declares which methods of the remote object are to be made available to remote clients, and it extends the Remote interface to indicate it's an interface for a remote object. For our DaytimeServlet, we can write the DaytimeServer interface shown in Example 10-11. Example 10-11. The DaytimeServer interfaceimport java.util.Date; import java.rmi.Remote; import java.rmi.RemoteException; public interface DaytimeServer extends Remote { public Date getDate() throws RemoteException; } This interface declares that our DaytimeServlet makes its getDate() method available to remote clients. Notice that the getDate() signature has been altered slightly--it now throws a RemoteException. Every method made available via RMI must declare that it throws this exception. Although the method itself may not throw the exception, it can be thrown by the system to indicate a network service failure. The code for DaytimeServlet remains mostly unchanged from its original version. In fact, the only changes are that it now implements DaytimeServer and extends com.oreilly.servlet.RemoteHttpServlet, the superclass that allows this servlet to remain so unchanged. The servlet also implements a destroy() method that calls super.destroy(). It's true that this method is perfectly useless in this example, but it points out that any destroy() method implemented in a remote servlet must call super.destroy() to give the RemoteHttpServlet object's destroy() method a chance to terminate RMI communication. Example 10-12 shows the new DaytimeServlet code. Example 10-12. The DaytimeServlet now supporting RMI accessimport java.io.*; import java.net.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.RemoteHttpServlet; // New addition public class DaytimeServlet extends RemoteHttpServlet // New addition implements DaytimeServer { // New addition // The single method from DaytimeServer // Note: the throws clause isn't necessary here public Date getDate() { return new Date(); } public void init(ServletConfig config) throws ServletException { super.init(config); // Additional code could go here } public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { // If the client says "format=object" then // send the Date as a serialized object if ("object".equals(req.getParameter("format"))) { ObjectOutputStream out = new ObjectOutputStream(res.getOutputStream()); out.writeObject(getDate()); } // Otherwise send the Date as a normal ASCII string else { PrintWriter out = res.getWriter(); out.println(getDate().toString()); } } public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { doGet(req, res); } public void destroy() { // If you override destroy() you have to call super.destroy() super.destroy(); } } So that's how to write a remote object servlet. We suggest you place such servlets directly in the server's classpath (server_root/classes) so they aren't reloaded, since reloading a remote object tends to cause unexpected results. Compiling a remote object servlet is the same as for every other servlet, with one additional step. After compiling the servlet source code, you now have to compile the servlet class with the RMI compiler rmic. The RMI compiler takes a remote object's class file and generates stuband skeleton versions of the class. These classes work behind the scenes to enable RMI communication. You don't need to worry about the details, but you should know that the stub helps the client invoke methods on the remote object and the skeleton helps the server handle those invocations. Using rmic is similar to using javac. For this example you can compile DaytimeServlet with the following command: % rmic DaytimeServlet Notice that you provide rmic with a Java class name to compile, not a file. Thus, if the servlet to compile is part of a package it should be given to rmic as package.name.ServletName. The rmic program can take a classpath to search with the -classpath parameter, as well as a destination directory for the stub and skeleton files with the -d parameter. After executing the above rmic command, you should see two new class files: DaytimeServlet_Stub.class and DaytimeServlet_Skel.class. We'll tell you what to do with these in just a minute. First, you should know that you don't have to rerun the RMI compiler every time you modify the remote servlet's code. This is because the stub and skeleton classes are built in terms of the servlet's interface, not its implementation of that interface. Accordingly, you need to regenerate them only when you modify the DaytimeServer interface (or your equivalent interface). Now, for the final step in writing a remote servlet: copying a few class files to the server's document root, where they can be downloaded by an applet. There are two class files that need to be downloaded: the stub class DaytimeServlet_Stub.class and the remote interface class DaytimeServer.class. The client (in this case the applet) needs the stub class to perform its half of the RMI communication, and the stub class itself uses the remote interface class. Be aware that the servlet needs to use these classes, too, so copy them to the server's document root and leave them in the server's classpath.[5]Figure 10-2 shows where all the server files go.
Figure 10-2. File locations for RMI communicationThat's it! If you follow these instructions you should be able to get a remote servlet operating in short order. Now let's look at the RemoteHttpServlet class and see what's going on behind the scenes. 10.2.5.2. The superclassA remote object needs to do two things to prepare itself for RMI communication: it needs to export itself and register itself. When a remote object exports itself, it begins listening on a port for incoming method invocation requests. When a remote object registers itself, it tells a registry server its name and port number, so that clients can locate it (essentially, find out its port number) and communicate with it. These two tasks are handled by the RemoteHttpServlet class, shown in Example 10-13. Example 10-13. The RemoteHttpServlet superclasspackage com.oreilly.servlet; import java.io.*; import java.net.*; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public abstract class RemoteHttpServlet extends HttpServlet implements Remote { protected Registry registry; public void init(ServletConfig config) throws ServletException { super.init(config); try { // Export ourself UnicastRemoteObject.exportObject(this); // Register ourself bind(); } catch (RemoteException e) { getServletContext().log(e, "Problem binding to RMI registry"); } } public void destroy() { // Unregister ourself unbind(); } // Returns the name under which we are to be registered protected String getRegistryName() { // First name choice is the "registryName" init parameter String name = getInitParameter("registryName"); if (name != null) return name; // Fallback choice is the name of this class return this.getClass().getName(); } // Returns the port on which the registry server is listening // (or should be listening) protected int getRegistryPort() { // First port choice is the "registryPort" init parameter try { return Integer.parseInt(getInitParameter("registryPort")); } // Fallback choice is the default registry port (1099) catch (NumberFormatException e) { return Registry.REGISTRY_PORT; } } protected void bind() { // Try to find the appropriate registry already running try { registry = LocateRegistry.getRegistry(getRegistryPort()); registry.list(); // Verify it's alive and well } catch (Exception e) { // Couldn't get a valid registry registry = null; } // If we couldn't find it, we need to create it. // (Equivalent to running "rmiregistry") if (registry == null) { try { registry = LocateRegistry.createRegistry(getRegistryPort()); } catch (Exception e) { log("Could not get or create RMI registry on port " + getRegistryPort() + ": " + e.getMessage()); return; } } // If we get here, we must have a valid registry. // Now register this servlet instance with that registry. // "Rebind" to replace any other objects using our name. try { registry.rebind(getRegistryName(), this); } catch (Exception e) { log("Could not bind to RMI registry: " + e.getMessage()); return; } } protected void unbind() { try { if (registry != null) registry.unbind(getRegistryName()); } catch (Exception e) { getServletContext().log(e, "Problem unbinding from RMI registry"); } } } If you've ever used or read about RMI before, you've probably seen remote objects that extend the java.rmi.server.UnicastRemoteObject class. This is the standard--and, in fact, recommended--way to write a remote object. The RemoteHttpServlet class, however, doesn't extend UnicastRemoteObject; it extends HttpServlet. As you may know, Java doesn't support multiple inheritance. This means that RemoteHttpServlet has to choose to extend eitherUnicastRemoteObject or HttpServleteven though it needs functionality from both classes. It's a difficult choice. Whichever class RemoteHttpServlet doesn't extend it has to basically reimplement on its own. In the end, we've extended HttpServlet because it's easier to rewrite the functionality of UnicastRemoteObject than that of HttpServlet. This rewrite requires RemoteHttpServlet to do two things it wouldn't have to do if it extended UnicastRemoteObject. First, it must declare that it implements the Remote interface. All remote objects must implement this interface, but normally, by extending UnicastRemoteObject, a class gets this for free. Still, the price for going it alone isn't too bad, as the Remote interface doesn't actually define any methods. An object declares that it implements Remote solely to express its desire to be treated as a remote object. The second thing RemoteHttpServlet has to do is manually export itself. Normally, this is performed automatically in the UnicastRemoteObject() constructor. But again, doing this without that constructor is not a problem. The UnicastRemoteObject class has a static exportObject(Remote) method that any Remote object can use to export itself. RemoteHttpServlet uses this method and exports itself with this single line: UnicastRemoteObject.exportObject(this); Those two steps, implementing Remote and exporting itself, are done by RemoteHttpServlet in lieu of extending UnicastRemoteObject.[6]
The rest of the RemoteHttpServlet code involves registering and unregistering itself with an RMI registry. As we said before, an RMI registry server acts as a location where clients can locate server objects. A remote object (server object) registers itself with the registry under a certain name. Clients can then go to the registry to look up the object by that name. To make itself available to clients then, our servlet has to find (or create) a registry server and register itself with that server under a specific name. In registry parlance, this is called binding to the registry. RemoteHttpServlet performs this binding with its bind() method, called from within its init() method. The bind() method uses two support methods, getRegistryPort() and getRegistryName(), to determine the port on which the servlet should be running and the name under which the servlet should be registered. With the current implementations, the port is fetched from the registryPort init parameter, or it defaults to 1099. The name is taken from the registryName init parameter or defaults to the servlet's class name--in this case, DaytimeServlet. Let's step through the bind() method. It begins by using the following code to try to find an appropriate registry that is already running: registry = LocateRegistry.getRegistry(getRegistryPort()); registry.list(); The first line attempts to get the registry running on the given port. The second asks the registry to list its currently registered objects. If both calls succeed, we have a valid registry. If either call throws an Exception, the bind() method determines there is no valid registry and creates one itself. It does this with the following line of code: registry = LocateRegistry.createRegistry(getRegistryPort()); After this, the bind() method should have either found or created a registry server. If it failed in getting the registry and failed again in creating it, it returns and the servlet remains unregistered. RemoteHttpServlet next binds itself to the registry using this line of code: registry.rebind(getRegistryName(), this); It uses the Registry.rebind() method instead of the Registry.bind() method to indicate that this binding should replace any previous binding using our name. This binding persists until the servlet is destroyed, at which time the destroy() method of RemoteHttpServlet calls its unbind() method. The code unbind() uses to unbind from the registry is remarkably simple: if (registry != null) registry.unbind(getRegistryName()); It simply asks the registry to unbind its name. Please note that a remote servlet must be loaded into its server and have its init() method called before it is ready for RMI communication. Thus, just as with a daemon servlet, you should either tell your server to load it at start-up or be sure it is always accessed via HTTP before it is accessed directly. 10.2.5.3. The appletNow let's turn our attention from the server and focus it on the client. The code our DaytimeApplet uses to invoke the getDate() method of our new DaytimeServlet is shown in Example 10-14. Example 10-14. The DaytimeApplet getting the time using RMIimport java.rmi.*; // New addition import java.rmi.registry.*; // New addition private String getRegistryHost() { return getCodeBase().getHost(); } private int getRegistryPort() { try { return Integer.parseInt(getParameter("registryPort")); } catch (NumberFormatException e) { return Registry.REGISTRY_PORT; } } private String getRegistryName() { String name = getParameter("registryName"); if (name == null) { name = "DaytimeServlet"; // default } return name; } private String getDateUsingRMIObject() { try { Registry registry = LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort()); DaytimeServer daytime = (DaytimeServer)registry.lookup(getRegistryName()); return daytime.getDate().toString(); } catch (ClassCastException e) { System.out.println("Retrieved object was not a DaytimeServer: " + e.getMessage()); } catch (NotBoundException e) { System.out.println(getRegistryName() + " not bound: " + e.getMessage()); } catch (RemoteException e) { System.out.println("Hit remote exception: " + e.getMessage()); } catch (Exception e) { System.out.println("Problem getting DaytimeServer reference: " + e.getClass().getName() + ": " + e.getMessage()); } return null; } The first three methods are support methods. getRegistryHost() returns the host on which the registry server should be running. This must always be the host from which the applet was downloaded. getRegistryPort() returns the port on which the registry server should be listening. It's normally the default registry port 1099, though it can be overridden with the registryPort parameter. getRegistryName() returns the name under which the servlet should have been registered. It defaults to "DaytimeServlet", but it can be overridden with the registryName parameter. The actual lookup of the remote servlet object and invocation of its getDate() method occur in these three lines of the getDateUsingRMIObject() method: Registry registry = LocateRegistry.getRegistry(getRegistryHost(), getRegistryPort()); DaytimeServer daytime = (DaytimeServer)registry.lookup(getRegistryName()); return daytime.getDate().toString(); The first line locates the registry for the given host and the given port. The second line uses this registry to look up the remote object registered under the given name, in the process casting the object to a DaytimeServer object. The third line invokes this object's getDate() method and receives a serialized Date object in return. Then, in the same line, it returns the String representation of that Date. The rest of the getDateUsingRMIObject() method handles the exceptions that could occur during these three lines. It catches a ClassCastException if the retrieved object is not a DaytimeServer, a NotBoundException if the registry has no object registered under the given name, and a RemoteException if there is a network service failure. It also catches a general Exception, in case there's some other problem. You may be wondering why DaytimeApplet uses Registry.lookup(String) instead of java.rmi.Naming.lookup(String) to retrieve its reference to the remote servlet. There's really no reason--it's simply a matter of personal taste. It would work just as well to replace the first two lines in getDateUsingRMIOb-ject() with the following code: DaytimeServer daytime = (DaytimeServer)Naming.lookup("rmi://" + getRegistryHost() + ":" + getRegistryPort() + "/" + getRegistryName()); That's it for the fifth and final method of DaytimeApplet. Go ahead and run the applet now. Do you see every date field nicely filled in? You shouldn't. You should instead see empty values for the socket communication options. If you remember, we removed support for socket communication when we made DaytimeServlet a remote object. Now let's put socket communication back in. 10.2.5.4. A full-service servletWhat we need now is a single servlet that can make itself available via HTTP communication, non-HTTP socket communication, and RMI communication. A servlet of this sort can extend a new superclass, com.oreilly.servlet.RemoteDaemonHttpServlet, implementing the capabilities discussed so far for both an RemoteHttpServlet and a DaemonHttpServlet. Here's the code that declares this full-service servlet: import java.io.*; import java.net.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; import com.oreilly.servlet.RemoteDaemonHttpServlet; public class DaytimeServlet extends RemoteDaemonHttpServlet implements DaytimeServer { public Date getDate() { return new Date(); } // The rest is unchanged This code is almost the same as Example 10-8. It's basically that example rewritten to declare that it extends RemoteDaemonHttpServlet and that it implements DaytimeServer. The code for the RemoteDaemonHttpServlet superclass also nearly matches the code for RemoteHttpServlet. There are just two changes: it extends DaemonHttpServlet instead of HttpServlet, and its destroy() method first calls super.destroy(): package com.oreilly.servlet; import java.io.*; import java.net.*; import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; public abstract class RemoteDaemonHttpServlet extends DaemonHttpServlet implements Remote { public void destroy() { super.destroy(); unbind(); } // The rest is unchanged Now our DaytimeApplet can connect to this revised remote daemon servlet and produce the full and complete output shown earlier in Figure 10-1. Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|