The JavaTM Tutorial
Previous Page Lesson Contents Next Page Start of Tutorial > Start of Trail > Start of Lesson Search

Trail: Putting It All Together
Lesson: BINGO!

Broadcasting Game Information

The Game sends three kinds of information to the Players:
  1. BINGO balls
  2. Player status updates (such as when a new player registers or when a player cries wolf)
  3. Game status updates (such as when the game is about to begin, when the game is over, and so on)
The Game needs to send this information to all of the Players simultaneously. Most notably, the balls must be sent to all Players at the same time so that the Game does not favor one Player over the others--timing can make a critical difference in the game. Sending information to many recipients at the same time can be accomplished using UDP and is known as broadcasting. If you don't know about UDP and how it works, refer to Overview of Networking(in the Putting It All Together trail) and All About Datagrams(in the Putting It All Together trail).

To broadcast a tidbit of information to the Players, the Game creates a DatagramPacket containing the information encoded in a byte array. The Game addresses the packet with a destination port number and a group identifier specified by a class D Internet address. The port number is an arbitrarily chosen number that the Player and Game have agreed to use. The group identifier simply identifies what type of information is contained in the packet.

The Game uses three different group identifiers each of which identifies one of the three kinds of information sent by the Game. Thus ball packets are marked with the BallListeningGroup identifier, the player status packets are marked with the PlayerListenerGroup identifier, and the game status packets are marked with the GameListenerGroup identifier.

To receive information broadcast by the Game, the Player opens a MulticastSocket on the same port number used by the Game to create its DatagramPacket. Next the Player asks the MulticastSocket to join one of the groups that the Game used to create its DatagramPacket. That MulticastSocket will only receive packets destined for that port and marked with that group identifier. If the Player wants information for a different group, it must create a different MulticastSocket and have it join a different group.

Using UDP in this way, the Game can broadcast information to all the Players simultaneously and uses only one port for all of its status updates. Additionally, through the use of group identifiers, the Game can identify the different kinds of information thereby allowing the Player application to receive only the information that it wants.


This figure has been reduced to fit on the page.
Click the image to view it at its natural size.

Currently, the Player application listens for announced balls and game status updates, but ignores player updates.

As the diagram implies, one interesting side-effect of broadcasting information to the Players is that the Game can listen to the information, too. This particular side-effect was used to advantage to design the various status panes to operate independently of the Game. Instead of having the Game directly update the status panes within its code, the status panes update themselves by listening to the broadcast information. Thus the status panes can be shared by the Player application (or any other application) as well.

How the Game Broadcasts Information

The SocketGate(in a .java source file) class, as its name implies, is the gate through which all information is broadcast from the Game application.

The Game application creates and uses one instance of SocketGate for the entire program. The RingMaster(in a .java source file) creates the SocketGate in its constructor:

socketGate = new SocketGate();
The constructor for SocketGate creates a MulticastSocket through which the Game sends DatagramPackets. Additionally, the constructor creates three InetAddress's. These are the group identifiers for the game. There is one group identifier for each type of information broadcast from the Game: balls, player status, and game status.
SocketGate () throws java.io.IOException {
    socket = new MulticastSocket(Constants.portNumber);

    ballListeningGroup =
        InetAddress.getByName(Constants.BallListeningGroup);
    playerListeningGroup =
        InetAddress.getByName(Constants.PlayerListeningGroup);
    gameListeningGroup =
        InetAddress.getByName(Constants.GameListeningGroup);
}
The argument to the constructor for the MulticastSocket, Constants.portNumber, is the port to which the socket is bound. The actual port number doesn't matter except that the Game and the Player agree on what it is and it's not one of the reserved port numbers between 0 and 1023. The BINGO game port number is 52596 which is Sophia's birthday (5/25/96).

Group identifiers are class D Internet address which range from 224.0.0.1 and 239.255.255.255. As with the port number, the actual addresses used by the BINGO game don't matter as long as the Player and Game agree on what they are. The addresses we chose for the BINGO game are consecutive and are as follows (for no particular reason at all):

All information is broadcast from the Game via the sendBytes method provided by SocketGate. Given an array of bytes and a group identifier, sendBytes packages the array into a DatagramPacket and sends it on its way.

private void sendBytes(byte[] data, InetAddress group) {
    DatagramPacket packet =
       new DatagramPacket(data, data.length, group,
		          Constants.portNumber);
    try {
        socket.send(packet);
    } catch (java.io.IOException e) {
        // PENDING: what should go in here?
    }
}
The sendBytes method is called from each of the three convenience methods implemented by SocketGate to send each kind of information sent by the Game:
void sendBall(BingoBall b) {
    sendBytes(b.getBytes(), ballListeningGroup);
}

void sendPlayerStatusMessage(PlayerRecord p) {
    sendBytes(p.getBytes(), playerListeningGroup);
}

