HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

I had conceived of an Internet server for the Chinese game of MahJong a long time ago, but had never gotten very far trying to implement it in C and Xlib. In January 1996, I learned of Java, and decided to start anew. Java's unique capabilities: multithreading, portability, and AWT just to name a few, proved to be extremely useful for such a project. In three weeks, I got the alpha version running, and soon I had dozens of people playing on my site.

Just like okbridge and the chess servers, my application provides a virtual casino for MahJong fans on the Internet. It actually consists of two parts: a server and a client. The server is a Java application that keeps running twenty-four hours a day. The client is a Java applet that runs under a user's webbrowser. A web page on the server's host machine gives access to the client applet and provides documentation and other information to the user. In this article, I will explain the design and implementation of both the server and the client. There's nothing special about MahJong; the same principles can be applied to many other multi-user Internet applications.

Packets and Client/Server Communication
The nature of a client/server application requires data packets to be sent between the clients and the server. In a typical C implementation (e.g. in Netrek), we define several types of packets, each corresponding to a C structure, and use a byte tag to distinguish different packets apart. To send a packet through a socket connection, we simply coarse the structure into an array of bytes, and reverse the process when we want to read it back in. However, this approach is not possible in a strongly typed language like Java, because we cannot perform pointer casting. Therefore we must first get this technicality out of our way.

My approach is only a slight deviation from what we would normally do in C. Each type of packet is defined to be a class that extends a base abstract class Packet. Its field variables contain information the packet carries. We need two constructors: one takes as its arguments values for the field variables, the other a DataInputStream from which the packet should be read in. We also need a method that converts the packet into a binary form that can be sent out to a DataOutputStream and read back in by the constructor mentioned above.

I chose to use DataInputStream and DataOutputStream because they offer the ability to send binary data which reduces network traffic, and they also provide a pretty complete set of methods that deals with many commonly used data types like integers and strings. It is still tedious to call those methods for all the field variables for all the packet classes, but I see no easy way around it. It is possible, however, to speed up sending the same packet to multiple clients by using a format() method to write the binary data into a byte array first.

One question remains: how do we discern all the packets being sent to us, and how do we act accordingly? The answer is a packet dispatcher along with handlers for all the packets. The dispatcher is a thread that does an infinite loop reading in a single byte from the input stream. This byte would be a tag that marks which packet follows it. Then we can use a switch()statement to call the constructor of the actual packet to read it in, and call an appropriate method that handles that packet. There are also more elegant solutions, but this approach works well for our purposes.

Listing 1 shows some packet examples. We define here a packet that carries a player's identity and a response from the server.

The Client
Now we are ready to turn to writing the client. This is a program that interacts with both the user and the server, but actually it is the easy part of the whole application. Here I will only give an outline.

Unlike most Java applets, the MahJong client needs to live in its own window (Frame in AWT parlence). There are two reasons for this. First, a user is likely to spend a lot of time playing the game with other people, so we want to give him the ability to browse other web sites without interrupting his game. Second, the client needs to display a lot of stuff, including lists of all the players and tables, a communication panel and a big panel that draws all the tiles on his table. Therefore, we are going to need all the screen real estate we can get. Thus we are led to defining two classes: a Greeting class that extends java.applet.Applet and a Client class that extends java.awt.Frame. Since it's the Client class that does all the real work, we should also let it implement Runnable. See Listing 2 for a sketch of Greeting and Client.

The class Greeting simply presents a button and waits for input. When a user wants to connect to the server, he presses the button, and Greeting tries to open a socket connection to the server. We get the host name of the server from an applet parameter embedded in the <applet> tag in our main web page. Upon successful connection, Greeting creates a new instance of Client, and we are in business.

The Client object first spawns a thread from its run() method. The thread is none other than a packet dispatcher that reads in packets from the server and calls the appropriate handler methods in Client. Then we can open our new window (recall that Client extends Frame) with the complete GUI in it. In the sample code, I present the simplest possible login window. A real working client will likely have a lot more GUI elements and other classes.

There is a convention I want to recommend, that all user actions should only cause packets to be sent to the server for processing (possibly with some minimal error-checking on the client side). Only the packet dispatcher thread that reads server packets can update the status variables in the client. For example, although the client keeps a local copy of all the players and which tables they are on, when the user requests to join a table, the client only sends a join-table packet to the server. The server then processes this request and sends back a join-table packet to the client if the request is granted, and only then does the client change its internal data and graphic display to match the action. This convention vastly reduces the care that needs to be taken for client/server synchronization, and also makes future changes in the client/server protocol less likely to mess things up.

