These last two classes are not part of this applet. Rather, they must be installed and run separately on the web server that the applet classes are to be loaded from. This will require the security rights to install and run so-called daemon processes on the web site, which not many people have. Fortunately, most users of this game will not be setting up their own servers; more likely, they will just play games connected to existing ones.
Server.java
Server is the main class for the server side of Scrabblet. Once this is installed on the web server, you have to run it using the command-line Java interpreter for that system, as shown here:
C:\java\Scrabblet> java Server
Once running, Server will respond with the following message:
Server listening on port 6564
The Server class starts out by declaring a few variables. The port has to be the same number, 6564, as we saw in ServerConnection. The idcon Hashtable is used to store all of the connections to all of the clients. We use a hash table rather than an array to manage frequent insertion and deletion, which require lots of array copying. The id is incremented for each new connection. This corresponds to the id instance variable we saw earlier in the client.
import java.net.*;
import java.io.*;
import java.util.*;
public class Server implements Runnable {
private int port = 6564;
private Hashtable idcon = new Hashtable();
private int id = 0;
static final String CRLF = "\r\n";
addConnection( )
The addConnection( ) method is called every time a new client connects to our applet. This method creates a new instance of ClientConnection, described next, to manage the client. It passes in a reference to this Server, the socket the client connected with, and the current value of id. Finally, it increments the id to have it ready for the next connection.
synchronized void addConnection(Socket s) {
ClientConnection con = new ClientConnection(this, s, id);
// we will wait for the ClientConnection to do a clean
// handshake setting up its "name" before calling
// set() below, which makes this connection "live."
id++;
}
set( )
The set( ) method is called from ClientConnection in response to the client telling us its “name.” set( ) tracks all of the connections in the idcon hash table, and first it removes this id from the table so that it won’t get duplicates if the client sends its name twice. The method calls setBusy(false) to signify that this connection is available to play a game. Then it walks through all of the other connections by enumerating the keys of the idcon hash table. For all nonbusy connections (those players waiting for an opponent), set( ) sends an “add” protocol message so they will all know about this connection.
synchronized void set(String the_id, ClientConnection con) {
idcon.remove(the_id) ; // make sure we're not in there twice.
con.setBusy(false);
// tell this one about the other clients.
Enumeration e = idcon.keys();
while (e.hasMoreElements()) {
String id = (String)e.nextElement();
ClientConnection other = (ClientConnection) idcon.get(id);
if (!other.isBusy())
con.write("add " + other + CRLF);
}
idcon.put(the_id, con);
broadcast(the_id, "add " + con);
}
sendto( )
sendto( ) is called in response to a “to” protocol message. It writes whatever is in the body string directly to the connection identified by dest.
synchronized void sendto(String dest, String body) {
ClientConnection con = (ClientConnection)idcon.get(dest);
if (con != null) {
con.write(body + CRLF);
}
}
broadcast( )
The broadcast( ) method is used to send a single message, in body, to every single connection except the one identified in exclude (typically, the sender).
synchronized void broadcast(String exclude, String body) {
Enumeration e = idcon.keys();
while (e.hasMoreElements()) {
String id = (String)e.nextElement();
if (!exclude.equals(id)) {
ClientConnection con = (ClientConnection) idcon.get(id);
con.write(body + CRLF);
}
}
}
delete( )
The delete( ) method is used to tell all of the connected clients to forget they ever heard of the_id. This is used by clients that are engaged in a game to remove themselves from other players’ eligibility lists.
synchronized void delete(String the_id) {
broadcast(the_id, "delete " + the_id);
}
kill( )
The kill( ) method is called whenever a client explicitly quits, sending the “quit” message, or when a client simply dies if the browser quits.
synchronized void kill(ClientConnection c) {
if (idcon.remove(c.getId()) == c) {
delete(c.getId());
}
}
run( )
The run( ) method is the main loop of the server. It creates a new socket on port 6564
and goes into an infinite loop accepting socket connections from clients. It calls
addConnection( ) with each socket that it accepts.
public void run() {
try {
ServerSocket acceptSocket = new ServerSocket(port);
System.out.println("Server listening on port " + port);
while (true) {
Socket s = acceptSocket.accept();
addConnection(s);
}
} catch (IOException e) {
System.out.println("accept loop IOException: " + e);
}
}
main( )
main( ) is, of course, the method run by the Java command-line interpreter. It creates a new instance of Server and launches a new Thread to run it.
public static void main(String args[]) {
new Thread(new Server()).start();
try {
Thread.currentThread().join();
} catch (InterruptedException e) { }
}
}
ClientConnection.java
This class is the mirror image of ServerConnection in the applet. One of these is created for each client. Its job is to manage all of the I/O to and from a client. The private instance variables hold all of the states about this client. The Socket is stored in sock. The buffered reader and output streams are stored in in and out. The host name of the client machine is kept in host. A reference to the Server instance that created this client is held in server. The name of the player on this client is stored in name, while the player’s automatically assigned ID number is held in id. The busy Boolean variable stores whether or not this client is actively engaged in a game.
import java.net.*;
import java.io.*;
import java.util.*;
class ClientConnection implements Runnable {
private Socket sock;
private BufferedReader in;
private OutputStream out;
private String host;
private Server server;
private static final String CRLF = "\r\n";
private String name = null; // for humans
private String id;
private boolean busy = false;
ClientConnection( )
The constructor saves the reference to the server and socket and remembers the unique ID. We wrap an InputStreamReader and a BufferedReader around the input so that it can call readLine( ) on it. Then it writes the id back to the client to let it know what number it is. Finally, it creates and starts a new Thread to handle this connection.
public ClientConnection(Server srv, Socket s, int i) {
try {
server = srv;
sock = s;
in = new BufferedReader(new
InputStreamReader(s.getInputStream()));
out = s.getOutputStream();
host = s.getInetAddress().getHostName();
id = "" + i;
// tell the new one who it is...
write("id " + id + CRLF);
new Thread(this).start();
} catch (IOException e) {
System.out.println("failed ClientConnection " + e);
}
}
toString( )
We override toString( ) so that we can have a clean representation of this connection for logging.
public String toString() {
return id + " " + host + " " + name;
}
getHost( ), getId( ), isBusy( ), and setBusy( )
We wrap host, id, and busy in public methods to allow read-only access.
public String getHost() {
return host;
}
public String getId() {
return id;
}
public boolean isBusy() {
return busy;
}
public void setBusy(boolean b) {
busy = b;
}
close( )
The close( ) method is called if the client explicitly quits or if we get an exception reading from the socket. We call kill( ) in the server, which removes us from any lists. Then we close the socket, which also closes both the input and output streams.
public void close() {
server.kill(this);
try {
sock.close(); // closes in and out too.
} catch (IOException e) { }
}
write( )
To write a string to a stream, we have to convert it to an array of bytes, using getBytes( ).
public void write(String s) {
byte buf[];
buf = s.getBytes();
try {
out.write(buf, 0, buf.length);
} catch (IOException e) {
close();
}
}
readline( )
The readline( ) method merely converts the IOException from readLine( ) into a null return value.
private String readline() {
try {
return in.readLine();
} catch (IOException e) {
return null;
}
}
Keywords
This section is very similar to the same part of the ServerConnection class, which represents the other end of the wire. The static variables and static block shown here are used to initialize the keys Hashtable with a mapping between the strings in keystrings and their position in the array—for example, keys.get(“quit”) == QUIT The lookup( ) method takes care of unpacking the Integer objects into the right int, with –1 meaning the keyword was not found.
static private final int NAME = 1;
static private final int QUIT = 2;
static private final int TO = 3;
static private final int DELETE = 4;
static private Hashtable keys = new Hashtable();
static private String keystrings[] = {
"", "name", "quit", "to", "delete"
};
static {
for (int i = 0; i < keystrings.length; i++)
keys.put(keystrings[i], new Integer(i));
}
private int lookup(String s) {
Integer i = (Integer) keys.get(s);
return i == null ? -1 : i.intValue();
}
run( )
run( ) has the loop that manages all of the communication with this client. It uses a StringTokenizer to parse the input lines, keying off of the first word in each line. The lookup( ) method just shown is used to look up these first words in the keys hash table. We then switch, based on the integer value of the keyword. The NAME message comes from clients when they first gain a human identity. We call set( ) in the server to get this connection set up. The QUIT message is sent when the client wants to end its server session. The TO message contains a destination ID and a message body to be sent to that client. We call sendto( ) in the server to pass the message along. The last message is DELETE, which is sent by clients that want to continue being connected but no longer want to have their names listed as available to play. run( ) sets the busy flag and calls delete( ) in the server, which notifies the clients that we don’t want to be called.
public void run() {
String s;
StringTokenizer st;
while ((s = readline()) != null) {
st = new StringTokenizer(s);
String keyword = st.nextToken();
switch (lookup(keyword)) {
default:
System.out.println("bogus keyword: " + keyword + "\r");
break;
case NAME:
name = st.nextToken() +
(st.hasMoreTokens() ? " " + st.nextToken(CRLF) : "");
System.out.println("[" + new Date() + "] " + this + "\r");
server.set(id, this);
break;
case QUIT:
close();
return;
case TO:
String dest = st.nextToken();
String body = st.nextToken(CRLF);
server.sendto(dest, body);
break;
case DELETE:
busy = true;
server.delete(id);
break;
}
}
close();
}
}
Enhancing Scrabblet
This applet represents a complete client/server, multiplayer board game. In the future, the code in Server and ServerConnection could be extended in many ways. It could be used to support other turn-based games. It could track and maintain a high-score list for each game. It could be dynamically extensible to understand new protocol verbs. One such example for the game described in this chapter would be to have a lookup function that checked a series of submitted words against a dictionary stored on the server. The server could then be the arbiter for such disputes as whether xyzy is a valid word. You could also construct a word robot, which would reside on the server but act like another player and use the dictionary to generate the best word placement from its current set of seven letters. It could even use a list of pithy quotes to throw into the chat window after each move. You might want to try making some of these enhancements yourself. This applet is intended for entertainment and educational purposes. Any similarity to any and all commercial products is merely coincidental.