void sendGameStatusMessage(String msg) {
    sendBytes(msg.getBytes(), gameListeningGroup);
}
The information sent in a Datagrampacket must be bytes, so all of the information is converted to bytes before being broadcast. The receiver must reconstruct the actual information from bytes on the other side. You'll see this in How to Receive Information Broadcast by the Game.

Note that the code is a bit cavalier about converting everything to bytes. Although careless, the code works for all of the converted data except in one, unlikely, scenario.

The BINGO ball numbers always survive the conversion because they range from 1 to 75 which is small enough to fit into a byte. The game status string survives the conversion because the code calls the getBytes method from the String class which automatically encodes Unicode characters to bytes (and back again on the other side) using the default character encoding.

Potential problems occur with the player status. The player name is a String and survives the conversion for the same reason that the game status string survives. However, problems may occur with the numbers in the player status--the player ID, the number of cards used by the player, and the number of wolf cries made by the player--which are all declared to be integers.

By default, the game parameters limit these numbers to those that fit within a byte (player ID won't exceed 100, the number of cards won't exceed 3, and so on). However, if you change the game parameters, and allow them to exceed 255, then the numbers will not survive the conversion and will be broadcast incorrectly.

How to Receive Information Broadcast by the Game

The bingo.shared package contains three classes whose sole purpose is to just sit around and listen for packets to come across a MulticastSocket. Each class listens for packets marked with a different group identifier: All of the listening thread classes subclass the abstract superclass called ListenerThread(in a .java source file) shown here:
package bingo.shared;

import java.net.*;
import java.io.*;

public abstract class ListenerThread extends Thread {

    boolean stopListening = false;
    MulticastSocket socket;

    private InetAddress group;
    private String groupString;

    public ListenerThread(String groupString)
        throws UnknownHostException, IOException
    {
	super();

	this.groupString = groupString;

        this.group = InetAddress.getByName(groupString);
        socket = new MulticastSocket(Constants.portNumber);
	socket.joinGroup(group);
    }

    public void stopListening() throws IOException{
	stopListening = true;
	socket.leaveGroup(group);
	socket.close();
    }
}
The abstract superclass provides a constructor that its subclasses must call from their constructors, and a mechanism for gently stopping the thread.

As you can see from the code, the constructor in ListenerThread creates a group identifier based on the group string passed into the constructor. The group string is passed into the constructor by a subclass constructor. The BallListenerThread subclass passes in Constants.BallListeningGroup, and so on. This is the same group used by the Game to address packets sent to the MulticastSocket.

The constructor then creates a MulticastSocket on the BINGO port, and joins a group on that socket. By joining the group this thread will only receive information for that particular group. Each ListenerThread must have its own MulticastSocket because a MulticastSocket can join only one group at a time. Thus a MulticastSocket cannot be shared for listening as it can be for broadcasting.

The subclasses of ListenerThread each provide their own run method which are all similar. Each loops until stopListening becomes true. During each iteration of the loop, the thread waits for a packet to come over the socket. When a packet arrives, the thread gets the information from the packet, converts it from bytes to the appropriate type, and notifies another object (its notifyee) of the event.

The three subclasses of ListenerThread are basically the same, so let's just look at one: the BallListenerThread(in a .java source file) class:

package bingo.shared;

import java.net.*;
import java.io.*;

public class BallListenerThread extends ListenerThread {

    private BallListener notifyee;

    public BallListenerThread(BallListener notifyee)
        throws IOException
    {

	super(Constants.BallListeningGroup);
	this.notifyee = notifyee;
    }

    public synchronized void run() {
	DatagramPacket packet;

        while (stopListening == false) {
	    byte[] buf = new byte[256];
            packet = new DatagramPacket(buf, 256);
	    try {
                socket.receive(packet);
		byte[] rcvd = packet.getData();
		BingoBall b = new BingoBall(rcvd);
		if (b.getNumber() == BingoBall.GAME_OVER) {
		    notifyee.noMoreBalls();
		} else
		    notifyee.ballCalled(b);

	    } catch (IOException e) {
		    // PENDING: what goes in here?
	    }
        }
    }
}
The constructor for BallListenerThread requires a BallListener(in a .java source file)-- an object that implements the BallListener interface and thus implements two methods: ballCalled and noMoreBalls. The BallListenerThread notifies its BallListener whenever a ball is called, or when the end-of-game ball arrives by calling the appropriate one of those two methods. The notifyee then takes whatever action is appropriate.

PlayerListenerThread(in a .java source file) is similar but its notifyee is a PlayerListener(in a .java source file). Likewise, GameListenerThread(in a .java source file)'s notifyee is a GameListener(in a .java source file).


Previous Page Lesson Contents Next Page Start of Tutorial > Start of Trail > Start of Lesson Search