Clients Using a Servlet as a ServerFigure 30-7 shows the various objects in the servlet-based chat application. Each client is represented by two objects: a URLChat object manages the GUI and translates user input into messages for the web server. The URLChatWatcher thread periodically queries the server for new messages sent by the other users. The communication is implemented by sending the server a GET request for the ChatServlet object, with message details added as arguments to the URL. A client is identified by a name and a cookie. The cookie is presented to users when they send a Figure 30-7. The servlet-based chat systemhi message to the servlet upon joining the chat system. Most messages require the name and cookie information, which verify the identity of the message sender. The servlet maintains a synchronized ChatGroup object, which bears many similarities to the ChatGroup object used in the threaded chat application. Client information is held in Chatter objects, while client messages are stored in a messages ArrayList. A key difference between ChatServlet and the server in the first chat example is that ChatServlet cannot initiate communication with its clients. It must wait for a URLChatWatcher tHRead to ask for messages before it can send them out. URLChat TRansmits its messages as arguments attached to the URL http://localhost:8100/servlet/ChatServlet. The notation for an argument in a GET request is arg-name=arg-value, with multiple argument name/value pairs separated by &. The argument pairs follow a ? after the URL. The four possible message types are:
The cookie acts as an additional form of identification, paired with the client's name. However, cookies are not passwords, being passed in plain text form in the headers of the messages. To act as a password, it would be necessary to add encryption to the interaction, possibly by using HTTPS rather than HTTP. Figure 30-8 gives the class diagrams for the application, showing the public methods. Figure 30-8. Class diagrams for the servlet-based chat systemThe URL-Based Chat ClientFigure 30-9 shows the GUI for URLChat, which is identical to the earlier examples. A hi message is generated when the client is first invoked, a bye message is sent when the user clicks on the window's close box, a who message is transmitted when the Who button is pressed, and the entering of a string in the text field results in a msg message. The constructor carries out several tasksit creates the GUI, sets timeout properties, sends a hi message, links the bye output to the close box, and invokes a URLChatWatcher: // globals private String userName; // for this client private String cookieStr = null; public URLChat(String nm) { super( "URL Chat Client for "+ nm); userName = nm; initializeGUI( ); Figure 30-9. The URLChat GUI // set the properties used for URL timeouts (in ms) Properties props = System.getProperties( ); props.put("sun.net.client.defaultConnectTimeout", "2000"); props.put("sun.net.client.defaultReadTimeout", "2000"); System.setProperties(props); sayHi( ); addWindowListener( new WindowAdapter( ) { public void windowClosing(WindowEvent e) { sayBye( ); } }); setSize(300,450); show( ); new URLChatWatcher(this, userName, cookieStr).start( ); } // end of URLChat( ); A client utilizes URL GET requests to communicate with the servlet. If the server is down or the servlet is unavailable, then the client will have to wait for the URL request to timeout. This often amounts to several minutes delay before an exception is raised. Fortunately, J2SE 1.4 introduced two properties for adjusting the default connection timeout and the reading timeout (how long to wait when the reading of a web page is delayed). These are associated with every URLConnection object created in the application. The main problem with timeouts, as always, is deciding on a reasonable value that takes network latency into account. Another approach is to wrap each connection in a thread using Thread.join( ) as a timeout mechanism. This has the advantage that different timeout values can be assigned to different URLs. sayHi( ) illustrates how a GET request is constructed using a URL object. The response from the servlet may include a Set-Cookie header: this is the usual way that a cookie is sent to a browser. Set-Cookie isn't a part of the HTTP standard but is used by all web servers and browsers. The need to read the response page's headers requires a URLConnection object. The returned page is plain text consisting of a single line containing "ok" or "no": private void sayHi( ) { try { URL url = new URL(SERVER + "?cmd=hi&name=" + URLEncoder.encode(userName, "UTF-8") ); URLConnection conn = url.openConnection( ); cookieStr = conn.getHeaderField("Set-Cookie"); // get cookie System.out.println("Received cookie: " + cookieStr); if (cookieStr != null) { int index = cookieStr.indexOf(";"); if (index != -1) cookieStr = cookieStr.substring(0, index); //remove extras } BufferedReader br = new BufferedReader( new InputStreamReader( conn.getInputStream( ) )); String response = br.readLine( ).trim( ); br.close( ); if (response.equals("ok") && (cookieStr != null)) showMsg("Server Login Successful\n"); else { System.out.println("Server Rejected Login"); System.exit(0); } } catch(Exception e) { System.out.println(e); System.exit(0); } } // end of sayHi( ) The client name must be URL-encoded prior to transmission. Essentially, this replaces any spaces in the string by +s and alters certain other characters to a hexadecimal format. The value of a Set-Cookie header is a string containing the cookie value separated from various other things by a ; (these may include a comment, path and domain qualifiers, a maximum age, and a version number). This additional information isn't required, so sayHi( ) strips it away before storing the string in the cookieStr global. In fact, the servlet creates a cookie holding only a value, so the editing code is not really necessary in this example. The client will terminate if the response to the hi message is "no," or the cookie string is not initialized. All further communication with the servlet (aside from who messages) requires the cookie value. If a servlet (or its web server) is unavailable, then the URL request will timeout after two seconds (due to the timeout properties set in the constructor), and raise an exception. This allows a client to respond to the server's absence by exiting. sayHi( ) uses showMsg( ) to write into the GUI's text area, which is implemented in the same way as in the threaded client that I discussed first. The updates to the text area are performed by Swing's event dispatching thread. Talking to the servletURLChat employs sendURLMessage( ) to send a chat message to the servlet. It's quite similar to sayHi( ) except that it adds the cookie value to the GET request by including a Cookie header in the output. The response page returned by the servlet is one line containing "ok" or "no": private void sendURLMessage(String msg) { try { URL url = new URL(SERVER + "?cmd=msg&name=" + URLEncoder.encode(userName, "UTF-8") + "&msg=" + URLEncoder.encode(msg, "UTF-8") ); URLConnection conn = url.openConnection( ); conn.setRequestProperty("Cookie", cookieStr); // add cookie BufferedReader br = new BufferedReader( new InputStreamReader( conn.getInputStream( ) )); String response = br.readLine( ).trim( ); br.close( ); if (!response.equals("ok")) showMsg("Message Send Rejected\n"); else // display message immediately showMsg("(" + userName + ") " + msg + "\n"); } catch(Exception e) { showMsg("Servlet Error. Did not send: " + msg + "\n"); System.out.println(e); } } // end of sendURLMessage( ) A small but significant part of sendURLMessage( ) is the call to showMsg( ) to display the output message if the servlet's response was "ok." This means the message appears in the client's text area almost as soon as it is typed. The alternative would be to wait until it was retrieved, together with the other recent messages, by URLChatWatcher. The drawback of this approach is the delay before the user sees a visual conformation that their message has been sent. This delay depends on the frequency of URLChatWatcher's requests to the servlet (currently every two seconds). A message echoing delay like this gives an impression of execution slowness and communication latency, which is best avoided. The drawback of the current design (immediate echoing of the output message) is that the user's contribution to a conversation may not appear on screen in the order that the conversation is recorded in the servlet. Polling for Chat MessagesURLChatWatcher starts an infinite loop that sleeps for a while (two seconds) and then sends a read request for new chat messages. The answer is processed and displayed in the client's text area, and the watcher repeats: public void run( ) { URL url; URLConnection conn; BufferedReader br; String line, response; StringBuffer resp; try { String readRequest = SERVER + "?cmd=read&name=" + URLEncoder.encode(userName, "UTF-8") ; while(true) { Thread.sleep(SLEEP_TIME); // sleep for 2 secs url = new URL(readRequest); // send a "read" message conn l.openConnection( ); // Set the cookie value to send conn.setRequestProperty("Cookie", cookieStr); br = new BufferedReader( new InputStreamReader( conn.getInputStream( ) )); resp = new StringBuffer( ); // build up the response while ((line = br.readLine( )) != null) { if (!fromClient(line)) // if not from client resp.append(line+"\n"); } br.close( ); response = resp.toString( ); if ((response != null) && !response.equals("\n")) client.showMsg(response); // show the response } } catch(Exception e) { client.showMsg("Servlet Error: watching terminated\n"); System.out.println(e); } } // end of run( ) The try-catch block allows the watcher to respond to the server's absence by issuing an error message before terminating. The response page may contain multiple lines of text, one line for each chat message. The watcher filters out chat messages originating from its client since these will have been printed when they were sent out. A drawback of this filtering is that when a client joins the chat system again in the future, messages stored from a prior visit won't be shown in the display text area. This can be remedied by making the fromClient( ) test a little more complicated than the existing code: private boolean fromClient(String line) { if (line.startsWith("("+userName)) return true; return false; } The Chat ServletThe initialization phase of a ChatServlet object creates a new ChatGroup object for holding client details. Its doGet( ) method tests the cmd parameter of GET requests to decide what message is being received: // globals private ChatGroup cg; // for storing client information public void init( ) throws ServletException { cg = new ChatGroup( ); } public void doGet( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException // look at cmd parameter to decide which message the client sent { String command = request.getParameter("cmd"); System.out.println("Command: " + command); if (command.equals("hi")) processHi(request, response); else if (command.equals("bye")) processBye(request, response); else if (command.equals("who")) processWho(response); else if (command.equals("msg")) processMsg(request, response); else if (command.equals("read")) processRead(request, response); else System.out.println("Did not understand command: " + command); } // end of doGet( ) An understanding of this code requires a knowledge of the servlet lifecycle. A web server will typically create a single servlet instance, resulting in init( ) being called once. However, each GET request will usually be processed by spawning a separate thread which executes doGet( ) for itself. This is similar to the execution pattern employed by the threaded server in the first chat example. The consequence is that data which may be manipulated by multiple doGet( ) threads must be synchronized. The simplest solution is to place this data in an object that is only accessible by synchronized methods. Consequently, the single ChatGroup object (created in init( )) holds client details and the list of chat messages. Dealing with a new clientprocessHi( ) handles a message with the format ChatServlet?cmd=hi&name=??. The name parameter must be extracted and tested, and a cookie must be created for the client's new user ID. The cookie is sent back as a header, and the response page is a single line containing "ok," or "no" if something is wrong: private void processHi(HttpServletRequest request, HttpServletResponse response) throws IOException { int uid = -1; // default for failure String userName = request.getParameter("name"); if (userName != null) uid = cg.addUser(userName); // attempt to add to group if (uid != -1) { // the request has been accepted Cookie c = new Cookie("uid", ""+uid); response.addCe(c); } PrintWriter output = response.getWriter( ); if (uid != -1) output.println("ok"); else output.println("no"); // request was rejected output.close( ); } // end of processHi( ) The cookie is created using the Cookie class and is added to the headers of the response with the addCookie( ) method. The actual cookie header has this format: Set-Cookie: uid= <some number> The uid number is generated when ChatGroup creates a Chatter object for the new client. It may have any value between 0 and 1024. If the value is -1, then the membership request will be rejected because the client's name is being used by another person. Processing client messagesprocessMsg( ) is typically of the other processing methods in ChatServlet. It handles a message with the format ChatServlet?cmd=msg&name=??&msg=?? and a uid cookie in the header. It attempts to add the chat message to the messages list maintained by ChatGroup: private void processMsg(HttpServletRequest request, HttpServletResponse response) throws IOException { boolean isStored = false; // default for failure String userName = request.getParameter("name"); String msg = request.getParameter("msg"); System.out.println("msg: " + msg); if ((userName != null) && (msg != null)) { int uid = getUidFromCookie(request); isStored = cg.storeMessage(userName,uid,msg); //add msg to list } PrintWriter output = response.getWriter( ); if (isStored) output.println("ok"); else output.println("no"); // something wrong output.close( ); } // end of processBye( ) Processing is aborted if the name or chat message is missing. The call to getUidFromCookie( ) will return a number (which may be -1, signifying an error). The ChatGroup object is then given the task of storing the chat message, which it only does if the name/uid pair match an existing client. The resulting isStored value is employed to decide on the response page sent back to the client. getUidFromCookie( ) must deal with the possibility of multiple cookies in the request, so has to search for the one with the uid name. A missing uid cookie, or one with a nonnumerical value, causes a -1 to be returned: private int getUidFromCookie(HttpServletRequest request) { Cookie[] cookies = request.getCookies( ); Cookie c; for(int i=0; i < cookies.length; i++) { c = cookies[i]; if (c.getName( ).equals("uid")) { try { return Integer.parseInt( c.getValue( ) ); } catch (Exception ex){ System.out.println(ex); return -1; } } } return -1; } // end of getUidFromCookie( ) The getCookies( ) method returns an array of Cookie objects, which can be examined with getName( ) and getValue( ). Storing Chat Group InformationChatGroup maintains two ArrayLists: chatUsers and messages. chatUsers is an ArrayList of Chatter objects; each Chatter object stores a client's name, uid, and the number of messages read by its CharURLWatcher tHRead. messages is an ArrayList of strings (one for each chat message). All the public methods are synchronized since many doGet( ) tHReads may be competing to access the ChatGroup object simultaneously. The messages ArrayList solves the problem of storing chat messages. The drawback is the list will grow long as the volume of communication increases. However, the messages list is cleared when no users are in the chat group. Another possible optimization, which isn't utilized in this code, is to delete messages which have been read by all the current users. This would keep the list small since each client's URLChatWatcher reads the list every few seconds. The drawback is the extra complexity of adjusting the list and the Chatter objects' references to it. A possible advantage of maintaining a lengthy messages list is that new members get to see earlier conversations when they join the system. The list is sent to a new client when its watcher thread sends its first read message. Another approach might be to archive older messages in a text file. This could be accessed when required without being a constant part of the messages list. It could be employed as a backup mechanism in case of server failure. Adding a new client to the groupaddUser( ) adds a new client to the ChatGroup if his or her name is unique. A uid for the user is returned: // globals private ArrayList chatUsers; private ArrayList messages; private int numUsers; synchronized public int addUser(String name) // adds a user, returns uid if okay, -1 otherwise { if (numUsers == 0) // no one logged in messages.clear( ); if (isUniqueName(name)) { Chatter c = new Chatter(name); chatUsers.add(c); messages.add("(" + name + ") has arrived"); numUsers++; return c.getUID( ); } return -1; } Reading client messagesOne of the more complicated methods in ChatGroup is read( ), which is called when a watcher thread requires new messages: synchronized public String read(String name, int uid) { StringBuffer msgs = new StringBuffer( ); Chatter c = findUser(name, uid); if (c != null) { int msgsIndex = c.getMsgsIndex( ); // where read to last time String msg; for(int i=msgsIndex; i < messages.size( ); i++) { msg = (String) messages.get(i); if (isVisibleMsg(msg, name)) msgs.append( msg + "\n" ); } c.setMsgsIndex( messages.size( )); //update client's read index } return msgs.toString( ); } The method begins by calling findUser( ) to retrieve the Chatter object for the given name and uid, which may return null if there isn't a match. Since the messages list retains all the chat messages, it's necessary for each Chatter object to record the number of messages read. This is retrieved with a call to getMsgsIndex( ) and updated with setMsgsIndex( ). This number corresponds to an index into the messages list, and can be used to initialize the for loop that collects the chat messages. If the messages list was periodically purged of messages that had been read by all the current users, then it would be necessary to adjust the index number stored in each Chatter object. Only visible messages are returnedi.e., those without a / toName extension, or an extension, which names the client. The Chatter ClassEach Chatter object stores the client's name, the uid, and the current number of messages read from the chat messages list. The uid is generated in a trivial manner by generating a random number between 0 and ID_MAX (1024). The choice of ID_MAX has no significance: uid = (int) Math.round( Math.random( )* ID_MAX); |