23 Apr 2012 [ 201 week15 ]

Here's a final version of our our Hangman client and server. I did several things to clean up the code:

Note that I still haven't added in any error handling. Properly handling all possible I/O errors and dealing with malformed messages would probably triple the code size.

package org.ilzd.netexamples;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Client for hangman game.
 * 
 * @author jlepak
 */
public class HangmanClient {
    
    /** Communicate with server.
     */
    public static class Handler extends SimpleComm {
        public Handler(Socket server) throws IOException {
            super(server);
        }

        /** Repeatedly play game as long as user elects to start again. */
        public void run() {
            try {
                while (runGame()) {}
                close();
            } catch (IOException ex) {
                System.err.println("IOException caught.  Exiting.");
                System.exit(1);
            }
        }

        /** Handle a single game. */
        public boolean runGame() throws IOException {
            // First message receives word length.
            int length = Integer.parseInt(read());
            // Track status using a string with "-" for missing entries.
            String blanks = "";
            for (int i = 0; i < length; ++i)
                blanks += HangmanServer.MISSING;

            boolean gameOver = false;
            Scanner input = new Scanner(System.in);

            // Repeated send guess and receive response.
            while (!gameOver) {
                System.out.println(blanks);
                System.out.print("\nGuess: ");
                char guess = input.next().charAt(0);
                send(guess);

                String response = read();
                // Response is in form of "ID [argument]"
                Scanner rscan = new Scanner(response);
                // All enum types have a valueOf() method that converts from 
                // a String to the enum value.
                HangmanServer.ID code = HangmanServer.ID.valueOf(rscan.next());
                // 3 possible cases for response type
                switch (code) {
                case GAMEOVER:
                    gameOver = true;
                    break;
                case CORRECT:
                    blanks = rscan.next();
                    break;
                case WRONG:
                    System.out.println("You have " + rscan.next() + " misses left");
                    break;
                }
            }
            // Get final message.
            String response = read();
            Scanner rscan = new Scanner(response);
            HangmanServer.ID code = HangmanServer.ID.valueOf(rscan.next());
            if (code == HangmanServer.ID.WIN)
                System.out.println("You won!");
            else {
                System.out.println("You lost.  The word was: " + rscan.next());
            }
            // Send message to indicate if you want to play another game.
            System.out.println("Play again (y/n)?");
            String again = input.next();
            if (again.charAt(0) == 'y') {
                send(HangmanServer.ID.YES);
                return true;
            } else {
                send(HangmanServer.ID.NO);
                return false;
            }
        }
    }
    
    public static void main(String[] args) throws IOException {
        // Connect to server.
        Socket server = new Socket("localhost", HangmanServer.PORT);

        new Handler(server).run();
    }
}
package org.ilzd.netexamples;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Server for hangman game.
 * 
 * Protocol for communication with client is as follows:
 * 
 * Each message is text terminated by a newline.
 * 
 * Server initiates by sending length of WORD (the target).
 * 
 * Each turn, client sends letter to guess, and server replies
 * with a message indicating correctness and status.  If guess 
 * is wrong, server replies with message "WRONG N" where N is
 * the number of allowed wrong guesses remaining.  If guess is 
 * correct, server replies with "CORRECT LETTERS", where LETTERS
 * is a string showing all correctly guessed letters, or '-'
 * for unguessed entries.
 * 
 * After the final guess, server sends "WIN WORD" or "LOSS WORD",
 * depending on if the client won or lost, where WORD is the 
 * target word.  The client replies with "YES" or "NO", indicating
 * if another game should be played.  If NO, the connection is 
 * terminated.
 * 
 * @author jlepak
 */
public class HangmanServer {
    public static final int PORT = 8889;
    
    // Flag to indicate unguessed letter in word.
    public static final char MISSING = '-';
    // Maximum allowed missed guesses.
    public static final int MISSES = 5;
    
    /** Message types for both server and client */
    public static enum ID {
        GAMEOVER, CORRECT, WRONG, WIN, LOSS, YES, NO
    }
    
    /**
     * Fill in correct slots and return number of newly correct entries.
     * @param target
     * @param guess
     * @param letters
     * @return 
     */
    public static int fillGuess(String target, char guess, char[] letters) {
        int correct = 0;
        for (int i = 0; i < target.length(); i++)
            if (target.charAt(i) == guess) {
                letters[i] = guess;
                correct++;
            }
        return correct;        
    }
    
    // Count up unguessed letters.
    public static int countMissing(char[] letters) {
        int count = 0;
        for (char c : letters)
            if (c == MISSING)
                count++;
        return count;
    }
    