The Basic Server
The server is the central piece that ties everything together, so it's expected to be a very complicated program. However, we do have a lot of technical means at our disposal, notably Java's built-in multithreading capability. To begin with, we will first sketch an outline: the classes we will define, and what methods we need to operate on them.

First of all, we need a Server class, which will have a main() method to start the whole thing. It first creates a ServerSocket and listens on a predefined port number. Each time it accepts a connection, it will create a new instance of a Player class.

The Player class stores all the information about a particular player, like his socket connection, character name, total score, table, etc. In particular, we need a DataInputStream and a DataOutputStream, which we can figure out from the socket connection. To receive packets from the player's client, the Player class should extend Thread, have a run() method that is a packet dispatcher, and implement a bunch of packet handler methods. Let's call this thread the player's input thread.

We will also need to send server packets to each player's client, so the Player class should have an output (Packet p) method. Let's think for a moment what this method should do. The naive implementation is to just take the Packet p that we want to send out, and call its output() method with our DataOutputStream. But which threads will be calling Player. output()? And what might happen during the output process? Let's imagine that one player, A, wants to send a text message to a player B (this is actually not allowed on the MahJong server to prevent fraud, but there are many other similar circumstances). So player A's input thread reads in a text message packet and finds out it's intended for player B, and it proceeds to call B's output() method with a server-side text message packet. Thus, in effect, any player's input thread could call any other player's output thread. However, this may cause a problem, because as we know, writing data into a socket can take a long time to finish, or may not finish at all. Thus A's input thread may be blocked indefinitely trying to send packets to B.

My solution is to introduce another class per Player, namely PlayerOutput, that also extends Thread. Let's call it the output thread. Each output thread maintains a queue of preformatted (i.e. converted to byte[] by the format() method) packets to be sent out. The output thread simply waits for a packet to appear on its queue, pops it off the queue, sends it out, and repeats this process. Now, each Player's output() method simply pushes the packet onto the associated PlayerOutput's queue. Writing the Queue class is a simple exercise using linked lists and monitors. We present a version in Listing 3.

The MahJong server provides a bunch of MahJong tables, each table supporting four players and several spectators. We can make a Table class to encapsulate all the table-related data and methods. A typical method of a Table object is to handle some game play made by one of the players on that table. To simplify things, I chose not to implement Table as a Thread. Instead, it's the various Player input threads that read in game-play packets and call the appropriate Table methods to handle them. To make this plan work, the various Table methods should be declared synchronized, and take only a short time to execute. This last requirement is easily satisfied for games like MahJong, bridge, or chess, where the game status can change only when some player makes an action, and the exact effect of a play can be easily figured out. If you are writing a real-time game where the game status changes independently of player actions, you will need to use a dedicated thread that updates the game status periodically. Now we basically have a skeleton of the server. We show it in Listing 4.

Making a Robust Application
We will now explore some more subtle issues. They mostly have to do with synchronizations and dead socket connections. If we don't take care of them, we won't be able to run our application on the Internet for an hour before either the server or the clients crash!

Here's the first problem. In the server, we keep a list of all the players connected to our server. This is simply an array (or Hashtable) of Player objects. Now, when some player joins or leaves our server, we need to update this global list. We also need to occasionally send all the players' names to some player, or perhaps send a particular packet to everyone. Theoretically, these operations may happen at the same time in many threads. There is a similar situation for the list of all open MahJong tables.

