The Game Networking Process

Software engineers typically learn best from examples, so the rest of this chapter describes a simple example program. Although the actual functionality is simple—just a chat program—this program does everything a game would do to implement LAN networking.

Discovery

Discovery is the process of finding the other chat peers with whom we will connect. The functionality is basically the same as a buddy list. Unlike instant message services that typically have a central machine that keeps the list, LAN discovery must be done a priori and in a distributed manner.

UUIDs

To identify the individual programs, each one needs a unique identifier that no other program will have. Such an identifier is called a universally unique identifier (UUID)2. The first class needed for LANChat is a UUID class. Java 1.5 is slated to have one built into the JDK APIs, but for now we must make one of our own. It must be able to be compared with other UUIDs, and it must be communicable across the Internet. The easiest way to read and write Java objects is with object serialization, so it should be serializable. An interface that defines what we need follows:

package com.worldwizards.shawnbook; <import java.io.Serializable; public interface UUID extends Serializable, Comparable{ }

There are two kinds of UUID. A truly unique UUID is one that will never conflict with any other UUID. To create a truly unique UUID you need a unique starting seed number. All Ethernet adapters have a unique ID built into them that Ethernet uses called a MAC address. Unfortunately, there is now no way to access the MAC address directly from Java. Therefore, to write a truly unique UUID class, it is necessary to write some native code to fetch the MAC address and a JNI wrapper to allow it to be called from Java. The resulting code would, of course, not be totally portable because the native code would have to be rewritten on every OS on which you wanted to run the program.

There is, however, a second kind of UUID—a statistically unique ID. A statistically unique ID is not 100 percent guaranteed to be unique, but the chance of a collision is so small as to be virtually impossible. The simplest way to construct a statistically unique UUID is with a combination of a large-bit random number and a time stamp. For two UUIDs to collide, they would have to generate the same random number at the exact same time. This type of UUID can be created completely in Java, so this is the kind used for LANChat.

/*****************************************************************************  * Copyright (c) 2003 Sun Microsystems, Inc.  All Rights Reserved.  * Redistribution and use in source and binary forms, with or without  * modification, are permitted provided that the following conditions are met:  *  * - Redistribution of source code must retain the above copyright notice, *   this list of conditions and the following disclaimer.  *  * - Redistribution in binary form must reproduce the above copyright notice,  *   this list of conditions and the following disclaimer in the documentation  *   and/or other materials provided with the distribution.  *  * Neither the name Sun Microsystems, Inc. or the names of the contributors  * may be used to endorse or promote products derived from this software  * without specific prior written permission.  *  * This software is provided "AS IS," without a warranty of any kind.  * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING  * ANY IMPLIED WARRANT OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR  * NON-INFRINGEMEN, ARE HEREBY EXCLUDED.  SUN MICROSYSTEMS, INC. ("SUN") AND  * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS  * A RESULT OF USING, MODIFYING OR DESTRIBUTING THIS SOFTWARE OR ITS   * DERIVATIVES.  IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST  * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,  * INCIDENTAL OR PUNITIVE DAMAGES.  HOWEVER CAUSED AND REGARDLESS OF THE THEORY  * OF LIABILITY, ARISING OUT OF THE USE OF OUR INABILITY TO USE THIS SOFTWARE,  * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.  *  * You acknowledge that this software is not designed or intended for us in  * the design, construction, operation or maintenance of any nuclear facility * *****************************************************************************/ 

package com.worldwizards.sbook; public class StatisticalUUID implements UUID{   transient static SecureRandom random=null;   private long randomValue;   private long timeValue;   public StatisticalUUID() {     if (random == null) {       try {         random = SecureRandom.getInstance("SHA1PRNG");       }       catch (NoSuchAlgorithmException ex) {         ex.printStackTrace();       }     }     randomValue = random.nextLong();     timeValue = System.currentTimeMillis();   }   public int compareTo(Object object) {     StatisticalUUID other  = (StatisticalUUID)object;     if (timeValue < other.timeValue) {       return -1;     } else if (timeValue:other.timeValue) {       return 1;     } else {       if (randomValue < other.randomValue) {         return -1;       } else if (randomValue > other.randomValue) {         return 1;       }     }     return 0;   }   public String toString(){     return ("UUID("+timeValue+":"+randomValue+")");   }   public int hashCode() {     return ((int)((timeValue::32)&0xFFFFFFFF))^            ((int)(timeValue&0xFFFFFFFF))^            ((int)((randomValue)::32)&0xFFFFFFFF)^            ((int)(randomValue&0xFFFFFFFF));   }   public boolean equals(Object obj) {     StatisticalUUID other = (StatisticalUUID)obj;     return ((timeValue==other.timeValue)&&             (randomValue == other.randomValue));   } } 

