Networking our whiteboard with servlets

Find out how to easily replace the RMI and sockets networking layers with servlets

You gotta be Java.

Play with the whiteboard!

Note: This applet will run only in the HotJava browser, the Windows 95/NT implementation of Internet Explorer 4.0, or in an AWT 1.1-enabled version of Netscape (see Resources for a link to the patch required to bring Netscape up to speed).

If you are not using any of these browsers, you can use appletviewer to view the applet.

The whiteboard we're using was first developed back inNovember's column. The whiteboard uses 1.1 AWT features, such as lightweight components to display objects as the user draws them.

The whiteboard

In December's column we added to the functionality of the previous month by creating a network of whiteboards that shared a common list of objects. All the whiteboards in the collaborative group could add and move the shared whiteboard objects. We developed both RMI- and sockets-based networking layers to enable communication among the whiteboards.

This month we're going one step further: We're going to use servlets to provide the same function that sockets and RMI provided last month.

Servlets are server extensions written in Java, usually for Web servers. A growing number of Web servers support them, including Apache, Java Web Server (JWS), O'Reilly's WebSite, and Netscape's various offerings.

Servlets are interesting because they don't fork a new process for every request that comes in, which makes them much faster than CGI. Under some Web servers, such as JWS, servlets are even faster than Fast-CGI because there is no process task switch; the servlets run as threads within the server process itself.

Because Web-based servlets respond to HTTP methods such as GET and POST, servlet-based communication is able to get around firewalls, which block sockets and RMI. As an added bonus, it's easy to use SSL to secure communications between SSL-enabled clients (Netscape, for example) and servlets running under an SSL-enabled Web server. SSL (or Secure Sockets Layer) is an encryption-based protocol used to protect data sent over the Internet from eavesdropping.

Servlet structure and life cycle

At a high level, servlets are just like applets, with the exception that they run in the server environment instead of the browser environment. Like applets, they have a definite life cycle, which is controlled by the environment. Unlike applets, however, only one instance of a servlet is actually created for each Web server. Each request to the servlet's URL is passed into the same instance of the servlet.

Servlets are accessed from clients in the same manner as CGI scripts. For example, an HTTP GET request to a URL like http://www.mycom.com/servlet/CurrentTime could return a bit of HTML containing the current time on the server.

The server loads the servlet when the first request is directed to it (unless the server allows pre-loading). The server then calls the servlet's init () method. All other requests arriving before the init () method completes block until it does so.

Once the init () method completes, the servlet is ready to service requests via its service () method. The environment puts each request into its own thread, which then enters the servlet's service () method.

The servlet continues servicing requests until it is unloaded. When the environment unloads the servlet (when the server shuts down, for example), it calls the servlet's destroy () method.

Brief overview of the servlet API

The servlet API is provided in the

javax.servlet

and

javax.servlet.http

packages.

javax.servlet is the base servlet API, and contains the interface Servlet and its generic implementation, GenericServlet. This package also contains ServletRequest and ServletResponse interfaces.

The most important method of GenericServlet is service (), which is where request threads enter. Specializations of GenericServlet such as HttpServlet override this method to handle protocol-specific requests.

javax.servlet.http specializes these classes and implements interfaces to handle HTTP requests. We'll look at the HTTP servlet classes in more depth in this application.

A servlet for the whiteboard

The whiteboard, as you will remember from last time, used a distributed data structure called

ObservableList

to keep track of elements that it displayed. Distributed implementations using sockets and RMI simply subclassed

ObservableList

and overrode method implementations based on which type of networking was used. In both cases, a server provided a central

IDList

to keep track of elements shared among the clients.

This time, we're going to do much the same thing, except we'll extend ObservableList to provide a servlet-based implementation called ServletList. The servlet implementation will share methods employed by both networking models we used last month: the client will poll the server periodically to ask for updates as it did in the RMI approach, and it will pass specialized "message objects" to the server using object streams as in the sockets approach.

The client object streams will use the URL and URLConnection classes to send message objects using the HTTP POST protocol, which allows a Web server extension to send binary data with a POST request. The response to the POST will be binary data as well, which an object stream will decode into a response object.

Servlet-based communications architecture

Class ServletList

ServletList

extends

ObservableList

and uses a few of the message wrappers from the previous socket-based implementation to communicate with the server. One new message type,

UpdateMsg

, is necessary so that the client can alert the server that it needs an update. The server responds with an

InitMsg

containing the update. Let's take a look.

package shoffner.step.jan;

import java.io.*; import java.util.*; import java.net.*;

import org.merlin.step.dec.*; import org.merlin.step.dec.socket.*;

