Firewalls


Client/Server Programming in Java

The four examples in this section have a similar structure: the server maintains a high scores list, and the clients read and add scores to the list. Here are the four variants of this idea:

  1. A client and sequential server. The server can process only one client at a time. The TCP/IP protocol is utilized.

  2. The same client and the same protocol as (1), but the server is threaded, enabling it to process multiple clients at the same time. Synchronization issues arise because the high score list may be accessed by multiple threads at the same time.

  3. The same client and the same protocol as (1) and (2), but Java's NIO is used to implement a multiplexing server without the need of threads.

  4. A client and server using UDP. The server exhibits multiplexing due to the self-contained nature of datagram communication.

TCP Client and Sequential Server

The communications network created by the client and server is shown in Figure 29-6.

The server instantiates a ServerSocket object at port 1234 and waits for a connection from a client by calling accept( ). When a connection is established, a new socket is created (some people call this a rendezvous socket), which is used for the subsequent communication with the client. Input and output streams are layered on top of the socket, utilizing the bidirectional nature of a TCP link. When the client has finished, the rendezvous socket is closed (terminated), and the server waits for another connection.

Figure 29-6. Client and sequential server


The client instantiates a Socket object to link to the server at its specified IP address and port. When a connection is obtained, the client layers input and output streams on top of its socket and commences communication.

A great aid to understanding networked applications is to understand the protocol employed between clients and the server. In simple examples (such as the ones in this chapter), that means the message interactions between network entities.

A client can send the following messages, which terminate with a newline character:


get

The server returns the high score list.


score name & score &

The server adds the name/score pair to its list.


bye

The server closes the client's connection.

A client only receives one kind of message from the server, the high scores list, which is sent in response to a get request. The list is sent as a string terminated with a newline character. The string has this format:

     HIGH$$ name1 & score1 & .... nameN & scoreN &

The server stores only 10 names and scores at most, so the string is unlikely to be excessively long.

Class diagrams

The class diagrams for this example are given in Figure 29-7. Only the public methods are shown.

A HighScores object manages the high scores list, with each name/score pair held in a ScoreInfo object.

The client code (ScoreClient) can be found in the NetBasics/ directory, while the sequential server (ScoreServer and its support classes) is in NetBasics/Sequential/. ScoreClient is used as the client for the sequential, threaded, and multiplexing servers.


Figure 29-7. Classes for the client and sequential server


The sequential score server