Discoverer

Now that there is a way of identifying the different runs of the program (sometimes called sessions), the runs can announce their presence on the Internet. An interface that defines the functionality of a Discoverer follows.

package com.worldwizards.sbook; import java.net.MulticastSocket; public interface Discoverer {   public void addListener(DiscoveryListener listener);   public UUID[] listSessions(); } 

To receive discovery events, an interface must be defined as follows:

package com.worldwizards.sbook; public interface DiscoveryListener {   public void sessionAdded(UUID sessionUUID);   public void sessionRemoved(UUID sessionUUID); } 

The simplest way to coordinate discovery is with multicast sockets. Every game program on the Internet subscribes to a single multicast channel. The hosts broadcast their presence over this channel to all the other players. By repeating that broadcast at regular intervals it becomes possible to tell when a game program leaves the chat by the absence of these updates. The code to implement this follows, starting with the MulticastDiscovery class:

****************************************************************************      * Copyright (c) 2003 Sun Microsystems, Inc.  All Rights Reserved.      * Redistribution and use in source and binary forms, with or without      * modification, are permitted provided that the following conditions are met:      *      * - Redistribution of source code must retain the above copyright notice,      *   this list of conditions and the following disclaimer.      *      * - Redistribution in binary form must reproduce the above copyright notice,      *   this list of conditions and the following disclaimer in the documentation      *   and/or other materials provided with the distribution.      *      * Neither the name Sun Microsystems, Inc. or the names of the contributors      * may be used to endorse or promote products derived from this software      * without specific prior written permission.      *      * This software is provided "AS IS," without a warranty of any kind.      * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING      * ANY IMPLIED WARRANT OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR      * NON-INFRINGEMEN, ARE HEREBY EXCLUDED.  SUN MICROSYSTEMS, INC. ("SUN") AND      * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS      * A RESULT OF USING, MODIFYING OR DESTRIBUTING THIS SOFTWARE OR ITS      * DERIVATIVES.  IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST      * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,      * INCIDENTAL OR PUNITIVE DAMAGES.  HOWEVER CAUSED AND REGARDLESS OF THE THEORY      * OF LIABILITY, ARISING OUT OF THE USE OF OUR INABILITY TO USE THIS SOFTWARE,      * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.      *      * You acknowledge that this software is not designed or intended for us in      * the design, construction, operation or maintenance of any nuclear facility      *      *****************************************************************************/ 

