Threaded TCP Clients and ServerFigure 30-1 shows the various objects in the threaded chat application. Figure 30-1. Objects in the threaded chat systemEach client is represented by a ChatClient object and a ChatWatcher thread. ChatClient maintains the GUI, processes the user's input, and sends messages to the server over a TCP link. ChatWatcher waits for messages from the server and displays them in ChatClient's GUI text area. ChatClient can send the following messages:
Server messages are received by the ChatWatcher tHRead, which is monitoring incoming messages on the ChatClient's TCP link. ChatServer is the top-level server, which spawns a ChatServerHandler thread to handle each new client. Since messages are to be transmitted between clients, the ChatServer maintains pertinent client information, accessible by all the threads. The shared data is held by a ChatGroup object, which offers synchronized methods to control the concurrency issues. A client's details, such as its input stream, are stored in a Chatter object. Figure 30-2 gives the class diagrams for the application, showing the public methods. Figure 30-2. Class diagrams for the threaded chat systemThe Chat ClientThe GUI supported by ChatClient is shown in Figure 30-3. The text area for displaying messages occupies the majority of the window. Outgoing messages are typed in a text field and sent when the user presses enter. A who message is outputted when the Who button is pressed. A bye message is transmitted as a result of the user clicking on the window's Close box. Figure 30-3. The ChatClient GUIThe server is contacted and the ChatWatcher thread is started in makeContact( ): // globals private static final int PORT = 1234; // server details private static final String HOST = "localhost"; private Socket sock; private PrintWriter out; // output to the server private void makeContact( ) { try { sock = new Socket(HOST, PORT); BufferedReader in = new BufferedReader( new InputStreamReader( sock.getInputStream( ) )); out = new PrintWriter( sock.getOutputStream( ), true); new ChatWatcher(this, in).start( ); // watch for server msgs } catch(Exception e) { System.out.println(e); } } The output stream, out, is made global so messages can be sent from various methods in ChatClient. However, server input is only processed by ChatWatcher, so it is declared locally in makeContact( ) before being passed to the thread. Sending messages is simple; for example, pressing the Who button results in: out.println("who"); ChatWatcher is passed a reference to ChatClient, so it can write into the GUI's text area, jtaMesgs. This is done via the method showMsg( ). Showing a Message and ThreadsshowMsg( ) adds a message to the end of the jtaMesgs text area and is called by the ChatClient or ChatWatcher object. However, updates to Swing components must be carried out by Swing's event dispatcher thread; otherwise, synchronization problems may arise between Swing and the user threads. The SwingUtilities class contains invokeLater( ), which adds code to the event dispatcher's queue; the code must be packaged as a Runnable object: public void showMsg(final String msg) { Runnable updateMsgsText = new Runnable( ) { public void run( ) { jtaMesgs.append(msg); // append message to text area jtaMesgs.setCaretPosition( jtaMesgs.getText( ).length( ) ); // move insertion point to the end of the text } }; SwingUtilities.invokeLater( updateMsgsText ); // add code to queue } // end of showMsg( ) Though showMsg( ) can be called concurrently by ChatClient and ChatWatcher, there's no need to synchronize the method; multiple calls to showMsg( ) will cause a series of Runnable objects to be added to the event dispatcher's queue, where they'll be executed in sequential order. An invokeAndWait( ) method is similar to invokeLater( ) except that it doesn't return until the event dispatcher has executed the Runnable object. Details on these methods, and more background on Swing and threads, can be found at http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html.
Waiting for Chat MessagesThe core of the ChatWatcher class is a while loop inside run( ) that waits for a server message, processes it, and repeats. The two message types are the WHO$$... response and broadcast messages from other clients: while ((line = in.readLine( )) != null) { if ((line.length( ) >= 6) && // "WHO$$ " (line.substring(0,5).equals("WHO$$"))) showWho( line.substring(5).trim( ) ); // remove WHO$$ keyword and surrounding space else // show immediately client.showMsg(line + "\n"); } showWho( ) reformats the WHO$$... string before displaying it. The Chat ServerThe ChatServer constructor initializes a ChatGroup object to hold client information, and then enters a loop that deals with client connections by creating ChatServerHandler threads: public ChatServer( ) { cg = new ChatGroup( ); try { ServerSocket serverSock = new ServerSocket(PORT); Socket clientSock; while (true) { System.out.println("Waiting for a client..."); clientSock = serverSock.accept( ); new ChatServerHandler(clientSock, cg).start( ); } } catch(Exception e) { System.out.println(e); } } Each handler is given a reference to the ChatGroup object. The Threaded Chat HandlerThe ThreadedChatServerHandler class is similar to the ThreadedScoreHandler class from Chapter 29. The main differences are the calls it makes to the ChatGroup object while processing the client's messages. The run( ) method sets up the input and output streams to the client and adds (and later removes) a client from the ChatGroup object: public void run( ) { try { // Get I/O streams from the socket BufferedReader in = new BufferedReader( new InputStreamReader( clientSock.getInputStream( ) )); PrintWriter out = new PrintWriter( clientSock.getOutputStream( ), true); cg.addPerson(cliAddr, port, out); // add client to ChatGroup processClient(in, out); // interact with client // the client has finished when execution reaches here cg.delPerson(cliAddr, port); // remove client details clientSock.close( ); System.out.println("Client (" + cliAddr + ", " + port + ") connection closed\n"); } catch(Exception e) { System.out.println(e); } } processClient( ) checks for the client's departure by looking for a bye message. Other messages (who and text messages) are passed on to doRequest( ): private void doRequest(String line, PrintWriter out) { if (line.trim( ).toLowerCase( ).equals("who")) { System.out.println("Processing 'who'"); out.println( cg.who( ) ); } else // use ChatGroup object to broadcast the message cg.broadcast( "("+cliAddr+", "+port+"): " + line); } Storing Chat Client InformationChatGroup handles the addition/removal of client details, the answering of who messages, and the broadcasting of messages to all the clients. The details are stored in an ArrayList of Chatter objects (called chatPeople), one object for each client. A single ChatGroup object is used by all the ChatServerHandler threads, so methods that manipulate chatPeople must be synchronized. An example of this is the broadcast( ) method, which sends the specified message to all the clients, including back to the sender: synchronized public void broadcast(String msg) { Chatter c; for(int i=0; i < chatPeople.size( ); i++) { c = (Chatter) chatPeople.get(i); c.sendMessage(msg); } } The Chatter ClassThe client details managed by a Chatter object are its address, port, and PrintWriter output stream. The address and port are employed to identify the client (a client has no name). The output stream is used to send messages to the client. For example: private PrintWriter out; // global public void sendMessage(String msg) { out.println(msg); } DiscussionMessages can only be broadcast, and there's no capability to send private messages, though that's easily fixed, as you'll see in later examples. The communication protocol is defined by the server, so creating a new message format is simple: message / toName The implementation would require that clients be named; then ChatServerHandler could examine the toName part of the message and route the message only to that client. The delivery would use a new method in ChatGroup (e.g., void sendPrivate(String message, String toName)) to call the sendMessage( ) method of the Chatter object for toName. Other communication patterns (e.g., periodic announcements linked to the current time) are straightforward to implement by extending the protocols supported by ChatServer. This illustrates one of the advantages of localizing communication management in the server. Central control has its drawbacks as well, namely that if ChatServer fails, then the entire system fails. However, the nonfunctioning of a single ChatServerHandler tHRead doesn't affect the others. A likely scenario is one thread being made to wait indefinitely by a client who doesn't communicate with it. |