The reader may recognize this as the classical "reader-writer" problem: we have a document (the list of all players), many readers (threads that try to send packets to everyone or query everyone's name,) and many writers (threads that handle players joining or leaving the server). An arbitrary number of readers are allowed to read at the same time, but reading and writing are mutually exclusive, and all the writers are mutually exclusive. It's also a good idea to give writers higher priority than readers.

We solve this problem with a class RWMonitor, that solves the "reader-writer" problem in general. The code is in Listing 5. To use it on the player list, for example, we need to create a new RWMonitor object first, call it rw_mon. Whenever we need read (or write) access to the player list, we first call rw_mon.inRead() (or rw_mon.inWrite()) to enter a critical region. Then we can proceed, but remember never to modify the player list in a read-only critical region! After we are done, we simply call rw_mon.outRead() (or rw_mon.outWrite()) to exit the critical region. Listing 4 shows some typical usage of the RWMonitor class.

Granted, our MahJong server spends most of its time waiting for packets to arrive, but race conditions could occur, and it almost certainly will if the server is to run for an extended period of time. It's never a good idea to ignore possible synchronization problems, however unlikely they are to happen.

The other problem I want to mention has to do with dead socket connections. As we know, the Internet is far from being robust. Routers probably go down at a rate faster than car accidents occur. Now it would be all the same if Java's socket library could tell us that some link is dead as soon as it happens, but unfortunately it doesn't all the time. On the alpha version of my server some dead connections simply didn't get reported to my program. No exceptions. The read and write calls simply hang there. And in Java, there is currently no way to specify a time-out on read and write calls.

There is a solution using so-called ping packets. These are packets that are sent from the client (by a dedicated thread) regularly, say at intervals of thirty seconds. When the Player object on the server side receives any packet, it always increases a ping counter. This way, even if the user is idle, his ping counter always increases at least once every thirty seconds. On the server side, we make another Thread, called WatchDog, that is usually sleeping but wakes up once every minute or so, to check the ping counters of all the players on the server. If some player's ping counter is zero, that means the connection between the server and the client must be dead, so our WatchDog proceeds to clear this player's slot. In any case, WatchDog will reset all players' ping counters to zero before it goes to sleep again. We won't go into more details here, as the implementation of such a method depends on the actual client/server protocol. Sample code can be found in my MahJong Server distribution.

This method has the added bonus of being able to gauge a user's idle time. If a player is on a table playing a game, and the ping counter reaches two before we receive any game-play packet from his client, we know that the player is probably answering the phone or caught by his boss, and we can choose to kick him off the table to let someone else on.

The last advice I want to give may sound like a cliche. That is, always follow the practices of object-oriented programming! Do not save on classes. Put methods where they belong. In my beta version I had a persisting dead-lock problem. Eventually I tracked it down to a synchronized method that really belonged to another class, but I had put it in the wrong place in order to save myself some typing!

I hope my article can help you write your own Internet game. The source code listing is brief but self-contained and runs as is, so it can be used as a starting point. Happy Java programming!

The MahJong server: has kept moving for various reasons. When I do find a permanent home for it, I'll put a link in my personal home page. The source code is available under GNU Public License (GPL) at "http://ftp.princeton.edu/pub/MJ_dist.tar.gz".

About the Author
Thomas Z. Feng is a Ph.D. student in the Mathematics Department at Princeton University. He is working on Algebraic Geometry, and loves to write programs for fun. His personal home page is: "http://www.math.princeton.edu/~ztfeng/".

	

Listing 1:  Packet Examples

// File Packet.java
import java.io.*;

public abstract class Packet {
  // client -> server packet tags
  public static final byte CP_LOGIN = 0;
  // server -> client packet tags
  public static final byte SP_LOGIN = 0;

  // abstract method to send the packet in binary form
  public abstract void output (DataOutputStream S) throws IOException;

  // write the packet into a byte-array buffer
  public byte[] format () throws IOException {
    ByteArrayOutputStream buf = new ByteArrayOutputStream();
    DataOutputStream S = new DataOutputStream(buf);
    output(S);
    return buf.toByteArray();
  }
}

// File CPLogin.java
import java.io.*;

public class CPLogin extends Packet {
  public String name;

  public CPLogin (String n) {
    name = n;
  }

  public CPLogin (DataInputStream S) throws IOException {
    // we assume the tag is already read in
    name = S.readUTF();
  }
  public void output (DataOutputStream S) throws IOException {
    S.writeByte(CP_LOGIN);                 // always write the tag first
    S.writeUTF(name);
  }
}

// File SPLogin.java
import java.io.*;

public class SPLogin extends Packet {
  public static final byte OK = 0;
  public static final byte REJECTED = 1;
  public byte result;

  public SPLogin (byte r) {
    result = r;
  }
  public SPLogin (DataInputStream S) throws IOException {
    result = S.readByte();
  }
  public void output (DataOutputStream S) throws IOException {
    S.writeByte(SP_LOGIN);
    S.writeByte(result);
  }
}


Listing 2:  The Client

// File Greeting.java
import java.net.*;
import java.io.*;
import java.awt.*;

public class Greeting extends java.applet.Applet {
  Button main_button = new Button('Connect Now!');
  Client main = null;

  public void init () {
    add(main_button);
    show();
  }
  public void restart () {
    main = null;
    main_button.setLabel('Connect again');
    main_button.enable();
  }
  public boolean action (Event evt, Object what) {
    if (evt.target == main_button) {
      main_button.disable();
      main_button.setLabel('Connecting...');
      String server_host = getParameter('ServerHost');
      int server_port = 5555;  // default port number
      try {
        server_port = Integer.parseInt(getParameter('ServerPort'));
      } catch (NumberFormatException e) {
      }
      try {
        Socket sock = new Socket(server_host, server_port);
        DataInputStream in = new DataInputStream(sock.getInputStream());
        DataOutputStream out = new DataOutputStream(sock.getOutputStream());
        main_button.setLabel('Connected!');
        main = new Client(this, sock, in, out);
      } catch (Exception e) {
        restart();
      }
    }
    return true;
  }
}

// File Client.java
import java.awt.*;
import java.io.*;
import java.net.*;

class Client extends Frame implements Runnable {
  Socket sock;
  DataInputStream in;
  DataOutputStream out;
  Greeting greeter;
  Thread listener;
  TextField name_field;
  // other variables

  public Client (Greeting g, Socket s, DataInputStream i, DataOutputStream o) {
    sock = s;
    in = i;
    out = o;
    greeter = g;
    (listener = new Thread(this)).start();
    setTitle('Login');
    name_field = new TextField(20);
    setLayout(new FlowLayout());
    add(name_field);
    pack();
    show();
  }
  void output (Packet p) {
    try {
      p.output(out);
    } catch (IOException e) {
      disconnect();
      listener.stop();
      listener = null;
      dispose();
      greeter.restart();
    }
  }
  void disconnect () {
    try {
      in.close();
      out.close();
      sock.close();
    } catch (IOException e) {
    }
  }
  public boolean action (Event evt, Object what) {
    if (evt.target == name_field) {
      output(new CPLogin(name_field.getText()));
    }
    // handle other user interactions and send client->server packets
    return true;
  }
  public void run () {
    for (;;) {
      try {
        byte type = in.readByte();
        switch (type) {
        case Packet.SP_LOGIN:
          hdLogin(new SPLogin(in));
          break;
        // other packets
        }
      } catch (IOException e) {
        disconnect();
        listener = null;
        dispose();
        greeter.restart();
        return;
      }
    }
  }
  void hdLogin (SPLogin pkt) {
    if (pkt.result == SPLogin.OK) {
      System.out.println('We are logged in!');
      // we can proceed to start games now
    } else {
      System.out.println('Login rejected!');
    }
  }
  // other packet handlers
}

// File applet.html

<html>
<applet	CODE='Greeting.class'
 WIDTH=150
 HEIGHT=40>
<param	NAME='ServerHost'
 VALUE='localhost'>
<param	NAME='ServerPort'
 VALUE='5555'>
</applet>
</html>


Listing 3:  A Queue Class

// File Queue.java

class QueueLink {
  QueueLink next;
  Object obj;

  public QueueLink (Object o) {
    obj = o;
    next = null;
  }
}

public class Queue {
  private QueueLink head = null, tail = null;

  public synchronized Object pop () {
    while (head == null) {
      try {
 wait();
      } catch (InterruptedException e) {
      }
    }
    Object o = head.obj;
    head = head.next;
    if (head == null) {
      tail = null;
    }
    return o;
  }

  public synchronized void push (Object o) {
    if (head == null) {
      head = tail = new QueueLink(o);
      notify();
    } else {
      tail.next = new QueueLink(o);
      tail = tail.next;
    }
  }
}


Listing 4: The Server

// File Server.java
import java.io.*;
import java.net.*;

class Server {
  public static void main (String argv[]) {
    ServerSocket main_sock = null;
    try {
      main_sock = new ServerSocket(5555);  // default port to listen on
    } catch (IOException e) {
      System.out.println(e.toString());
      System.exit(1);
    }
    // other initializations
    for (;;) {
      try {
        Socket s = main_sock.accept();
        DataInputStream in = new DataInputStream(sock.getInputStream());
        DataOutputStream out = new DataOutputStream(sock.getOutputStream());
        new Player(s, in, out).start();
      } catch (IOException e) {
        System.out.println(e.toString());
      }
    }
  }
}

// File Player.java
import java.io.*;
import java.net.*;
import java.util.Hashtable;

class Player extends Thread {
  static Hashtable list = new Hashtable();
  static RWMonitor rw_mon = new RWMonitor();
  Socket sock;
  DataInputStream in;
  DataOutputStream out;
  PlayerOutput out_thread;
  String name = null;
  // other variables

  public Player (Socket s, DataInputStream i, DataOutputStream o) {
    sock = s;
    in = i;
    out = o;
    (out_thread = new PlayerOutput(this, out)).start();
    // other initialization stuff
  }
  public void output (Packet p) {
    try {
      out_thread.output(p.format());
    } catch (IOException e) {
    }
  }
  public void disconnect () {
    try {
      in.close();
      out.close();
      sock.close();
    } catch (IOException e) {
    }
    if (name != null) {
      rw_mon.inWrite();  // enter critical region
        list.remove(this);
      rw_mon.outWrite();  // leave critical region
    }
    // other clean up stuff
  }
  public void run () {
    for (;;) {
      try {
        byte type = in.readByte();
        switch (type) {
        case Packet.CP_LOGIN:
          hdLogin(new CPLogin(in));
          break;
        // other packets
        default:  // unrecognized packet
          disconnect();
          out_thread.stop();
          return;
        }
      } catch (IOException e) {
        disconnect();
        out_thread.stop();
        return;
      }
    }
  }
  void hdLogin (CPLogin pkt) {
    if (name != null) {  // can't login twice
      output(new SPLogin(SPLogin.REJECTED));
      return;
    }
    rw_mon.inWrite();  // enter critical region    
    if (list.get(pkt.name) == null) {  // check if he's already on the server
      name = pkt.name;
      list.put(name, this);
      output(new SPLogin(SPLogin.OK));
    } else {
      output(new SPLogin(SPLogin.REJECTED));
    }
    rw_mon.outWrite();  // leave critical region
    // other stuff to do after successful login
  }
  // other packet handlers
}

// File PlayerOutput.java
import java.io.*;

class PlayerOutput extends Thread {
  private static Player the_player;
  private DataOutputStream out;
  private Queue pkt_queue = new Queue();

  public PlayerOutput (Player p, DataOutputStream S) {
    the_player = p;
    out = S;
  }
  public void output (byte p[]) {
    pkt_queue.push(p);
  }
  public void run () {
 loop:
    while (true) {
      byte p[] = (byte[])pkt_queue.pop();
      try {
 if (p.length != 0) {
   out.write(p, 0, p.length);
 }
      } catch (IOException e) {
 break loop;
      }
    }
    the_player.disconnect();
    the_player.stop();
  }
}


Listing 5:  Reader-Writer Problem

// File RWMonitor.java

class Mutex {
  public synchronized void mywait () throws InterruptedException {
    wait();
  }
  public synchronized void mynotify () {
    notify();
  }
  public synchronized void mynotifyAll () {
    notifyAll();
  }
}

public class RWMonitor {
  private int n_readers = 0;
  private int n_writers = 0;
  private int n_queued = 0;
  private Mutex reader_mon = new Mutex();
  private Mutex writer_mon = new Mutex();

  private synchronized boolean readable () {
    if (n_writers > 0) {
      return false;
    }
    n_readers++;
    return true;
  }
  private synchronized boolean writable () {
    n_writers++;
    if (n_writers > 1) {
      return false;
    }
    if (n_readers > 0) {
      return false;
    }
    return true;
  }
  public void inRead () {
    while (!readable()) {
      try {
 reader_mon.mywait();
      } catch (InterruptedException e) {
      }
    }
  }
  public synchronized void outRead () {
    if (n_readers == 0) {
      if (n_writers > 0) {
 writer_mon.mynotify();
      }
    }
  }
  public void inWrite () {
    if (!writable()) {
      try {
 writer_mon.mywait();
      } catch (InterruptedException e) {
      }
    }
  }
  public synchronized void outWrite () {
    if (n_writers > 0) {
      writer_mon.mynotify();
    } else {
      reader_mon.mynotifyAll();
    }
  }
}

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.