The constructor for the ScoreServer creates the ServerSocket object, then enters a loop that waits for a client connection by calling accept( ). When accept( ) returns, the server processes the client and goes back to waiting for the next connection:

     public ScoreServer( )     {       hs = new HighScores( );       try {         ServerSocket serverSock = new ServerSocket(PORT);         Socket clientSock;         BufferedReader in;     // i/o for the server         PrintWriter out;         while (true) {           System.out.println("Waiting for a client...");           clientSock = serverSock.accept( );           System.out.println("Client connection from " +                         clientSock.getInetAddress( ).getHostAddress( ) );           // Get I/O streams from the socket           in  = new BufferedReader(  new InputStreamReader(                                              clientSock.getInputStream( ) ) );           out = new PrintWriter( clientSock.getOutputStream( ), true );           processClient(in, out); // interact with a client           // Close client connection           clientSock.close( );           System.out.println("Client connection closed\n");           hs.saveScores( );      // backup scores after client finish         }       }       catch(Exception e)       {  System.out.println(e);  }     }  // end of ScoreServer( )

The server-side socket is created with the ServerSocket class:

     ServerSocket serverSock = new ServerSocket(PORT);

The waiting for a connection is done via a call to accept( ):

     Socket clientSock;     clientSock = serverSock.accept( );

When accept( ) returns, it instantiates the rendezvous socket, clientSock. clientSock can be used to retrieve client details, such as its IP address and host name.

The input stream is a BufferedReader to allow calls to readLine( ). Since client messages end with a newline, this is a convenient way to read them. The output stream is a PrintWriter, allowing println( ) to be employed. The stream's creation includes a Boolean to switch on auto-flushing, so there's no delay between printing and output to the client:

     in = new BufferedReader( new InputStreamReader( clientSock.getInputStream( ) ));     out = new PrintWriter( clientSock.getOutputStream( ), true);

The client is processed by a call to processClient( ), which contains the application-specific coding. Almost all the rest of ScoreServer is reusable in different sequential servers.

When processClient( ) returns, the communication has finished and the client link can be closed:

     clientSock.close( );

The call to saveScores( ) in the HighScores object is a precautionary measure: it saves the high scores list to a file, so data won't be lost if the server crashes.

Most of the code inside the constructor is inside a TRy-catch block to handle I/O and network exceptions.

Processing a client

processClient( ) deals with message extraction from the input stream, which is complicated by having to deal with link termination.

The connection may close because of a network fault, which is detected by a null being returned by the read, or may be signaled by the client sending a "bye" message. In both cases, the loop in processClient( ) finishes, passing control back to the ScoreServer constructor:

     private void processClient(BufferedReader in, PrintWriter out)     {       String line;       boolean done = false;       try {         while (!done) {           if((line = in.readLine( )) == null)             done = true;           else {             System.out.println("Client msg: " + line);             if (line.trim( ).equals("bye"))               done = true;             else               doRequest(line, out);           }         }       }       catch(IOException e)       {  System.out.println(e);  }     }  // end of processClient( )

The method uses a try-catch block to deal with possible I/O problems.

processClient( ) does a very common task and is portable across various applications. It requires that the termination message ("bye") can be read using readLine( ).

doRequest( ) deals with the remaining two kinds of client message: "get" and "score". Most of the work in doRequest( ) involves the checking of the input message embedded in the request string. The score processing is carried out by the HighScores object:

     private void doRequest(String line, PrintWriter out)     {       if (line.trim( ).toLowerCase( ).equals("get")) {         System.out.println("Processing 'get'");         out.println( hs.toString( ) );       }       else if ((line.length( ) >= 6) &&     // "score"           (line.substring(0,5).toLowerCase( ).equals("score"))) {         System.out.println("Processing 'score'");         hs.addScore( line.substring(5) );    // cut the score keyword       }       else         System.out.println("Ignoring input line");     }

It's a good idea to include a default else case to deal with unknown messages. In doRequest( ), the server only reports, problems to standard output on the server side. It may be advisable to send a message back to the client.

Maintaining the scores information

The HighScores object maintains an array of ScoreInfo objects, which it initially populates by calling loadScores( ) to load the scores.txt text file from the current directory. saveScores( ) writes the array's contents back into scores.txt.

It's preferable to maintain simple data (such as these name/score pairs) in text form rather than a serialized object. This makes the data easy to examine and edit with ordinary text-processing tools.

The scores client

The ScoreClient class seems complicated because of its GUI interface, shown in Figure 29-8.

Figure 29-8. A ScoreClient object


The large text area is represented by the jtaMesgs object. Two text fields are used for entering a name and score. Pressing Enter in the score field will trigger a call to actionPerformed( ) in the object, as will pressing the Get Scores button.

ScoreClient calls makeContact( ) to instantiate a Socket object for the server at its specified IP address and port. When the connection is made, input and output streams are layered on top of its socket:

     // global constants and variables     private static final int PORT = 1234;     // server details     private static final String HOST = "localhost";     private Socket sock;     private BufferedReader in;     // i/o for the client     private PrintWriter out;     private void makeContact( )     {       try {         sock = new Socket(HOST, PORT);         in  = new BufferedReader( new InputStreamReader( sock.getInputStream( ) ));         out = new PrintWriter( sock.getOutputStream( ), true );       }       catch(Exception e)       {  System.out.println(e);  }     }

"localhost" is given as the server's host name since the server is running on the same machine as the client. "localhost" is a loopback address and can be employed even when the machine is disconnected from the Internet, though the TCP/IP must be set up in the OS. On most systems (including Windows), it's possible to type the command ping localhost to check the functioning of the loopback.

actionPerformed( ) differentiates between the two kinds of user input:

     public void actionPerformed(ActionEvent e)      /* Either a name/score is to be sent or the "Get Scores"         button has been pressed. */     {       if (e.getSource( ) == jbGetScores)         sendGet( );       else if (e.getSource( ) == jtfScore)         sendScore( );     }

sendGet( ) shows how the client sends a message to the server (in this case a "get" string) and waits for a response (a "HIGH$$..." string), which it displays in the text area:

     private void sendGet( )     {     // Send "get" command, read response and display it     // Response should be "HIGH$$ n1 & s1 & .... nN & sN & "       try {         out.println("get");         String line = in.readLine( );         System.out.println(line);         if ((line.length( ) >= 7) &&     // "HIGH$$ "             (line.substring(0,6).equals("HIGH$$")))           showHigh( line.substring(6).trim( ) );             // remove HIGH$$ keyword and surrounding spaces         else    // should not happen           jtaMesgs.append( line + "\n");       }       catch(Exception ex)       { jtaMesgs.append("Problem obtaining high scores\n");         System.out.println(ex);       }     }

Figure 29-9 shows how the high score list looks in the text area window.

sendGet( ) makes the client wait for the server to reply:

     out.println("get");     String line = in.readLine( );

This means the client will be unable to process further user commands until the server has sent back the high scores information. This is a bad design strategy for more complex client applications. The chat client from Chapter 30 shows how

Figure 29-9. High scores output in the client


threads can be employed to separate network interaction from the rest of the application.

The client should send a "bye" message before breaking a connection, and this is achieved by calling closeLink( ) when the client's close box is clicked:

     public ScoreClient( )     {  super( "High Score Client" );        initializeGUI( );        makeContact( );        addWindowListener( new WindowAdapter( ) {          public void windowClosing(WindowEvent e)          { closeLink( ); }        });        setSize(300,450);        setVisible(true);     } // end of ScoreClient( );     private void closeLink( )     { try {         out.println("bye");    // tell server         sock.close( );       }       catch(Exception e)       {  System.out.println( e );  }       System.exit( 0 );     }

A simple alternative client

Since the client is communicating with the server using TCP/IP, it's possible to replace the client with the telnet command:

     telnet localhost 1234

This will initiate a TCP/IP link at the specified host address and port, where the server is listening. The advantage is the possibility of testing the server without writing a client. The disadvantage is that the user must type the messages directly, without the help of a GUI interface. Figure 29-10 shows a Telnet window after the server has responded to a "get" message.

Figure 29-10. A Telnet client


It may be necessary to switch on the Local Echo feature in Telnet's preferences dialog before the user's typing (e.g., the "get" message) is seen on-screen.

TCP Client and Multithreaded Server

The ScoreServer class of the previous example is inadequate for real server-side applications because it can only deal with one client at a time. The ThreadedScoreServer class described in this section solves that problem by creating a thread (a ThreadedScoreHandler object) to process each client who connects. Since a thread interacts with each client, the main server is free to accept multiple connections. Figure 29-11 shows this in diagram form.

The ScoreClient class needn't be changed: a client is unaware that it's talking to a thread.

The HighScores object is referenced by all the threads (indicated by the dotted lines in Figure 29-11), so the scores can be read and changed. The possibility of change means that the data inside HighScores must be protected from concurrent updates by multiple threads and from an update occurring at the same time as the scores are being read. These synchronization problems are quite easily solved, as detailed in the rest of this section.

Figure 29-11. Clients and multithreaded server


The code for the multithreaded server (ThreadedScoreServer and its support classes) is in NetBasics/Threaded/. The client code (ScoreClient) can be found in the NetBasics/ directory.


ThreadedScoreServer is simpler than its sequential counterpart since it no longer processes client requests. It consists only of a constructor that sets up a loop waiting for client contacts, handled by threads:

     public ThreadedScoreServer( )     {       hs = new HighScores( );       try {         ServerSocket serverSock = new ServerSocket(PORT);         Socket clientSock;         String cliAddr;         while (true) {           System.out.println("Waiting for a client...");           clientSock = serverSock.accept( );           cliAddr = clientSock.getInetAddress( ).getHostAddress( );           new ThreadedScoreHandler(clientSock, cliAddr, hs).start( );         }       }       catch(Exception e)       {  System.out.println(e);  }     }  // end of ThreadedScoreServer( )

Each thread gets a reference to the client's rendezvous socket and the HighScores object.

ThreadedScoreHandler contains almost identical code to the sequential ScoreServer class; for example, it has processClient( ) and doRequest( ) methods. The main difference is the run( ) method:

     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);         processClient(in, out); // interact with a client         // Close client connection         clientSock.close( );         System.out.println("Client (" + cliAddr +                                   ") connection closed\n");       }       catch(Exception e)       {  System.out.println(e);  }     }

A comparison with the sequential server shows that run( ) contains code like that executed in ScoreServer's constructor after a rendezvous socket is created.

Maintaining scores information

The ThreadedScoreHandler objects call HighScores's toString( ) and addScore( ) methods. toString( ) returns the current scores list as a string, addScore( ) updates the list. The danger of concurrent access to the scores list is easily avoided since the data is maintained in a single object referenced by all the threads. Manipulation only occurs through the toString( ) and addScore( ) methods.

A lock can be placed on the object by making the toString( ) and addScore( ) methods synchronized:

          synchronized public String toString( )     { ... }     synchronized public void addScore(String line)     { ... }

The lock means that only a single thread can be executing inside toString( ) or addScore( ) at any time. Concurrent access is no longer possible. This approach is relatively painless because of my decision to wrap the shared data (the scores list) inside a single shared object. A possible concern may be the impact on response times by prohibiting concurrent access, but toString( ) and addScores( ) are short, simple methods that quickly return after being called.

One advantage of the threaded handler approach isn't illustrated by this example: each handler can store client-specific data locally. For instance, each ThreadedScoreHandler could maintain information about the history of client communication in its session. Since this data is managed by each thread, the main server is relieved of unnecessary complexity.

TCP Client and Multiplexing Server

J2SE 1.4. introduced nonblocking sockets, which allow networked applications to communicate without blocking the processes/threads involved. This makes it possible to implement a server that can multiplex (switch) between different clients without the need for threads.

At the heart of this approach is a method called select( ), which may remind the Unix network programmers among you of the select( ) system call. They're closely related, and the coding of a multiplexing server in Java is similar to one written in C on Unix.

An advantage of the multiplexing server technique is the return to a single server without threads. This may be an important gain on a platform with limited resources. A related advantage is the absence of synchronization problems with shared data because there are no threads. The only process is the server.

A disadvantage is that any client-specific state (which had previously been placed in each thread) must be maintained by the server. For instance, if multiple clients are connected to the server, each with a communications history to be maintained, then those histories will have to be held by the server.

Nonblocking sockets mean that method calls that might potentially block forever, such as accept( ) and readLine( ), need to be executed only when data is known to be present or can be wrapped in timeouts. This is particularly useful for avoiding some forms of hacker attack or dealing with users who are too slow.

Figure 29-12 shows the various objects involved in the multiplexing server.

Figure 29-12. Clients and multiplexing server


Selector is the main new class in the nonblocking additions to Java. A Selector object can monitor multiple socket channels and returns a collection of keys (client requests) as required. A socket channel is a new type of socket (built using the SocketChannel class).

Each ClientInfo object in the hash map in Figure 29-12 contains details about a client and methods for receiving and sending messages to the client. The principal complexity of ClientInfo is in its handling of nonblocking client input.

The main purpose of the server is to listen to several socket channels at once by using a Selector object. Initially, the server's own socket channel is added to the selector, and subsequent connections by new clients, represented by new socket channels, are also added.

When input arrives from a client, it's read immediately. However, the input may contain only part of a message: there's no waiting for a complete message.

The code for the multiplexing server (SelectScoreServer and its support classes) is in NetBasics/NIO/. The client code (ScoreClient) can be found in the NetBasics/ directory.


The multiplexing scores server

A pseudocode algorithm for the server is given below:

     create a SocketChannel for the server;     create a Selector;     register the SocketChannel with the Selector (for accepts);     while(true) {       wait for keys (client requests) in the Selector;       for each key in the Selector {         if (key is Acceptable) {           create a new SocketChannel for the new client;           register the SocketChannel with the Selector (for reads);         }         else if (key is Readable) {           extract the client SocketChannel from the key;           read from the SocketChannel;           store partial message, or process full message;         }       }     }

The server waits inside a while loop for keys to be generated by the Selector. A key contains information about a pending client request. A Selector object may store four types of key:

  • A request by a new client to connect to the server (an isAcceptable key)

  • A request by an existing client to deliver some input (an isReadable key)

  • A request by an existing client for the server to send it data (an isWriteable key)

  • A request by a server accepting a client connection (an isConnectable key)

The first two request types are used in my multiplexing server. The last type of key is typically employed by a nonblocking client to detect when a connection has been made with a server.

The socket channel for the server is created in the SelectScoreServer( ) constructor:

     ServerSocketChannel serverChannel = ServerSocketChannel.open( );     serverChannel.configureBlocking(false);   // use nonblocking mode     ServerSocket serverSocket = serverChannel.socket( );     serverSocket.bind( new InetSocketAddress(PORT_NUMBER) );

The nonblocking nature of the socket is achieved by creating a ServerSocketChannel object and then by extracting a ServerSocket.

The server's socket channel has a Selector registered with it to collect connection requests:

     Selector selector = Selector.open( );     serverChannel.register(selector, SelectionKey.OP_ACCEPT);

Other possible options to register( ) are OP_READ, OP_WRITE, and OP_CONNECT, corresponding to the different types of keys.

The while loop in the pseudocode can be translated fairly directly into real code:

     while (true) {       selector.select( );                  // wait for keys       Iterator it = selector.selectedKeys( ).iterator( );       SelectionKey key;       while (it.hasNext( )) {              // look at each key         key = (SelectionKey) it.next( );   // get a key         it.remove( );                      // remove it         if (key.isAcceptable( ))       // a new connection?           newChannel(key, selector);         else if (key.isReadable( ))    // data to be read?           readFromChannel(key);         else           System.out.println("Did not process key: " + key);       }     }

Having the server accept a new client

newChannel( ) is called when a new client has requested a connection. The connection is accepted and registered with the selector to make it collect read requests (i.e., notifications that client data have arrived):

     private void newChannel(SelectionKey key, Selector selector)     {       try {         ServerSocketChannel server = (ServerSocketChannel) key.channel( );         SocketChannel channel = server.accept( );         // get channel         channel.configureBlocking (false);          // use nonblocking         channel.register(selector, SelectionKey.OP_READ);                                // register it with selector for reading         clients.put(channel, new ClientInfo(channel, this) );   // store info       }       catch (IOException e)       {  System.out.println( e ); }     }

The call to accept( ) is nonblocking: it'll raise a NotYetBoundException if there's no pending connection. The connection is represented by a SocketChannel (a nonblocking version of the Socket class), and this is registered with the Selector to collect its read requests.

Since a new client has just accepted, a new ClientInfo object is added to the HashMap. The key for the HashMap entry is the client's channel, which is unique.

Having the server accept a request from an existing client

readFromChannel( ) is called when there's a request by an existing client for the server to read data. As mentioned earlier, this may not be a complete message, which introduces some problems. It's necessary to store partial messages until they're completed (a complete message ends with a \n). The reading and storage is managed by the ClientInfo object for the client:

     private void readFromChannel(SelectionKey key)     // process input that is waiting on a channel     {       SocketChannel channel = (SocketChannel) key.channel( );       ClientInfo ci = (ClientInfo) clients.get(channel);       if (ci == null)         System.out.println("No client info for channel " + channel);       else {         String msg = ci.readMessage( );         if (msg != null) {           System.out.println("Read message: " + msg);           if (msg.trim( ).equals("bye")) {             ci.closeDown( );             clients.remove(channel);  // delete ci from hash map           }           else             doRequest(msg, ci);         }       }     } // end of readFromChannel( )

readFromChannel( ) exTRacts the channel reference from the client request and uses it to look up the associated ClientInfo object in the hash map. The ClientInfo object deals with the request via a call to readMessage( ), which returns the full message or null if the message is incomplete.

If the message is "bye", then the server requests that the ClientInfo object closes the connection, and the object is discarded. Otherwise, the message is processed using doRequest( ):

     private void doRequest(String line, ClientInfo ci)     /*  The input line can be one of:                "score name & score &"         or     "get"     */     {       if (line.trim( ).toLowerCase( ).equals("get")) {         System.out.println("Processing 'get'");         ci.sendMessage( hs.toString( ) );       }       else if ((line.length( ) >= 6) &&     // "score "           (line.substring(0,5).toLowerCase( ).equals("score"))) {         System.out.println("Processing 'score'");         hs.addScore( line.substring(5) );    // cut the score keyword       }       else         System.out.println("Ignoring input line");     }

The input line can be a "get" or a "score" message. If it's a "get", then the ClientInfo object will be asked to send the high scores list to the client. If the message is a new name/score pair, then the HighScores object will be notified.

Storing client information

ClientInfo has three public methods: readMessage( ), sendMessage( ), and closeDown( ). readMessage( ) reads input from a client's socket channel. sendMessage( ) sends a message along a channel to the client, and closeDown( ) closes the channel.

Data is read from a socket channel into a Buffer object holding bytes. A Buffer object is a fixed-size container for items belonging to a Java base type, such as byte, int, char, double, and Boolean. It works in a similar way to a file: there's a "current position," and after each read or write operation, the current position indicates the next item in the buffer.

There are two important size notions for a buffer: its capacity and its limit. The capacity is the maximum number of items the buffer can contain, and the limit is a value between 0 and capacity, representing the current buffer size.

Since data sent through a socket channel is stored in a ByteBuffer object (a buffer of bytes), it's necessary to translate the data (decode it) into a String before being tested to see if the data is complete or not. A complete message is a string ending with a \n.

The constructor for ClientInfo initializes the byte buffer and the decoder:

     // globals     private static final int BUFSIZ = 1024;  // max size of a message     private SocketChannel channel;  // the client's channel     private SelectScoreServer ss;   // the top-level server     private ByteBuffer inBuffer;    // for storing input     private Charset charset;     // for decoding bytes --> string     private CharsetDecoder decoder;     public ClientInfo(SocketChannel chan, SelectScoreServer ss)     {       channel = chan;       this.ss = ss;       inBuffer = ByteBuffer.allocateDirect(BUFSIZ);       inBuffer.clear( );       charset = Charset.forName("ISO-8859-1");       decoder = charset.newDecoder( );       showClientDetails( );     }

The buffer is a fixed size, 1,024 bytes. The obvious question is whether this is sufficient for message passing. The only long message is the high scores list, which is sent from the server back to the client, and 1,024 characters (bytes) should be enough.

Reading a message

readMessage( ) is called when the channel contains data:

     public String readMessage( )     {       String inputMsg = null;       try {         int numBytesRead = channel.read(inBuffer);         if (numBytesRead == -1) {     // channel has gone           channel.close( );           ss.removeChannel(channel); // tell SelectScoreServer         }         else           inputMsg = getMessage(inBuffer);       }       catch (IOException e)       {  System.out.println("rm: " + e);          ss.removeChannel(channel); // tell SelectScoreServer       }       return inputMsg;     }  // end of readMessage( )

A channel read( ) will not block, returning the number of bytes read (which may be 0). If it returns -1, then something has happened to the input channel; the channel is closed and the ClientInfo object removed by calling removeChannel( ) in the main server. read( ) may raise an IOException, which triggers the same removal.

The real work of reading a message is done by getMessage( ):

     private String getMessage(ByteBuffer buf)     {       String msg = null;       int posn = buf.position( );   // current buffer sizes       int limit = buf.limit( );       buf.position(0);    // set range of bytes for translation       buf.limit(posn);       try {   // translate bytes-->string         CharBuffer cb = decoder.decode(buf);         msg = cb.toString( );       }       catch(CharacterCodingException cce)       { System.out.println( cce );  }       // System.out.println("Current msg: " + msg);       buf.limit(limit);    // reset buffer to full range of bytes       buf.position(posn);       if (msg.endsWith("\n")) {    // I assume '\n' is the last char         buf.clear( );         return msg;       }       return null;      // since I still only have a partial mesg     }  // end of getMessage( )

position( ) returns the index position of the next empty spot in the buffer: bytes are stored from position 0 up to posn-1. The current limit for the buffer is stored and then changed to be the current position. This means that when decode( ) is called, only the part of the buffer containing bytes will be considered. After the translation, the resulting string is checked to see if it ends with a \n, in which case the buffer is reset (treated as being empty) and the message returned.

There's a potential problem with this approach: the assumption that the last character in the buffer will be \n. This depends on what data is present in the channel when read( ) is called in readMessage( ). It might be that the channel contains the final bytes of one message and some bytes of the next, as illustrated by Figure 29-13.

The read( ) call in Figure 29-13 will add the bytes for a\nbbb to the byte buffer, placing \n in the midst of the buffer rather than at the end. Consequently, an endsWith( ) test of the extracted string is insufficient:

     if (msg.endsWith("\n")) {. . .}

In my tests, this problem never appeared since read( ) was called quickly, adding each incoming byte to the buffer as soon as it arrived; an \n was always read before the next byte appeared in the channel.

Figure 29-13. Reading bytes in a channel


Sending a message

sendMessage( ) sends a specified message along the channel back to the client:

     public boolean sendMessage(String msg)     {       String fullMsg = msg + "\r\n";       ByteBuffer outBuffer = ByteBuffer.allocateDirect(BUFSIZ);       outBuffer.clear( );       outBuffer.put( fullMsg.getBytes( ) );       outBuffer.flip( );       boolean msgSent = false;       try {         // send the data; don't assume it goes all at once         while( outBuffer.hasRemaining( ) )           channel.write(outBuffer);         msgSent = true;       }       catch(IOException e)       { System.out.println(e);         ss.removeChannel(channel); // tell SelectScoreServer       }       return msgSent;     }  // end of sendMessage( )

Two issues are at work here:

  • The need to translate the string to bytes in a buffer before transmission

  • Dealing with the case that the message requires several writes before it's all placed onto the channel

The buffer is filled with the message. The flip( ) call sets the buffer limit to the current position (i.e., the position just after the end of the message) and then sets the current position back to 0.

The while loop uses hasRemaining( ), which returns TRue as long as elements remain between the current position and the buffer limit. Each write( ) calls advances the position through the buffer. One write( ) call should be sufficient to place all the bytes onto the channel unless the buffer is large or the channel overloaded.

write( ) may raise an exception, which causes the channel and the ClientInfo object to be discarded.

Waiting to send

There's a potential problem with sendMessage( ), pointed out to me by Marcin Mank. The offending code is in its while loop:

     while( outBuffer.hasRemaining( ) )       channel.write(outBuffer);

If the channel's output buffer cannot accept any more data, the write( ) call will return 0. outBuffer won't be emptied, and the while loop will keep iterating. This looping will cause sendMessage( ) to wait, which in turn will cause the server to wait in doRequest( ), thereby stopping any other clients from being processed.

Though this is a problem, it's unlikely to occur in this example since the channel is only being written to by a single process. This means that any delays will be brief and probably acceptable.

Implementing a decent solution involves a considerable increase in complexity, so I'll only sketch out what the code might look like.

When sendMessage( ) TRies to write outBuffer to the channel and only some (or none) of it is written, the method must request a notification when the write can be completed. The unsent message must be stored until that notification arrives. The ideas are shown in TRySendMessage( ):

     private void trySendMessage( )     {       int len = outBuffer.hasRemaining( );   // outBuffer must now be global       try {         int nBytes = channel.write(outBuffer);         if (nBytes != len)   // data not all sent           channel.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);                 // need a way of referring to server's top-level selector         else {  // data all sent           outBuffer.clear( );           channel.register(selector, SelectionKey.OP_READ);   // remove OP_WRITE         }       }       catch(IOException e)       { System.out.println(e);         ss.removeChannel(channel); // tell SelectScoreServer       }     }

The notification technique relies on registering an interest in the OP_WRITE key for that channel. When the channel becomes writeable, the key change will be caught by the while loop in SelectScoreServer, TRiggering a call to a new finishSend( ) method:

     // in SelectScoreServer while-loop     else if (key.isWritable( ))    // data can now be written       finishSend(key);     private void finishSend(SelectionKey key)     // send remaining output     {       SocketChannel channel = (SocketChannel) key.channel( );       ClientInfo ci = (ClientInfo) clients.get(channel);       if (ci == null)         System.out.println("No client info for channel " + channel);       else         ci.trySendMessage( );     } // end of finishSend( )

An example of this approach in a complete server can be found in Chapter 12 of Java Threads by Scott Oaks and Henry Wong (O'Reilly).

Unfortunately, this coding approach isn't good enough. The server may want to send several messages to the client, so it would have to store each one until the channel becomes writeable. This suggests the need for a buffer list of delayed messages.

The example in Java Threads avoids the messiness of a buffer list by canceling all further sends to the client until the single output buffer has been emptied.


The client

The ScoreClient class from previous examples can stay as the client-side application, unchanged. The advantage is that high-level I/O can be used on the client side instead of byte buffers.

A nonblocking client may be useful for attempting a connection without having to wait for the server to respond. Whether a connection operation is in progress can be checked by calling isConnectionPending( ).

UDP Client and Server

All the previous examples use TCP, but the client and server in this section are recoded to utilize UDP communication. The result is another form of multiplexing server but without the need for nonblocking sockets. The complexity of the code is much less than in the last example.

The downsides of this approach are the usual ones related to UDP: the possibility of packet loss and reordering (these problems didn't occur in my tests, which were run on the same machine) and machines connected by a LAN.

Another disadvantage of using UDP is the need to write a client before the server can be tested. Telnet uses TCP/IP and can't be employed.

Figure 29-14 illustrates the communication between ScoreUDPClient objects and the ScoreUDPServer.

Figure 29-14. UDP clients and server


Since no long-term connection exists between a client and the server, multiple clients could send datagrams at the same time. The server will process them in their order of arrival. A datagram automatically includes the hostname and IP address of its sender, so response messages can be easily sent back to the right client.

The code for the UDP client and server can be found in the NetBasics/udp/ directory.


The UDP-based scores server

The server sets up a DatagramSocket listening at port 1234, enters a loop to wait for a packet, processes it, and then repeats:

     // globals     private static final int PORT = 1234;     private static final int BUFSIZE = 1024;   // max size of a message     private HighScores hs;     private DatagramSocket serverSock;     public ScoreUDPServer( )     {       try {  // try to create a socket for the server         serverSock = new DatagramSocket(PORT);       }       catch(SocketException se)       {  System.out.println(se);          System.exit(1);       }       waitForPackets( );     }     private void waitForPackets( )     {       DatagramPacket receivePacket;       byte data[];       hs = new HighScores( );       try {         while (true) {           data = new byte[BUFSIZE];  // set up an empty packet           receivePacket = new DatagramPacket(data, data.length);           System.out.println("Waiting for a packet...");           serverSock.receive( receivePacket );           processClient(receivePacket);           hs.saveScores( );   // backup scores after each package         }       }       catch(IOException ioe)       {  System.out.println(ioe);  }     }  // end of waitForPackets( )

The data in a packet is written to a byte array of a fixed size. The size of the array should be sufficient for the kinds of messages being delivered.

processClient( ) extracts the client's address and IP number and converts the byte array into a string:

     InetAddress clientAddr = receivePacket.getAddress( );     int clientPort = receivePacket.getPort( );     String clientMesg = new String( receivePacket.getData( ), 0,                              receivePacket.getLength( ) );

These are passed to doRequest( ), which deals with the two possible message types: get and score. There's no bye message because no long-term connection needs to be broken. Part of the reason for the simplicity of coding with UDP is the absence of processing related to connection termination (whether intended or due to an error).

A reply is sent by calling sendMessage( ):

     private void sendMessage(InetAddress clientAddr, int clientPort, String mesg)     // send message to socket at the specified address and port     {       byte mesgData[] = mesg.getBytes( );   // convert to byte[] form       try {         DatagramPacket sendPacket =                   new DatagramPacket( mesgData, mesgData.length,                                            clientAddr, clientPort);         serverSock.send( sendPacket );       }       catch(IOException ioe)       {  System.out.println(ioe);  }     }

The UDP-based scores client

The client has the same GUI interface as before (see Figure 29-15), allowing the user to send commands by clicking on the Get Scores button or by entering name/score pairs into the text fields.

Figure 29-15. The UDP client GUI


The application uses the implicit thread associated with Swing's processing of GUI events to send commands to the server. Processing of the messages returned by the server is handled in the application's main execution thread.

The constructor starts the application thread by setting up the client's datagram socket:

     // globals     private static final int SERVER_PORT = 1234;     // server details     private static final String SERVER_HOST = "localhost";     private static final int BUFSIZE = 1024;   // max size of a message     private DatagramSocket sock;     private InetAddress serverAddr;     public ScoreUDPClient( )     {  super( "High Score UDP Client" );        initializeGUI( );        try {   // try to create the client's socket          sock = new DatagramSocket( );        }        catch( SocketException se ) {          se.printStackTrace( );          System.exit(1);        }        try {  // try to turn the server's name into an internet address          serverAddr = InetAddress.getByName(SERVER_HOST);        }        catch( UnknownHostException uhe) {          uhe.printStackTrace( );          System.exit(1);        }        setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );        setSize(300,450);        setResizable(false);    // fixed-size display        setVisible(true);        waitForPackets( );     } // end of ScoreUDPClient( );

waitForPackets( ) bears a striking resemblance to the same named method in the server. It contains a loop that waits for an incoming packet (from the server), processes it, and then repeats:

     private void waitForPackets( )     { DatagramPacket receivePacket;       byte data[];       try {         while (true) {           // set up an empty packet           data = new byte[BUFSIZE];           receivePacket = new DatagramPacket(data, data.length);           System.out.println("Waiting for a packet...");           sock.receive( receivePacket );           processServer(receivePacket);         }       }       catch(IOException ioe)       {  System.out.println(ioe);  }     }

processServer( ) extracts the address, port number, and message string from the packet, prints the address and port to standard output, and writes the message to the text area.

The GUI thread is triggered by the system calling actionPerformed( ):

     public void actionPerformed(ActionEvent e)     {       if (e.getSource( ) == jbGetScores) {         sendMessage(serverAddr, SERVER_PORT, "get");         jtaMesgs.append("Sent a get command\n");       }       else if (e.getSource( ) == jtfScore)         sendScore( );     }

An important issue with threads is synchronization of shared data. The DatagramSocket is shared, but the GUI thread only transmits datagrams; the application thread only receives, so conflict is avoided.

The JTextArea component, jtaMesgs, is shared between the threads, as a place to write messages for the user. However, there's little danger of multiple writes occurring at the same time due to the request/response nature of the communication between the client and server: a message from the server only arrives as a response to an earlier request by the client. Synchronization would be more important if the server could deliver messages to the client at any time, as you'll see in the chat systems developed in Chapter 30.

Another reason for the low risk of undue interaction is that the GUI thread only writes short messages into the text area, which are added quickly.



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