Here's a final version of our our Hangman client and server. I did several things to clean up the code:
- extended our protocol to allow for multiple games
- created a
ClientHandler
class that handles a single client (this becomes useful when we want to handle multiple clients at once) - cleaned up the logic to centralize all the message types (see the
ID
enum). - moved common code to the
SimpleComm
class that provides simpleread
andsend
methods.
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();
}
}
}