16 Apr 2012 [ 201 week14 ]

We'll continue our discussion of I/O in Java by moving on to custom-built networking. The I/O hierarchy we saw before will still play a major role.

We'll see a couple new issues that pop up when communicating with other processes (which might be running on other computers). The first is making sure our output is flushed before closing any connection. We'll use the PrintWriter class to make sure of that. The second new issue (which we'll ignore like other important but tricky bits) is that of text encoding -- when we transmit text to another computer, we need to make sure that both computers interpret the text the same way. See this discussion for more information about how we could properly set up our PrintWriters to do that.

Here's a simple server that we'll start with that accepts a single connection, receives a question, and replies with an answer:

package org.ilzd.netexamples;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
 * Simple server that listens for a question and provides an answer.
 * What's missing: error handling, and a well-defined protocol.
 *
 * @author jlepak
 */
public class QAServer {
    public static final int PORT = 8888;
    
    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 = "10.38.159.59"; 
        ServerSocket server = new ServerSocket(PORT, 0, 
                InetAddress.getByName(addr));
        System.out.format("QAServer running on %s:%s\n", 
                addr, PORT);
        
        // Read input line from client.
        while (true) {
            Socket client = null;
            client = server.accept();
            InputStream clientInput = client.getInputStream();
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(clientInput));
            String line = reader.readLine();
            System.out.println(line);

            // Send output line to client.
            OutputStream clientOutput = client.getOutputStream();
            // Second argument indicates that all println() calls 
            // should flush output.
            PrintWriter writer = new PrintWriter(clientOutput, true);
            Scanner answerScanner = new Scanner(System.in);
            writer.println(answerScanner.nextLine());

            reader.close();
            writer.close();
            client.close();
        }
        // server.close();
    }
}

Here's the corresponding client that connects to the server, sends a question, and waits for an answer.

package org.ilzd.netexamples;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

/**
 * Simple client for the QAServer.
 * What's missing: error handling and a well-defined protocol.
 * 
 * @author jlepak
 */
public class QAClient {
    
    public static void main(String[] args) throws IOException {
        // Connect to server.
        Socket server = new Socket("10.38.159.59", QAServer.PORT);
        
        // Send question.
        OutputStream questionStream = server.getOutputStream();
        PrintWriter writer = new PrintWriter(questionStream, true);
        Scanner questionScanner = new Scanner(System.in);
        writer.println(questionScanner.nextLine());
        
        // Receive answer.
        InputStream answerStream = server.getInputStream();
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(answerStream));
        String answer = reader.readLine();
        System.out.println(answer);
        
        reader.close();
        writer.close();
        server.close();
    }
}

Hangman, again

We're going to work out a protocol for playing hangman against a partner in class (as long as the lab firewall allows it).

Protocol just means an agreement on how to communicate; such an agreement needs to describe exactly what kinds of messages are allowed and how to respond to them.

It's easiest to see how protocols work with an example. This example shows how SMTP (the protocol used to transmit email) works.

Here's a starting point:

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.*;
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();
    }
}