public class ServletList extends ObservableList implements Runnable { public static final int UPDATE_DELAY = 10 * 1000;

URL server; IDList list; Thread processor;

public ServletList (String host, String loc) { try { server = new URL ("http://" + host + "/" + loc); } catch (MalformedURLException ex) { ex.printStackTrace (); } list = new IDList (); update (); processor = new Thread (this); processor.start (); }

We import classes from the dec and dec.socket packages because we are going to use some of the data structures and socket message classes from last month. The constructor sets up a URL to the host and a location supplied by the class that instantiates the ServletList.

public Enumeration elements () { return list.elements (); }

public void addElement (Object element) { Object id = queryServer (new AddElementMsg (element)); list.addElementWithID (id, element); }

public void replaceElementAtEnd (Object oldE, Object newE) { Object oldID = list.getIDOfElement (oldE); Object id = queryServer (new ReplaceElementMsg (oldID, newE)); if (id != null) list.replaceElementWithID (oldID, id, newE); else System.out.println ("Unknown id:" + oldID); }

These three methods (elements (), addElement (), and replaceElementAtEnd ()) are part of the public interface to the ServletList. They override methods found in ObservableList. The central feature of these methods is the call to queryServer (), which returns an Object result from the server to the calling method. Notice that we're wrapping the requests in objects to denote their type to the server.

public void run () { while (true) { try { Thread.sleep (UPDATE_DELAY); update (); } catch (Exception ignored) { ignored.printStackTrace (); } } }

public void stop () { if (processor != null) { processor.stop (); processor = null; } }

void update () { InitMsg i = (InitMsg) queryServer (new UpdateMsg (list.getUpdateCount ())); IDList initList = i.getList (); if (initList != null) { list = initList; fireUpdate (); } }

Here we see the update thread that periodically polls the server. If the server has a different updateCount from that maintained by list, the server sends back its own IDList. We replace our local copy with IDList and call fireUpdate (). Obviously, we could use a more sophisticated update mechanism here, but this approach is easy and adequate for our needs.

The run () method must not be allowed to exit while the client is active, or the client will stop polling for updates. Therefore, run () sits in a while loop and catches exceptions that may be generated if the server goes down while the client is attempting an update.

Now we get to the interesting part of the client implementation:

Object queryServer (Object arg) { URLConnection con; ObjectOutputStream req = null; ObjectInputStream res = null; Object result = null; try { con = server.openConnection (); con.setDoOutput (true); req = new ObjectOutputStream (new BufferedOutputStream (con.getOutputStream ())); req.writeObject (arg); req.flush (); req.close ();

res = new ObjectInputStream (new BufferedInputStream (con.getInputStream ())); result = res.readObject (); } catch (IOException ignored) { ignored.printStackTrace (); } catch (ClassNotFoundException ex) { ex.printStackTrace (); } finally { try { res.close (); } catch (IOException ex) {} } return result; } }

The queryServer () method sends an object, arg, to the servlet using the HTTP POST method to access the servlet's URL. Before the object can be sent, it must be serialized using ObjectOutputStream. The server's response will be read in from an ObjectInputStream and returned by the method.

Interestingly, the queryServer () method does not require a servlet at the other end. Any Web server extension/CGI that can understand POST, and can accept and return a serialized Java object will work fine. Alternatively, we could override the queryServer () method to make it access other types of message-oriented servers besides those dealing in serialized Java objects.

Class ServletListServlet

It's servlet time!

import javax.servlet.*; import javax.servlet.http.*; import java.io.*;

import org.merlin.step.dec.IDList; import org.merlin.step.dec.socket.*; import shoffner.step.jan.*;

public class ServletListServlet extends HttpServlet {

IDList list;

public void init (ServletConfig c) throws ServletException { super.init (c); list = new IDList (); log ("ServletListServlet initialized."); System.out.println ("ServletListServlet initialized."); }

We first import all the necessary classes and declare that our servlet extends HttpServlet so that it can make use of the specialized doPost () method. The next step is to override the default init () method. The log () statement writes our startup message to a Web server log file, and the println () method prints it to the terminal from which the Web server was started (assuming the terminal is available).

We could also override the generic destroy () method to make sure that any long-running connections finish before the servlet is unloaded. This approach is a good idea if the application is sensitive to connection death, as would be the case during a long-running data transfer, for example. Instead, we will leave it to the client to handle recovery if the servlet is unloaded. (The Web server could potentially die unexpectedly anyway, but that's rare.)

public void doPost (HttpServletRequest httpReq, HttpServletResponse httpRes) throws ServletException, IOException {

// handle request ServletInputStream sis = httpReq.getInputStream (); BufferedInputStream bis = new BufferedInputStream (sis); ObjectInputStream ois = new ObjectInputStream (bis); DListMsg msg = null; Object response = null; try { msg = (DListMsg) ois.readObject (); } catch (ClassNotFoundException ex) { ex.printStackTrace (); } if (msg instanceof AddElementMsg) { Object element = ((AddElementMsg) msg).getElement (); list.addElement (element); response = list.getIDOfElement (element); } else if (msg instanceof ReplaceElementMsg) { Object oldID = ((ReplaceElementMsg) msg).getID (); Object element = ((ReplaceElementMsg) msg).getElement (); response = list.replaceElement (oldID, element); } else if (msg instanceof UpdateMsg) { if (list.getUpdateCount () == ((UpdateMsg) msg).getUpdateCount ()) response = new InitMsg (null); else response = new InitMsg ((IDList) list.clone ()); } else { System.out.println ("Unknown message:" + msg); } System.out.println (msg);

doPost () does the real work for the servlet. Its function is to accept an incoming POST and generate a response. The request and response are passed into the method by the environment in the form of HttpServletRequest and HttpServletResponse, respectively, when the environment calls doPost ().

Once the msg object is retrieved from the input stream associated with HttpServletRequest, the message logic takes over and acts based on the type of the message.

1 2 Page 1