    /** Handles interaction with client.
     */
    public static class ClientHandler extends SimpleComm implements Runnable {
        
        public ClientHandler(Socket client) throws IOException {
            super(client);
        }

        /**
         * Repeatedly play game as long as client requests another.
         */
        public void run() {
            try {
                while (runGame()) {}
                close();
            } catch (IOException ex) {
                System.err.println("IOException caught.  Exiting.");
                System.exit(1);
            }
        }

        /**
         * Run a single iteration of the game.
         * @return true if another games was requested
         * @throws IOException 
         */
        private boolean runGame() throws IOException {
            // Initialize word and game state.
            String word = "ytterbium";
            int missesLeft = MISSES;
            boolean gameOver = false;
            char[] letters = new char[word.length()];
            Arrays.fill(letters, MISSING);
            
            // Start of communication.
            send(word.length());
            
            // Repeatedly recieve guess and send reply.
            while (!gameOver) {
                char letter = read().charAt(0);
                int correct = fillGuess(word, letter, letters);
                if (correct == 0)
                    missesLeft--;
                if (missesLeft < 0 || countMissing(letters) == 0) {
                    gameOver = true;
                    send(ID.GAMEOVER);
                } else if (correct > 0)
                    send(ID.CORRECT, new String(letters));
                else
                    send(ID.WRONG, missesLeft);
            }

            // Send message to indicate final status.
            if (countMissing(letters) == 0)
                send(ID.WIN, word);
            else
                send(ID.LOSS, word);
            
            // Receive reply indicating if new game should start.
            return ID.valueOf(read()) == ID.YES;
        }
    }
    
    public static void main(String[] args) throws IOException {
        // Set up server.  localhost is the host name of the loopback
        // interface, which you use to communicate with other processes 
        // only on your own computer.
        String addr = "localhost"; 
        ServerSocket server = new ServerSocket(PORT, 0, 
                InetAddress.getByName(addr));
        System.out.format("HangmanServer running on %s:%s\n", 
                addr, PORT);
        
        Socket client = server.accept();
        new ClientHandler(client).run();
        server.close();
    }
}
/*
 * 
 */
package org.ilzd.netexamples;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * Provide simple methods for communication across a socket.
 * @author jlepak
 */
public class SimpleComm {

    private Socket socket;
    private BufferedReader reader;
    private PrintWriter writer;

    public SimpleComm(Socket sock) throws IOException {
        socket = sock;
        writer = new PrintWriter(socket.getOutputStream(), true);
        reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
    }

    /**
     * Send all parts of message to client, separated by spaces.
     *
     * @param message
     */
    protected void send(Object... message) {
        // Note: the (Object... message) notation allows you to pass
        // a variable number of arguments into this message.  The 
        // arguments are all collected into the variable called message, 
        // of type Object[].
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < message.length; ++i) {
            builder.append(message[i]);
            if (i < message.length - 1) {
                builder.append(" ");
            }
        }
        writer.println(builder.toString());
    }

    /**
     * Read a line from client.
     *
     * @return The line read
     * @throws IOException
     */
    protected String read() throws IOException {
        return reader.readLine();
    }
    
    /**
     * Close socket.
     * @throws IOException 
     */
    protected void close() throws IOException {
        reader.close();
        writer.close();
        socket.close();
    }
}

The simplest way to allow the server to handle multiple clients at once is to use threads to essentially run a separate server for every client that connects. This is a reasonable solution when we're sure that not too many clients will ever try to connect at once.

/*
 * 
 */
package org.ilzd.netexamples;

import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Simple multi-threaded server that spawns a new thread for every connection.
 * @author jlepak
 */
public class HangmanServerMulti {
    public static void main(String[] args) throws IOException {
        // Set up server.  localhost is the host name of the loopback
        // interface, which you use to communicate with other processes 
        // only on your own computer.
        String addr = "localhost"; 
        ServerSocket server = new ServerSocket(HangmanServer.PORT, 0, 
                InetAddress.getByName(addr));
        System.out.format("HangmanServer running on %s:%s\n", 
                addr, HangmanServer.PORT);
        
        // Spawn a new thread for every client that connects.
        while (true) {
            Socket client = server.accept();
            // The constructor argument is an instance of Runnable that
            // defines what the new thread executes.  In this case, it's 
            // a completely separate ClientHandler (essentially a whole
            // new server) for each connection.
            Thread t = new Thread(new HangmanServer.ClientHandler(client));
            t.start();
        }
    }
}