package com.worldwizards.sbook; import java.io.*; import java.net.*; import java.util.*; import java.util.Map.*; public class MulticastDiscovery     implements Discoverer, DatagramListener,     Runnable {   InetAddress discoveryGroup;   MulticastSocket discoverySocket;   List listeners = new ArrayList();   Map timeTracker = new HashMap();   UUID myUUID;   private boolean done = false;   private boolean announcer = false; // half a second btw them   private static final long HEARTBEAT_TIME = 500;    private DatagramPacket hbDatagram;   private static final int DISCOVERY_PORT = 8976;   public MulticastDiscovery() {     try {       discoveryGroup = InetAddress.getByName("228.9.8.7");       discoverySocket = new MulticastSocket(DISCOVERY_PORT);       discoverySocket.joinGroup(discoveryGroup); // just run on our local sub-net       discoverySocket.setTimeToLive(1);      }     catch (Exception ex) {       ex.printStackTrace();     }     myUUID = new StatisticalUUID();     // create heartbeat datagram     byte[] hbdata = null;     try {       ByteArrayOutputStream baos = new ByteArrayOutputStream();       ObjectOutputStream oos = new ObjectOutputStream(baos);       oos.writeObject(myUUID);       oos.flush();       hbdata = baos.toByteArray();       oos.close();       baos.close();     }     catch (Exception e) {       e.printStackTrace();     }     hbDatagram = new DatagramPacket(hbdata, hbdata.length,                                     discoveryGroup, DISCOVERY_PORT);     // create and start listen thread  on discovery socket     DatagramSocketHandler rdr = new DatagramSocketHandler(         discoverySocket, 1000);     rdr.addListener(this);     new Thread(rdr).start();     // start heartbeating     new Thread(this).start();   }   /**    * This method starts this discoverer announcing itself to the world.    */   public void startAnnouncing() {     announcer = true;   }   /**    * addListener    *    * <param listener DiscoveryListener    */   public void addListener(DiscoveryListener listener) {     listeners.add(listener);   }   /**    * listSessions    *    * <return UUID[]    */   public UUID[] listSessions() {     Set ids = timeTracker.keySet();     UUID[] ar = new UUID[ids.size()];     return (UUID[]) ids.toArray(ar);   }   /**    * run    * This method is a loop that runs on its own thread generating    * heartbeats.    */   public void run() {     while (!done) {       try {         if (announcer) {           discoverySocket.send(hbDatagram);         }         checkTimeStamps();         Thread.sleep(HEARTBEAT_TIME);       }       catch (Exception e) {         e.printStackTrace();       }     }   }   /**    * checkTimeStamps    */   private void checkTimeStamps() {     List removedIDs = new ArrayList();     // find all outdated peers     synchronized (timeTracker) {       for (Iterator i = timeTracker.entrySet().iterator();            i.hasNext(); ) {         Entry entry = (Entry) i.next();         long tstamp = ( (Long) entry.getValue()).longValue();         if ( (System.currentTimeMillis() - tstamp) >             (HEARTBEAT_TIME * 3)) {           // missed the alst 3 heartbeats. we think its gone           removedIDs.add(entry.getKey());         }       }     }     // now remove them     for (Iterator i = removedIDs.iterator(); i.hasNext(); ) {       UUID id = (UUID) i.next();       synchronized (timeTracker) {         timeTracker.remove(id);       }       for (Iterator i2 = listeners.iterator(); i2.hasNext(); ) {         ( (DiscoveryListener) i2.next()).sessionRemoved(id);       }     }   }   /**    * datagramReceived    *    * <param dgram DatagramPacket    */   public void datagramReceived(DatagramPacket dgram) {     try {       ByteArrayInputStream bais = new ByteArrayInputStream(dgram.           getData());       ObjectInputStream ois = new ObjectInputStream(bais);       UUID uuidIn = (UUID) ois.readObject();       ois.close();       bais.close();       boolean isNew = (timeTracker.get(uuidIn) == null);       synchronized (timeTracker) {         timeTracker.put(uuidIn, new              Long(System.currentTimeMillis()));       }       if (isNew) {         for (Iterator i = listeners.iterator(); i.hasNext(); ) {           ( (DiscoveryListener) i.next()).sessionAdded(dgram.               getAddress(),               uuidIn);         }       }     }     catch (Exception e) {       e.printStackTrace();     }   }   // test main   public static void main(String[] args) {     MulticastDiscovery mcd = new MulticastDiscovery();     mcd.addListener(new DiscoveryListener() {       public void sessionAdded(InetAddress address,                                UUID sessionUUID) {         System.out.println("Session joined with ID = " +                            sessionUUID);       }       public void sessionRemoved(UUID sessionUUID) {         System.out.println("Session left with ID = " + sessionUUID);       }     });     mcd.startAnnouncing();   } }

This is a pretty complex piece of code. It is easier to understand if it is broken down by individual methods. It starts with the following declaration:

public class MulticastDiscovery implements Discoverer, DatagramListener,     Runnable{ 

This code states that this class is going to be implementing three interfaces. We have already discussed Discoverer. DatagramListener is an interface for clients of another class that is necessary for this discovery—the DatagramSocketHandler. The DatagramSocketHandler will be responsible for actually reading packets from the UDP multicast socket, much the way SimpleSocketListener previously listened for strings coming in through a TCP/IP stream socket. Rather then printing the contents, however, whenever it gets a packet it will call back all its listeners and tell them. MulticastDiscovery is declared as Runnable as well and will run on its own thread so it can do periodic heartbeats.

The constructor does many things and is best analyzed a few lines at a time.

public MulticastDiscovery() {     try {       discoveryGroup = InetAddress.getByName("228.9.8.7"); 

Multicast communications are organized into channels called groups. When a program sends a datagram to a group, every other participant in that group receives it. Groups are specified using special Internet addresses within the range 224.0.0.1 to 239.255.255.255.

Note 

224.0.0.0 is not included in this range. That address has a special meaning to the Internet and should not be used.

Because all Internet communication is done with sockets, we next need to create a special multicast socket. Multicast sockets are created with only a port number because one multicast socket can belong to many groups.

  discoverySocket = new MulticastSocket(DISCOVERY_PORT);

We must tell it that we want it to belong to the multicast group we just created, as follows:

  discoverySocket.joinGroup(discoveryGroup);

Finally, we need to set a time-to-live (TTL). This setting is really a count of how many subnet boundaries the packet will be allowed to cross. If this were infinite, the multicast packet would travel out over the entire Internet to everyone else’s computer,3 which would create a lot of unnecessary Internet traffic and be an inconvenience to everyone else on the Internet. Because this is a LAN game, we can safely assume we will all be in the same subnet and conservatively set our TTL to 1.

discoverySocket.setTimeToLive(1); // just run on our local sub-net

discoverySocket.setTimeToLive(1); // just run on our local sub-net

Previously, we said UUIDs were a basic necessity for this kind of dynamic discovery. Here we create a UUID that identifies this particular run of the discovery manager and, by association, the program using it:

myUUID = new StatisticalUUID();

Next, we need to create a Datagram for the heartbeat. This is the data packet we will periodically broadcast to all players to let them know we are still here. Java serialization is a convenient way to transfer objects. It allows us to write code that transmits and receives arbitrary objects with no knowledge of their actual data.4 The following code builds a Datagram packet that transfers one data object, our UUID:

// create heartbeat datagram     byte[] hbdata = null;         try {       ByteArrayOutputStream baos = new ByteArrayOutputStream();       ObjectOutputStream oos = new ObjectOutputStream(baos);       oos.writeObject(myUUID);       oos.flush();       hbdata = baos.toByteArray();       oos.close();       baos.close();     } catch (Exception e ) {       e.printStackTrace();     }     hbDatagram = new DatagramPacket(hbdata,hbdata.length,                                      discoveryGroup, DISCOVERY_PORT); 

It is important that you use the DatagramPacket constructor shown in the previous code because it creates the right kind of packet for transmission through a Multicast socket. If you don’t, strange errors can occur.

Next, we must create a DatagramSocketHandler as explained previously to listen for incoming packets and start them running on their own threads.

// create and start listen thread  on discovery socket DatagramSocketHandler rdr = new  DatagramSocketHandler(discoverySocket,1000); rdr.addListener(this); new Thread(rdr).start(); 

Finally, we need to start this object also running on its own thread to do heartbeats and heartbeat checks.

// start heartbeating   new Thread(this).start(); 

The following short method just sets a flag that tells the code in this object’s run() method to send heartbeats. If it is not called, the MulticastDiscovery object is a passive listener just collecting the discovery announcements of active MulticastDiscovery objects. The use for this will become obvious when we actually write the chat host and client code.

/*** This method starts this discoverer announcing  itself to the world.*/   public void startAnnouncing() {     announcer = true;   } 

The next unusual section of code is the listSessions method. It is fairly straightforward except for the fact that we are storing the UUIDs in a Map class.

/**    * listSessions    *    * <return UUID[]    */   public UUID[] listSessions() {     Set ids = timeTracker.keySet();     UUID[] ar = new UUID[ids.size()];         return (UUID[])ids.toArray(ar);   } 

It might seem counterintuitive that a Set would be more appropriate. The reason a Map is used is because we need to track a tuple consisting of <session UUID, last time it heartbeated>. For tuples, a Map is very convenient. If we used a Set, we would have had to store some kind of record object that contained the tuple.

As we get updates based on UUID, we would have needed a Map anyway, to efficiently find a record when it needed an update to the last heartbeat time. All in all, a Map where UUID is the key and the last heartbeat is the value is the simplest solution.

You will note that this method uses two synchronized blocks. They surround places where the timeTracker object is scanned or modified. This is because it is the one object that is used to communicate between the DatagramSocketHandler thread and the MulticastDiscovery thread. We need to make sure they don’t get in each others’ way.

The next section of code is the Run method. This is the method that is called on its own thread when we start a thread, which is constructed with an object of this class as its Runnable.

/**    * run    * This method is a loop that runs on its own thread generating    * heartbeats.    */   public void run() {     while (!done) {       try {         if (announcer) {           discoverySocket.send(hbDatagram);         }         checkTimeStamps();         Thread.sleep(HEARTBEAT_TIME);       }       catch (Exception e) {         e.printStackTrace();       }     }   } } 

This logic is simple: it sits in a loop until told to quit or until the program finishes execution. Every time around that loop, if announcer is set to true, it sends the heartbeat packet we constructed previously. Then it checks to see if any session time stamps have expired. Last, it goes to sleep, relinquishing the processor to other threads, and waits until it is time for the next heartbeat. Checking the time stamp for expiration is sufficiently complex (barely) that it deserves its own method, which is next in the file.

/**    * checkTimeStamps    */   private void checkTimeStamps() {     List removedIDs = new ArrayList();     // find all outdated peers     synchronized(timeTracker) {       for (Iterator i = timeTracker.entrySet().iterator(); i.hasNext(); )        {         Entry entry = (Entry) i.next();         long tstamp = ( (Long) entry.getValue()).longValue();         if ( (System.currentTimeMillis() - tstamp) > (HEARTBEAT_TIME *          3)) {           // missed the alst 3 heartbeats. we think its gone           removedIDs.add(entry.getKey());         }       }     }     // now remove them     for(Iterator i = removedIDs.iterator();i.hasNext();){       UUID id = (UUID)i.next();       synchronized(timeTracker){         timeTracker.remove(id);       }       for(Iterator i2 = listeners.iterator();i2.hasNext();){         ((DiscoveryListener)i2.next()).sessionRemoved(id);       }     }   } 

This methods contains two loops. First, it loops through all the entries in the timeTracker Map, checking to see if they have expired. Expiration is defined as missing three heartbeats in a row. Heartbeats may be delayed or even dropped entirely, being UDP. Three in a row, however, is a strong indication that the program sending the heartbeats has stopped.

The second loop then reads the list of expired time stamps, removes them from the timeTracker Map, and throws an event to the Listeners telling the listener they have been removed. It is necessary to do this in a separate loop because changing the map while we are traversing it has undefined results.

The next method is the datagramReceived method. This method is called whenever the DatagramSocketHandler receives a datagram. Serialization is again used to unpack the data buffer in the DatagramPacket and retrieve the transmitted UUID.

public void datagramReceived(DatagramPacket dgram) {     try {       ByteArrayInputStream bais = new        ByteArrayInputStream(dgram.getData());       ObjectInputStream ois = new ObjectInputStream(bais);       UUID uuidIn = (UUID) ois.readObject();       ois.close();       bais.close();       boolean isNew = (timeTracker.get(uuidIn) == null);       synchronized (timeTracker) {         timeTracker.put(uuidIn, new Long(System.currentTimeMillis()));       }       if (isNew) {         for (Iterator i = listeners.iterator(); i.hasNext(); ) {           ( (DiscoveryListener)            i.next()).sessionAdded(dgram.getAddress(),               uuidIn);         }       }     }     catch (Exception e) {       e.printStackTrace();     }   } 

The UUID is looked up in the <UUID,timestamp: tuple Map, timeTracker. If no entry for this UUID exists, it is a new session to us. Accordingly, we set a flag so we can do callbacks and let our Listeners know about it. Either way, the new <UUID, current time: tuple is inserted into the Map. If there was an existing one, this method overwrites it; if there was not an existing one, a new entry is made. Either way, it is recorded for checkTimeStamps to use.

This method gets called by the DatagramSocketHandler object. Therefore, it is executed on its thread, not on the MulticastDiscovery object’s thread. Accordingly, this is the other place where we need to synchronize access to the timeTracker object.

The last method is just a test we can use to make sure the MulticastDiscovery is working correctly. It uses an anonymous inner class as a Listener to turn the DiscoveryListener callbacks into prints we can see on the command line. Fire up a few of these and see how they report finding themselves and each other. Then kill one and see how the others note its passing.

Creating or Joining a Game

The code so far shows how to know who else is on the Internet with you, but that’s about it. Typically, games divide their players by game sessions. One player will be the creator of the session and will set up the game. The others join a game they want to play.

The next sections of code are for creating and joining game sessions.

Creating a Game Session

The goal here is to do two things. First, the code needs to open a server socket to accept joining players. This task can be accomplished using a ServerSocketListener class, which is similar to the SimpleSocketListener already defined at the start of this chapter.

Second, the game must announce that socket to all the other potential players. The MulticastDiscovery class defined previously works perfectly for this task.

Accordingly, the GameSessionCreator class looks like the following sample code:

/*****************************************************************************     * Copyright (c) 2003 Sun Microsystems, Inc.  All Rights Reserved.     * Redistribution and use in source and binary forms, with or without     * modification, are permitted provided that the following conditions are met:     *     * - Redistribution of source code must retain the above copyright notice,     *   this list of conditions and the following disclaimer.     *     * - Redistribution in binary form must reproduce the above copyright notice,     *   this list of conditions and the following disclaimer in the documentation     *   and/or other materials provided with the distribution.     *     * Neither the name Sun Microsystems, Inc. or the names of the contributors     * may be used to endorse or promote products derived from this software     * without specific prior written permission.     *     * This software is provided "AS IS," without a warranty of any kind.     * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING     * ANY IMPLIED WARRANT OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR     * NON-INFRINGEMEN, ARE HEREBY EXCLUDED.  SUN MICROSYSTEMS, INC. ("SUN") AND     * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS     * A RESULT OF USING, MODIFYING OR DESTRIBUTING THIS SOFTWARE OR ITS     * DERIVATIVES.  IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST     * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,     * INCIDENTAL OR PUNITIVE DAMAGES.  HOWEVER CAUSED AND REGARDLESS OF THE THEORY     * OF LIABILITY, ARISING OUT OF THE USE OF OUR INABILITY TO USE THIS SOFTWARE,     * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.     *     * You acknowledge that this software is not designed or intended for us in     * the design, construction, operation or maintenance of any nuclear facility     *     *****************************************************************************/ 

package com.worldwizards.sbook; import java.util.*; public class GameCreator implements ServerSocketListener{   Discoverer discoverer;   ServerSocketHandler serverSocketHandler;   List listeners = new ArrayList();   public GameCreator(int gameTCPPort) {     serverSocketHandler = new ServerSocketHandler(gameTCPPort);     serverSocketHandler.addListsener(this);     discoverer = new MulticastDiscovery();     discoverer.startAnnouncing();   }   public void addListener(GameCommListener l){     listeners.add(l);   }   // callbacks from serverSocketHandler   public void dataArrived(StreamSocketHandler rdr, byte[] data,                           int length) {     for(Iterator i= listeners.iterator();i.hasNext();){       ((GameCommListener)i.next()).dataArrived(data,length);     }   }   /**    * mainSocketClosed    */   public void mainSocketClosed() {     String txt = "Server Msg: Error, Server socket as closed!";     byte[] txtbuff = txt.getBytes();     for(Iterator i= listeners.iterator();i.hasNext();){       ((GameCommListener)i.next()).dataArrived(txtbuff,txtbuff.length);     }     System.exit(1);   }   /**    * playerJoined    */   public void playerJoined() {     String txt = "Server Msg: Player Connected!";     byte[] txtbuff = txt.getBytes();     for(Iterator i= listeners.iterator();i.hasNext();){       ((GameCommListener)i.next()).dataArrived(txtbuff,txtbuff.length);     }   }   /**    * playerLeft    */   public void playerLeft() {     String txt = "Server Msg: Player Disconnected!";    byte[] txtbuff = txt.getBytes();    for(Iterator i= listeners.iterator();i.hasNext();){      ((GameCommListener)i.next()).dataArrived(txtbuff,txtbuff.length);    }   }   /**    * sendData    *    * <param data byte[]    * <param i int    */   public void sendData(byte[] data, int length) {     serverSocketHandler.send(data,length);   } } 

This code is fairly simple and straightforward. It creates a ServerSocket to listen for connecting clients and wraps the returned Socket in a ServerSocketHandler that handles the details of the connection. It then announces its presence on the LAN using a MulticastDiscovery object. It calls setAnnounce() on the MulticastDiscovery object to tell it that we are a host, not a client.

The ServerSocketHandler class is a central component of the chat server and looks like the following sample code:

/*****************************************************************************  * Copyright (c) 2003 Sun Microsystems, Inc.  All Rights Reserved.  * Redistribution and use in source and binary forms, with or without  * modification, are permitted provided that the following conditions are met:  *  * - Redistribution of source code must retain the above copyright notice,  *   this list of conditions and the following disclaimer.  *  * - Redistribution in binary form must reproduce the above copyright notice,  *   this list of conditions and the following disclaimer in the documentation  *   and/or other materials provided with the distribution.  *  * Neither the name Sun Microsystems, Inc. or the names of the contributors  * may be used to endorse or promote products derived from this software  * without specific prior written permission.  *  * This software is provided "AS IS," without a warranty of any kind.  * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING  * ANY IMPLIED WARRANT OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR  * NON-INFRINGEMEN, ARE HEREBY EXCLUDED.  SUN MICROSYSTEMS, INC. ("SUN") AND  * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS  * A RESULT OF USING, MODIFYING OR DESTRIBUTING THIS SOFTWARE OR ITS  * DERIVATIVES.  IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST  * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, * INCIDENTAL OR PUNITIVE DAMAGES.  HOWEVER CAUSED AND REGARDLESS OF THE THEORY  * OF LIABILITY, ARISING OUT OF THE USE OF OUR INABILITY TO USE THIS SOFTWARE,  * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.  *  * You acknowledge that this software is not designed or intended for us in  * the design, construction, operation or maintenance of any nuclear facility  * *****************************************************************************/ 

package com.worldwizards.sbook; import java.net.*; import java.util.*; public class ServerSocketHandler     implements StreamSocketListener, Runnable {   ServerSocket myServerSocket;   List listeners = new ArrayList();   private List streams = new ArrayList();   public ServerSocketHandler(int port) {     // Create the Server Socket.  This makes an end-point for     // joining games to connect to.     try {       myServerSocket = new ServerSocket(port);     }     catch (Exception ex) {       ex.printStackTrace();     }     new Thread(this).start();   }   /**    * run    */   public void run() {     while (true) { // loop until program is interrupted       try {         // accept() says "I’m ready to handle a connector"         Socket newConnSocket = myServerSocket.accept();         StreamSocketHandler ssockrdr =             new StreamSocketHandler(newConnSocket);         ssockrdr.addListener(this);         streams.add(ssockrdr);         Thread socketThread = new Thread(             ssockrdr);         socketThread.start();         doPlayerJoined();       }       catch (Exception ex) {         ex.printStackTrace();       }     }   }   public void addListsener(ServerSocketListener l) {     listeners.add(l);   }   public void doPlayerJoined() {     for (Iterator i = listeners.iterator(); i.hasNext(); ) {       ( (ServerSocketListener) i.next()).playerJoined();     }   }   public void doPlayerLeft() {     for (Iterator i = listeners.iterator(); i.hasNext(); ) {       ( (ServerSocketListener) i.next()).playerLeft();     }   }   /**    * dataArrived    *    * <param rdr StreamSocketHandler    * <param data byte[]    */   public void dataArrived(StreamSocketHandler rdr, byte[] data,                           int length) {     for (Iterator i = listeners.iterator(); i.hasNext(); ) {       ( (ServerSocketListener) i.next()).dataArrived(rdr, data,           length);     }   }   /**    * socketClosed    *    * <param StreamSocketHandler StreamSocketHandler    */   public void socketClosed(StreamSocketHandler StreamSocketHandler) {     streams.remove(StreamSocketHandler);     doPlayerLeft();   }   /**    * send    *    * <param bs byte[]    */   public void send(byte[] bs, int sz) {     for (Iterator i = streams.iterator(); i.hasNext(); ) {       ( (StreamSocketHandler) i.next()).send(bs,sz);     }   } } 

The most interesting part of this code is the loop that handles the ServerSocket itself:

 /**    * run    */   public void run() {     while (true) { // loop until program is interrupted       try {         // accept() says "I’m ready to handle a connector"         Socket newConnSocket = myServerSocket.accept();         StreamSocketHandler ssockrdr =             new StreamSocketHandler(newConnSocket);         ssockrdr.addListener(this);         streams.add(ssockrdr);         Thread socketThread = new Thread(             ssockrdr);         socketThread.start();         doPlayerJoined();       }       catch (Exception ex) {         ex.printStackTrace();       }     }   } 

This object sits on its own thread and blocks on the accept() call. Whenever accept() returns, it returns a new Socket connected to a foreign socket that has initiated a connection to the ServerSocket. You can think of accept() as a telephone switchboard. All incoming calls come to it. It then connects those calls to a local telephone and hands the phone back to the program so it can communicate.

The Socket is added to a list of Sockets, so that all data sent to the server by one joiner can be easily echoed to all.

To read the data sent to the Server on that Socket, the code wraps that Socket in a StreamSocketHandler, which is a utility class that functions much like the SimpleSocketListener mentioned previously. Instead of calling a hardwired printing object, however, it calls back registered listeners whenever data arrives.

Finally, it calls back its Listeners, letting them know another joiner has arrived.

Joining a Game Session

The GameJoiner class provides the functionality for client game sessions to discover and connect to the ServerSocket created by the GameCreator. It looks like the following sample code:

/*****************************************************************************  * Copyright (c) 2003 Sun Microsystems, Inc.  All Rights Reserved.  * Redistribution and use in source and binary forms, with or without  * modification, are permitted provided that the following conditions are met:  *  * - Redistribution of source code must retain the above copyright notice,  *   this list of conditions and the following disclaimer.  *  * - Redistribution in binary form must reproduce the above copyright notice,  *   this list of conditions and the following disclaimer in the documentation  *   and/or other materials provided with the distribution. *  * Neither the name Sun Microsystems, Inc. or the names of the contributors  * may be used to endorse or promote products derived from this software  * without specific prior written permission.  *  * This software is provided "AS IS," without a warranty of any kind.  * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING  * ANY IMPLIED WARRANT OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR  * NON-INFRINGEMEN, ARE HEREBY EXCLUDED.  SUN MICROSYSTEMS, INC. ("SUN") AND  * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS  * A RESULT OF USING, MODIFYING OR DESTRIBUTING THIS SOFTWARE OR ITS  * DERIVATIVES.  IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST  * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,  * INCIDENTAL OR PUNITIVE DAMAGES.  HOWEVER CAUSED AND REGARDLESS OF THE THEORY  * OF LIABILITY, ARISING OUT OF THE USE OF OUR INABILITY TO USE THIS SOFTWARE,  * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.  *  * You acknowledge that this software is not designed or intended for us in  * the design, construction, operation or maintenance of any nuclear facility  * *****************************************************************************/ 

package com.worldwizards.sbook; import java.io.*; import java.net.*; import java.util.*; public class GameJoiner     implements DiscoveryListener {   Discoverer discoverer;   int port;   List listeners = new ArrayList();   public GameJoiner(int hostTCPPort) {     port = hostTCPPort;     discoverer = new MulticastDiscovery();     discoverer.addListener(this);   }   public void addListener(GameSessionListener l) {     listeners.add(l);   }   public GameSession joinGame(InetAddress address){     try {       Socket tcpSocket = new Socket();       tcpSocket.connect(new InetSocketAddress(address,port)); // make        connection to host       StreamSocketHandler hdlr = new StreamSocketHandler(tcpSocket);       new Thread(hdlr).start(); // start listening       return new GameSession(hdlr);     }     catch (IOException ex) {       ex.printStackTrace();     }     return null;   }   /**    * sessionAdded    *    * <param address InetAddress    * <param sessionUUID UUID    */   public void sessionAdded(InetAddress address, UUID sessionUUID) {     for (Iterator i = listeners.iterator(); i.hasNext(); ) {       ( (GameSessionListener) i.next()).sessionAdded(address,           sessionUUID);     }   }   /**    * sessionRemoved    *    * <param sessionUUID UUID    */   public void sessionRemoved(UUID sessionUUID) {     for (Iterator i = listeners.iterator(); i.hasNext(); ) {       ( (GameSessionListener) i.next()).sessionRemoved(sessionUUID);     }   } } 

The game joiner also begins by creating a MultiCastDiscovery object.

 public GameJoiner(int hostTCPPort) {     port = hostTCPPort;     discoverer = new MulticastDiscovery();     discoverer.addListener(this);   } 

Because it is interested only in hearing game-session advertisements and not advertising itself as a server, it does not call the discoverer’s startAnnouncing() method. Therefore, it just sits quietly listening to others’ announcements.

A port number is also passed in. This is the port on which the game host will open its ServerSocket. In this simple design, the port is known a priori by the game developer and passed to both GameCreator and GameJoiner. Because only one ServerSocket can listen at a given port at any time, we are limited to one GameCreator per machine. How to engineer around this limit is discussed briefly in the following section.

Most of GameJoiner methods are just callback routines handling the events sent by MulticastDiscovery. The most interesting part of the class code is what it does when you want to join a game session.

public GameSession joinGame(InetAddress address){     try {       Socket tcpSocket = new Socket();       tcpSocket.connect(new InetSocketAddress(address,port)); // make        connection to host       StreamSocketHandler hdlr = new StreamSocketHandler(tcpSocket);       new Thread(hdlr).start(); // start listening       return new GameSession(hdlr);     }     catch (IOException ex) {       ex.printStackTrace();     }     return null;   } 

The joinGame() method is called with the Internet address of the game host to which you want to connect. (The Internet address was supplied to the joiner in the sessionAdded() callback, which responds to game announcements from the host.)

The joinGame() routine creates a Socket and connects it to the well-known game session port (see previous) at the supplied Internet address. It then wraps that port in one of our StreamSocketHandler classes, which run the listening thread on that socket and return any arriving data as callbacks.

Finally, it wraps the StreamSocketHandler in a GameSession object. In this example, the GameSession object is a more or less empty wrapper that just converts the StreamSocketListener callbacks received from StreamSocketHandler to GameCommListener events. The same code can be used to respond to game data packets in the Joiner and in the Host and makes the client API layer a bit neater.5 It could, however, be extended to include game-specific data filtering or other such game-protocol handling. (See the Conclusion for more information.)

 On the CD   The rest of LANChat is mundane Java plumbing and should be familiar to anyone with moderate Java coding experience. The entire code for LANChat is on the CD-ROM included with this book.

Summary

The point of this chapter has been to demystify the fundamental plumbing needed in Java to hook LAN games together. To make that plumbing clear and easy to follow, many features have been left out that a real game might need, such as dynamic port allocation, attaching data to game sessions for display in the browser, attaching names or IDs to players for identification, and private communication, host migration, and various other extended tasks.

All these can be implemented by protocols on top of the basic networking concepts shown here. Dynamic port numbers and game-descriptive data could be added to the announcement packets. Player information could be sent by the player when it first connects to the ServerSocket and is read by the host from all brand-new connections. Similarly, data about the current game state and players could be sent back down the Socket connection as the first data from the host to the client.

Host migration is the term for handing the “host” designation off to a client in the event that the host suddenly dies. This can be tricky to get right and seriously complicates your logic because each client now has to have direct Socket connections to all other clients and a copy of the current host game state. In general, this sort of failure protection is overkill and few games implement it. Instead they just error out if the host dies and let the players create and join a new session.

The following section of this chapter steps up to the next level of logic and discusses what you might want to be communicating between game sessions and why.



Practical Java Game Programming
Practical Java Game Programming (Charles River Media Game Development)
ISBN: 1584503262
EAN: 2147483647
Year: 2003
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net