8.3. An Extended Example: Tic-Tac-ToeTic-tac-toe is a game ordinarily played on paper by two people sitting near each other. The board is a three-by-three grid and usually one player marks squares using X's and the other using O's. The players alternate marking the squares, trying to end up with three of their marked squares in a line (i.e., across, down, or diagonally). In the Zeroconf version, players register service instances of type _tic-tac-toe-ex._tcp and browse for other players. The game program has two main classes, TicTacToe and GameBoard. Class TicTacToe browses for other players and displays the list of what it finds. It also opens a listening socket, advertises it with DNS-SD, and then fires off an independent background thread to sit and wait for incoming connections. Class GameBoard can get instantiated in two ways. If the user clicks on one of the discovered players in the list, then we make a new GameBoard, and start a DNSSD.resolve( ) running for the named service. When the newly created GameBoard object receives the serviceResolved( ) callback, it connects to the specified host and port and begins playing the game, listing for messages received over the network from the peer, and sending messages to the peer every time the user clicks in a square. The other way a GameBoard can get instantiated is on the receiving end of a connection request. If another user clicks on us in their list, then our TicTacToe background thread will receive an incoming connection request. In this case it also makes a new GameBoard object, but in this case no resolve-and-connect is needed, because the TCP connection is already open. A player can have any number of active games, connected to different opponents, at once. Figure 8-1 shows the TicTacToe class's browser window showing the list of discovered opponents on the network. Figure 8-1. Window displaying available opponentsFigure 8-2 shows a GameBoard window for a game in progress with a player called "Mike." Figure 8-2. TicTacToe game boardNote that the purpose of this example is to demonstrate the Zeroconf-related aspects of writing a Java program. As a result, this example does not try to implement the rules of tic-tac-toe; for example, it does not enforce that the players are supposed to take turns clicking squares. Each time you run the program, it asks the system for a new unallocated TCP port to listen on and then advertises that port number to its peers using DNS-SD. One of the benefits of DNS-SD is that, because it advertises port numbers as well as hostnames and addresses, programs are no longer restricted to using fixed, hard-coded port numbers. This means you can write a program and it can use any available port when run, instead of your having to apply to IANA to get a new well-known port number reserved for every program you write. There are only 65,535 possible TCP port numbers, and they'll run out quickly if every person in the world gets one reserved for every program they write. In addition, even if you get a well-known port number reserved, you get only one, so that doesn't help when you want to run two copies of your program on the same machine. You'll see that, with the TicTacToe program, you can run as many copies as you like on the same machine, which can be very helpful when testing, especially if you're working on your laptop computer on an airplane and don't have a whole network of machines available. Most Unix systems allocate dynamic TCP port numbers starting at 49152 and working upward. If you have some kind of personal firewall running on your machine, ensure that it is configured to allow incoming connections to high-numbered ports (49152-65535). Otherwise, the firewall will do exactly what it is supposed to do: prevent your networking program from receiving any incoming connection requests. Most firewall programs don't give you any feedback to tell you when they've silently discarded an incoming connection request, so this can be quite frustrating to debug if your program is failing and you don't realize that the personal firewall is the cause. The TicTacToe program window title shows the port number it's listening on, so that you can cross-check with your firewall settings and verify that your personal firewall is allowing the necessary packets through. Our TicTacToe program calls DNSSD.register without specifying an instance name, so it automatically gets the system default. When it gets the serviceRegistered callback, it updates the window title to show its advertised name. You can try some experiments to see how Multicast DNS name conflict detection works. If you run a second copy of the TicTacToe program on the same machine, you'll see the second copy gets the same name with "(2)" appended. If you plug your Ethernet cable into a network where your name is already being advertised by another TicTacToe program, you'll see your window title update to show a new name. You can also change the system default name while the TicTacToe program is running, and you'll see that it gets informed of the new name and updates its windows. On Mac OS X, you set the system default name by setting the "Computer Name" in the Sharing Preferences. The TicTacToe program also pays attention to its own advertised name in order to exclude itself from the list of discovered games on the network. Example 8-7 shows the source code for TicTacToe.java, and Example 8-8 shows the source code for GameBoard.java. You can compile them both directly on the command line by typing javac TicTacToe.java and then run the program by typing java TicTacToe, or you can use the Makefile shown in Example 8-9. The Makefile builds the classes, placing them in a subdirectory called classes, then makes a Java jar file from the classes, and finally runs the resulting jar file with java -jar TicTacToe.jar. Example 8-7. TicTacToe.javaimport java.util.HashMap; import java.nio.*; import java.nio.channels.*; import java.net.InetSocketAddress; import javax.swing.*; import javax.swing.event.*; import com.apple.dnssd.*; // Our TicTacToe object does the following: // 1. It's a JFrame window. It's a DNSSD BrowseListener so it // gets add and remove events to tell it what to show in the window, // and a ListSelectionListener so it knows what the user clicked. // 2. It listens for incoming connections. It opens a listening TCP // socket and advertises the listening TCP socket with DNS-SD. // It's our RegisterListener, so that it knows our advertised name: // - To display it in the window title bar // - To exclude it from the list of discovered peers on the network // To safely call Swing routines to update the user interface, // we have to call them from the Swing event-dispatching thread. // To do this, we make little Runnable objects where necessary // and pass them to SwingUtilities.invokeAndWait( ). This makes // their run( ) method execute on event-dispatching thread where // it can safely make the calls it needs. For more details, see: // <http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html> public class TicTacToe extends JFrame implements Runnable, RegisterListener, BrowseListener, ListSelectionListener { public static void main(String[] args) { Runnable runOnSwingThread = new Runnable( ) { public void run( ) { new TicTacToe( ); } }; try { SwingUtilities.invokeAndWait(runOnSwingThread); } catch (Exception e) { e.printStackTrace( ); } } public static final String ServiceType = "_tic-tac-toe-ex._tcp"; public String myName; public HashMap activeGames; private DefaultListModel gameList; private JList players; private ServerSocketChannel listentingChannel; private int listentingPort; // NOTE: Because a TicTacToe is a JFrame, the caller MUST be running // on the event-dispatching thread before trying to create one. public TicTacToe( ) { super("Tic-Tac-Toe"); try { // 1. Make the browsing window, and start browsing activeGames = new HashMap( ); gameList = new DefaultListModel( ); players = new JList(gameList); players.addListSelectionListener(this); getContentPane( ).add(new JScrollPane(players)); setSize(200, 300); setDefaultCloseOperation(EXIT_ON_CLOSE); setVisible(true); DNSSD.browse(ServiceType, this); // 2. Make listening socket and advertise it listentingChannel = ServerSocketChannel.open( ); listentingChannel.socket( ).bind(new InetSocketAddress(0)); listentingPort = listentingChannel.socket( ).getLocalPort( ); setTitle(listentingPort + " registering"); DNSSD.register(null, ServiceType, listentingPort, this); // 3. If we sit here and hog the event-dispatching thread // the whole UI will freeze up, so instead we create a new // background thread to receive incoming connection requests. new Thread(this).start( ); } catch (Exception e) { e.printStackTrace( ); } } public void operationFailed(DNSSDService service, int errorCode) { System.out.println("DNS-SD operation failed " + errorCode); System.exit(-1); } // If our name changes while we're running, we update window title. // In the event that we're registering in multiple domains (Wide-Area // DNS-SD) we'll use the local (mDNS) name for display purposes. public void serviceRegistered(DNSSDRegistration sd, int flags, String serviceName, String regType, String domain) { if (!domain.equalsIgnoreCase("local.")) return; myName = serviceName; Runnable r = new Runnable( ) { public void run( ) { setTitle(listentingPort + " " + myName); } }; try { SwingUtilities.invokeAndWait(r); } catch (Exception e) { e.printStackTrace( ); } } // Our serviceFound and serviceLost callbacks just make Adder and // Remover objects that safely run on the event-dispatching thread // so they can modify the user interface public void serviceFound(DNSSDService browser, int flags, int ind, String name, String type, String domain) { if (name.equals(myName)) return; // Don't add ourselves to the list DiscoveredInstance x = new DiscoveredInstance(ind, name, domain); try { SwingUtilities.invokeAndWait(new Adder(x)); } catch (Exception e) { e.printStackTrace( ); } } public void serviceLost(DNSSDService browser, int flags, int ind, String name, String regType, String domain) { DiscoveredInstance x = new DiscoveredInstance(ind, name, domain); try { SwingUtilities.invokeAndWait(new Remover(x)); } catch (Exception e) { e.printStackTrace( ); } } // The Adder and Remover classes update the list of discovered instances private class Adder implements Runnable { private DiscoveredInstance add; public Adder(DiscoveredInstance a) { add = a; } public void run( ) { gameList.addElement(add); } } private class Remover implements Runnable { private DiscoveredInstance rmv; public Remover(DiscoveredInstance r) { rmv = r; } public void run( ) { String name = rmv.toString( ); for (int i = 0; i < gameList.size( ); i++) { if (gameList.getElementAt(i).toString( ).equals(name)) { gameList.removeElementAt(i); return; } } } } // When the user clicks in our list, if we already have a // GameBoard we bring it to the front, otherwise we make // a new GameBoard and initiate a new outgoing connection. public void valueChanged(ListSelectionEvent event) { int selected = players.getSelectedIndex( ); if (selected != -1) { DiscoveredInstance x = (DiscoveredInstance)players.getSelectedValue( ); GameBoard game = (GameBoard)activeGames.get(x.toString( )); if (game != null) game.toFront( ); else x.resolve(new GameBoard(this, x.toString( ), null)); } } // When we receive an incoming connection, GameReceiver reads the // peer name from the connection and then makes a new GameBoard for it. private class GameReceiver implements Runnable { private SocketChannel sc; public GameReceiver(SocketChannel s) { sc = s; } public void run( ) { try { ByteBuffer buffer = ByteBuffer.allocate(4 + 128); CharBuffer charBuffer = buffer.asCharBuffer( ); sc.read(buffer); int length = buffer.getInt(0); char[] c = new char[length]; charBuffer.position(2); charBuffer.get(c, 0, length); String serviceName = new String(c); GameBoard game = new GameBoard(TicTacToe.this, serviceName, sc); } catch (Exception e) { e.printStackTrace( ); } } } // Our run( ) method just sits and waits for incoming connections // and hands each one off to a new thread to handle it. public void run( ) { try { while (true) { SocketChannel sc = listentingChannel.accept( ); if (sc != null) new Thread(new GameReceiver(sc)).start( ); } } catch (Exception e) { e.printStackTrace( ); } } // Our inner class DiscoveredInstance has two special properties // It has a custom toString( ) method to display discovered // instances the way we want them to appear, and a resolve( ) // method, which asks it to resolve the named service it represents // and pass the result to the specified ResolveListener public class DiscoveredInstance { private int ind; private String name, domain; public DiscoveredInstance(int i, String n, String d) { ind = i; name = n; domain = d; } public String toString( ) { String i = DNSSD.getNameForIfIndex(ind); return(i + " " + name + " (" + domain + ")"); } public void resolve(ResolveListener x) { try { DNSSD.resolve(0, ind, name, ServiceType, domain, x); } catch (DNSSDException e) { e.printStackTrace( ); } } } } Example 8-8. GameBoard.javaimport java.nio.*; import java.nio.channels.SocketChannel; import java.net.InetSocketAddress; import java.awt.*; import java.awt.event.*; import javax.swing.*; import com.apple.dnssd.*; public class GameBoard extends JFrame implements ResolveListener, Runnable { private TicTacToe tictactoe; private String name, host; private int port; SocketChannel channel; // If we're passed in a SocketChannel, it means we received an // incoming connection, so we should start receiving clicks from it. // If channel is null, it means our user initiated an outgoing connection, // so we'll get a serviceResolved callback to tell us when to proceed. public GameBoard(TicTacToe t, String n, SocketChannel c) { super(n); tictactoe = t; name = n; channel = c; tictactoe.activeGames.put(n, this); getContentPane( ).setLayout(new GridLayout(3,3,6,6)); getContentPane( ).setBackground(Color.BLACK); for (int i = 0; i<9; i++) getContentPane( ).add(new SquareGUI(this, i)); setSize(200,200); setVisible(true); if (channel != null) new Thread(this).start( ); } public void operationFailed(DNSSDService service, int errorCode) { System.out.println("DNS-SD operation failed " + errorCode); System.exit(-1); } // When serviceResolved is called, we send our name to the other end // and then fire off our thread to start receiving the opponent's clicks. public void serviceResolved(DNSSDService resolver, int flags, int ifIndex, String fullName, String theHost, int thePort, TXTRecord txtRecord) { host = theHost; port = thePort; ByteBuffer buffer = ByteBuffer.allocate(4 + 128); CharBuffer charBuffer = buffer.asCharBuffer( ); buffer.putInt(0, tictactoe.myName.length( )); charBuffer.position(2); charBuffer.put(tictactoe.myName); try { InetSocketAddress socketAddress = new InetSocketAddress(host, port); channel = SocketChannel.open(socketAddress); channel.write(buffer); new Thread(this).start( ); } catch (Exception e) { e.printStackTrace( ); } resolver.stop( ); } // The GameBoard's run( ) method just sits in a loop receiving // clicks from the opponent and marking the indicated squares. public void run( ) { try { while (true) { ByteBuffer buffer = ByteBuffer.allocate(4); channel.read(buffer); int n = buffer.getInt(0); if (n >= 0 && n < 9) { try { SwingUtilities.invokeAndWait(new SquareMarker(n)); } catch (Exception e) { e.printStackTrace( ); } } } } catch (Exception e) { } // Connection reset by peer! } // When we get a message from the opponent, we make a SquareMarker // object and run it on the event-dispatching thread so it can // safely do Swing calls to update the user interface class SquareMarker implements Runnable { private int num; public SquareMarker(int n) { num = n; } public void run( ) { SquareGUI s = (SquareGUI)getContentPane( ).getComponent(num); s.setText("<html><h1><font color='blue'>O</font></h1></html>"); s.setEnabled(false); } } // Each GameBoard contains nine JButtons displayed in a 3x3 grid class SquareGUI extends JButton implements ActionListener { private int num; public SquareGUI(GameBoard b, int n) { num = n; addActionListener(this); } public void actionPerformed(ActionEvent event) { // Mark our square with an X setText("<html><h1><font color='red'>X</font></h1></html>"); setEnabled(false); // And tell the other end to mark the square too ByteBuffer buffer = ByteBuffer.allocate(4); buffer.putInt(0, num); try { channel.write(buffer); } catch (Exception e) { e.printStackTrace( ); } } } } Example 8-9. Makefile to build Tic-Tac-Toe examplerun: TicTacToe.jar java -jar TicTacToe.jar & clean: rm -rf classes TicTacToe.jar TicTacToe.jar: classes/TicTacToe.class @echo "Main-Class: TicTacToe" > Main-Class.txt jar cmf Main-Class.txt TicTacToe.jar -C classes . @rm Main-Class.txt # Building TicTacToe.class causes javac automatically # to find and build other necessary classes too classes/TicTacToe.class: TicTacToe.java GameBoard.java mkdir -p classes javac -encoding UTF8 -d classes TicTacToe.java You have now seen how to implement a basic service in Java and advertise it using DNS-SD. The TicTacToe application registers and browses for services of type _tic-tac-toe-ex._tcp. You have resolved services and provided the underlying plumbing to send and receive messages. The remainder of the code managed the GUI elements. With just a few lines of code, you can add DNS-SD advertising and browsing to your own Java programs. |