The Standalone Tic-Tac-Toe Game


Clients Using a Servlet as a Server

Figure 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 system


hi 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:


ChatServlet?cmd=hi&name=??

This is a hi message, used by a client to ask for chat group membership. The name parameter value (represented by ??) holds the client's name. The servlet returns a cookie containing a user ID (uid) or rejects the client.


ChatServlet?cmd=bye&name=?? and uid cookie

This is a bye message, which signals the client's departure. The cookie is included in the header part of the GET message, not as a URL argument.


ChatServlet?cmd=who

The who message requires no client name or cookie parameters. The servlet returns the names of the clients currently using the application.


ChatServlet?cmd=msg&name=??&msg=?? and uid cookie

This message sends chat text to the servlet as the msg parameter value. The string is added to the servlet's messages list if the client name and uid are correct.

The chat message format is the same as in the multicasting example: if the text has a / toName extension, then it will be intended only for the client with that name.

URLChatWatcher periodically sends a read message:


ChatServlet?cmd=read&name=?? and uid cookie

This retrieves all the visible chat messages stored by the servlet since the last read. Visible messages are intended for everyone (the default) or have a / toName extension which matches this client's name.

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 system


The URL-Based Chat Client

Figure 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 servlet

URLChat 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 Messages

URLChatWatcher 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 Servlet

The 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 client

processHi( ) 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 messages

processMsg( ) 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 Information

ChatGroup 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 group

addUser( ) 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 messages

One 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 Class

Each 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); 



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net