John is an independent consultant. You can reach him at john.rodley@channel1.com or visit his home page at http://www.channel1.com/users/ajrodley.
From HTML 2.0 to Netscape extensions to VRML, a slew of new technologies have popped up that promise to flesh out a Web that is still more flash than cash. Among these new developments, none is more eagerly anticipated than executable content--Web content that actually executes on the local computer. Java, a programming language from Sun Microsystems, makes executable content a reality.
HTML is essentially a "flat" technology--static text with hyperlinks to other lumps of static text. Current Web browsers take a stream of static text and display it on screen. The only logic embedded in HTML text consists of text formatting and image/sound file-loading commands. The combination of the HotJava browser and the Java programming language changes all of this.
A Java program (myprogram.java, for example) is compiled into bytecodes that are interpreted at run time by the Java interpreter. HotJava, a Web browser written in the Java language, supports <APP>, a new HTML tag that allows you to load an applet located at an arbitrary URL and run it locally. With appropriate limitations, this applet has broad access to the resources of the local machine--screen (via the browser window), mouse, keyboard, sound, and network cards. Applets are written in Java and compiled into Java bytecodes. You could write a stand-alone application in Java without involving the Web or HotJava in any way. In short, as Tim Lindholm of Sun said, HotJava is just a novel way of delivering Java applications. For more information on Java, see "Java and Internet Programming," by Arthur van Hoff (DDJ, August 1995); "Net Gets a Java Buzz," by Ray Valdés (Dr. Dobb's Developer Update, August 1995); and "Programming Paradigms," by Michael Swaine (DDJ, October 1995).
Java's raison d'etre is architecture neutrality. The language itself contains no platform dependencies. All types have a fixed size (8/16/32/64 bytes) that may or may not correspond to the norm on whatever platform you're running. But you pay a price for that neutrality. Java and its packages attempt to supply all the mechanisms of the native GUI APIs for systems such as X, Macintosh OS, and Windows through a common syntax. That search for common ground sometimes means throwing out features (the middle and right mouse buttons, for example) not supported on all platforms. Thus, the key to Java's eventual usefulness for developers is not so much its level of functionality (which will always lag behind that of native GUIs), but how many platforms it runs on.
Java makes it possible to quickly write big, portable, robust, graphical, network apps. To illustrate what you can do with Java, I've developed a WAN-based, multiuser game called "Battle of the Java Sea," a variant of the old board game "Battleship." Players on a 40x40 grid fire at each other, scoring points for hits on other players and losing points for being hit.
Battle of the Java Sea contains three parts:
To provide a manageable namespace, Java lumps classes into packages. In the Java source, you can import a package or a single class within a package. The compiler identifies classes as mypackage.myclass. My application defines a new package, Ship, and four classes within it: GameSrv, Ship, Explosion, and PortThread. There is still considerable debate as to the proper use of package and class names, given the distributed nature of the Java/HotJava developer community. The most sensible suggestion I've seen is to incorporate the company and project name into the package name, leaving classes unique on a per-project basis.
For the most part, my program uses only three of the packages delivered with HotJava: awt, browser, and net. awt encompasses all the graphics and drawing functionality, browser contains the applet wrapper class that our applet subclasses, and net implements the socket class that provides our connection to the game server.
C/C++ programmers used to chasing down and trying to prevent memory leaks will enjoy Java's automatic garbage collection. In this program, you'll see plenty of news, but no deletes. There's no need to call the garbage collector in your program, because it runs automatically in a separate thread.
Java requires a novel syntax when declaring arrays. Listing Two shows the declaration of the array of Explosions. The class variable xp is declared as an array of Explosion objects with no dimension. xp exists as a symbol with a type, but its actual Explosion objects are not instantiated until the new statement in the body of the init method. No memory is allocated for an array until it is created with new. In the Java hierarchy, arrays are objects, not simple types, and thus embody more intelligence than C/C++ arrays. All array references are bounds checked, and the length variable gives the size of the array.
Arrays bring up another key feature of Java: Exceptions. The Java language package (java.lang) defines a few dozen Exceptions which have default behaviors and can be caught and thrown as necessary. Bad array references throw an ArrayIndexOutOfBoundException, and bad arguments to the Integer constructor throw a NumberFormatException. You'd do well to understand Exceptions before proceeding with Java coding. I went to considerable trouble in the message-parsing routines (PortThread.java) to avoid throwing a NumberFormatException. A better approach would have been to catch the Exception and deal with the problem then.
In Java, interfaces allow you to define a type of object without subclassing. Listing Three shows the PortThread class, an implementation of the Runnable interface. Unlike a subclass, which attaches all the baggage of the superclass (class variables, un-overridden methods, and so on), an interface only requires the implementation to supply the methods specified by the interface. Unlike classes, interfaces can be multiply inherited. Often the interface system is a much more natural solution. For instance, for debugging purposes, I'd like to read a stream of game-server messages from a file, rather than opening a network socket. In that case, I'd define two new classes, FileStream and SocketStream, and one new interface, MyStream, which would define four methods: open, read, write, and close. FileStream and SocketStream would implement both Runnable (to get their own thread) and MyStream.
The application starts with the GameSrv class which subclasses Applet and implements the Runnable interface. Subclassing Applet allows GameSrv to be loaded as an applet by HotJava. GameSrv's implementation of the Runnable interface's run method and Thread classes' start and stop methods allow GameSrv to run in its own thread. The thread is actually created by allocating a Thread object and passing this as the sole parameter. Then, we can do whatever we want in the run method, in this case repainting the applet window at 100-ms intervals to reflect the changing game state.
GameSrv overrides four Applet methods: mouseUp, mouseMove, keyDown, and update. Of these, the most interesting is update (along with repaint), familiar to Windows coders as the WM_PAINT case in your message-processing switch. As with any GUI app, most of the detail work goes into the paint routine. Applets can override either paint or update to do their painting. If you choose paint, the window is cleared before paint gets called. That was no good for my application, as it gave the window an annoying, flickering appearance. update, on the other hand, leaves all window management up to the programmer, so each time a ship is moved, we have to erase the old ship, and each time a status string is changed, we have to erase the old one. Listing Four shows the update method and one of the paint methods it calls.
The thorniest problem in implementing the update method was a by-product of Java's inherent multithreadedness. In Windows 3.1 SDK programming, you can process your WM_PAINT message without worrying that globals or statics outside the paint routine will change unexpectedly. Not so in a multithreaded environment.
The paintShip method needs to erase the old ship and draw the new ship. This requires three steps: erasing the current ship, painting the new ship, and saving the new ship's coordinates as the current ship's. Listing Five shows the original update method. The keyDown method changes the ship's coordinates by calling Move. The bug in this is that during the X method invocations between clearRect, which erases the current ship, and the call to Ship.setLastLoc, the keyDown method can be invoked, setting LastXLoc and LastYLoc to values other than those at which that ship is currently painted. This occurs because update and keyDown can be called from separate threads. Figure 2 illustrates the problem.
Veteran painters will also notice that update contains an important cheat--it doesn't repaint the background. Were we to use an image background or a color other than the default browser background, we'd have to clear all the background sections of the window. As it is, we get a good visual effect for very little code.
The Applet class provides start and stop methods that are called whenever the applet becomes visible or invisible. Though this version of the game doesn't use them, future versions will skip repainting and stop all network communications when the applet is invisible, to minimize CPU load and network traffic.
The PortThread class, which reads input from the game server, uses the run method a little differently. Given an IP address and port number, the run method creates an instance of a socket and sits in a loop doing blocking reads of the socket's inputStream member. PortThread also illustrates that a Java application doesn't exit until all nondaemon threads are killed. The PortThread thread calls setDaemon so that the app can exit without explicitly killing the PortThread. Listing Two shows the PortThread use of the Runnable interface.
The fixed-message-size, fixed-field-length message protocol I started with was surprisingly painful to implement, mostly due to the difficulty of creating the necessary Java object from an array of bytes. I chose that message style because I've implemented it a hundred times in C. Having done it once in Java, I'll never do it again. There are no pointers in Java and you can't cast between different types, including char (16-bit Unicode) and byte (8 bit). Whereas in C I'd have written a couple of memmoves (with appropriate casts), in Java I had to create various objects from copied sections of the array. The next version will undoubtedly go with a variable-message-length, variable-field-length, character-delimited protocol. This will complicate the socket-reading routine a little, but will allow me to use the supplied StringTokenizer class to parse the message.
For someone who's built GUI apps with Java before, one of the greatest temptations is to just go wild--creating windows left and right, changing the menu bar, and so on--but you're often limited by the visibility (or lack thereof) of variables. For instance, to change the menu bar, you need access to mbar in browser .hotjava. Access to such components is an architectural issue within HotJava that is not really settled yet.
Since there's no Java debugger yet, debugging Java code is problematic--you're left with the tried and true "print to standard out" style. Java encapsulates some of the common system functions in a System object, so a module under development will be sprinkled with System.out.println() calls.
In general, the Java-language documentation is very good. The class documentation, on the other hand, is very frustrating. I often followed a package-class-method documentation trail that left me at a page that contained only the method's name. You really have to follow the mailing lists (including the archives and the soon-to-be-created HotJava newsgroup) to get the most out of the Java/HotJava class packages. That said, for anyone who's spent time writing C++ code for the current crop of GUIs, awt's classes will seem like a very intuitive and straightforward abstraction of the native GUI capabilities.
I built Ship using HotJava Alpha 2 running under Windows NT 3.51. While Java itself is relatively stable and well mannered, HotJava and the awt package are still moving targets. There is already an Alpha 3 running under Sun Solaris, and ports to Windows 95 and the Macintosh should be available as you read this. If you have HotJava, you can run Battle of the Java Sea by checking into http://www.channel1.com/users/ajrodley.
Special thanks to Dennis Foley for his help with this article.
Figure 1:The various pieces of a Java application and where they run. Figure 2: Calling update and keyDown from separate threads could cause an error.
<!doctype html public "-//IETF//DTD HTML//EN"> <HTML> <HEAD> <TITLE>Battleship</TITLE> <META NAME="GENERATOR" CONTENT="Internet Assistant for Word 1.0Z"> <META NAME="AUTHOR" CONTENT="John Rodley"> </HEAD> <BODY> <H1>Battleship</H1> <P> Naval free-for-all. You're the green square, everyone else is red. Place the mouse over your opponent and hit <space> to fire.<HR> <P> <APP class="GameSrv"> <HR> <ADDRESS> <APP class = "Scores"> </ADDRESS> <HR> <P> Click <A HREF="GameSrv.java">here</A> to see the Battleship Java applet source. <P> Click <A HREF="Scores.java">here</A> to see the Scoring applet source. <P> Click <A HREF="socket.c">here</A> to see the game server C source.<HR> <ADDRESS> <A href="http://www.channel1.com/ajrodley"><IMG SRC="images/jvr.gif"></A> John Rodley - john.rodley@channel1.com </ADDRESS> </BODY> </HTML>
public class GameSrv extends Applet implements Runnable {
...
Explosion xp[] = new Explosion[0];
...
public void init() {
...
Explosion axp[] = new Explosion[StartAmmo];
xp = axp;
for( i = 0; i < StartAmmo; i++ )
xp[i] = new Explosion();
...
package ship;
import awt.*;
import java.util.*;
import java.io.*;
import net.*;
import browser.*;
import browser.audio.*;
import ship.*;
// PortThread class - implements a socket reading daemon thread class
public class PortThread extends Thread {
public Socket s; // Our connection to the server
GameSrv g; // The game server that created us
public String MyID;
public final String ServerID = new String( "0001" );
// The maximum number of messages we can read at one time.
int maxMessages = 100;
// The fixed size of the messagr
int messagesize = 25;
public PortThread( GameSrv game ) {
// ID gets set to 0000 initially, then game server sets it to > 1
MyID = new String( "0000" );
// Save this so that we can do callbacks to our game
g = game;
// Turn this into a daemon thread so that the program
// doesn't hang on exit waiting for this thread to terminate
setDaemon( true );
}
public void run() {
s = new Socket( "0.0.0.0", 1099 );
sendStart( g.Mine.getXLoc(), g.Mine.getYLoc());
int ret = 0;
byte buffer[] = new byte[maxMessages*messagesize];
while( ret != -1 )
{
ret = s.inputStream.read( buffer );
if( ret > 0 )
{
parse( buffer, ret );
}
}
}
// sendStart - this is the message we send to the server to tell him we're
// here. The server responds with the ID__ message that tells us what
// ID we need to prepend to all our send messages
public void sendStart( int x, int y ) {
String Xm = new String( "00" + x );
System.out.println( "Xm = " + Xm );
String Ym = new String( "00" + y );
System.out.println( "Ym = " + Ym );
int xStart = Xm.length() - 3;
int yStart = Ym.length() - 3;
System.out.println( "xStart = " + xStart + " yStart = " + yStart );
String Msg = new String( MyID + ":" + "NEW_:" + "+" +
Xm.substring(xStart) + "," + "+" +
Ym.substring( yStart ) + ": " );
System.out.println( "Msg = " + Msg );
byte msg[] = new byte[messagesize];
Msg.getBytes( 0, messagesize-1, msg, 0 );
msg[messagesize-1] = 0;
s.outputStream.write( msg );
}
// sendMove - tell the game server that we moved x byte right and y bytes up
public void sendMove( int x, int y ) {
byte msg[] = new byte[messagesize];
msg[messagesize-1] = 0;
String Xm = new String( "00" + x );
String Ym = new String( "00" + y );
int xStart = Xm.length() - 3;
int yStart = Ym.length() - 3;
String Msg = new String( MyID + ":" + "MOVE:" + "+" +
Xm.substring(xStart) + "," + "+" +
Ym.substring( yStart ) + ": " );
Msg.getBytes( 0, messagesize-1, msg, 0 );
s.outputStream.write( msg );
}
// sendShot - We took a shot, tell the game server
public void sendShot( int x, int y ) {
byte msg[] = new byte[messagesize];
msg[messagesize-1] = 0;
String Xm = new String( "00" + x );
String Ym = new String( "00" + y );
int xStart = Xm.length() - 3;
int yStart = Ym.length() - 3;
String Msg = new String( MyID + ":" + "FIRE:" + "+" +
Xm.substring( xStart ) + "," + "+" +
Ym.substring(yStart ) + ": " );
Msg.getBytes( 0, messagesize-1, msg, 0 );
s.outputStream.write( msg );
}
// sendHitBy - tell the game server that we got hit by a shot
public void sendHitBy( int x, int y, String whoGotMe ) {
byte msg[] = new byte[messagesize];
msg[messagesize-1] = 0;
String Xm = new String( "00" + x );
String Ym = new String( "00" + y );
int xStart = Xm.length() - 3;
int yStart = Ym.length() - 3;
String Msg = new String( MyID + ":" + "HIT_:" + "+" +
Xm.substring( xStart ) + "," + "+" +
Ym.substring( yStart) + ":" + whoGotMe );
Msg.getBytes( 0, messagesize-1, msg, 0 );
s.outputStream.write( msg );
}
// parse - message format is:
// ID 4 bytes - a string representing integer id of sender 0000 to 9999
// : 1 byte b[4]
// type 4 bytes - either MOVE or FIRE NEW_ or ID______LINEEND____
// colon : 1 byte b[9]
// +/- 1 byte b[10]
// XCoord 3 bytes - 0 padded ASCII number 00 through 99
// coma , 1 byte b[14]
// +/- 1 byte b[15]
// YCoord 3 bytes - 0 padded
// terminator 0 1 byte b[19]
// Example:
// MOVE:+01,+00
// says to move enemy 1 in the x direction
// for now, this is a lossy protocol - if we get out of sync we throw
// out all data until we resync. We can afford to lose the MOVE messages,
// but losing a FIRE message that would have hit us is bad news. Oh
// well, Fog of War and all that ...
public void parse( byte b[], int numbytes ) {
Integer modifier;
for( int i = 0; (i+5) < numbytes; i++ )
{
if( i+messagesize > numbytes )
{
System.out.println("sync error - numbytes "+numbytes );
break;
}
switch( b[i+5] )
{
case 'N': // this is a NEW_ message
case 'M': // this is a MOVE message
case 'F': // this is a FIRE message
int ret = parsePositionMessage(b, i, numbytes);
i += ret;
break;
case 'I': // This is the ID__ message
{
boolean bBadNum = false;
char msg[] = new char[messagesize+1];
for( int j = 0; j < messagesize &&
((i+j) < messagesize); j++ )
msg[j] = (char )b[i+j];
msg[messagesize] = 0;
String s = new String( msg );
System.out.println( s );
char imsg[] = new char[4];
// If data errors go through to the point of
// trying to initialize a java.Integer with
// non-integer data, then we'll throw an
// exception which kills the
// thread--so catch them here
imsg[0] = (char )msg[0];
imsg[1] = (char )msg[1];
imsg[2] = (char )msg[2];
imsg[3] = (char )msg[3];
for( int k = 0; k < 4; k++ )
if( imsg[k] < '0' || imsg[k] > '9' )
bBadNum = true;
if( !bBadNum )
{
MyID = new String( imsg );
System.out.println("ID set to "+ MyID);
}
else
System.out.println( "Bad ID message" );
}
break;
default:
System.out.println( "bad message: " );
break;
}
}
}
// parsePositionMessage - all messages are position messages except ID message
public int parsePositionMessage( byte b[], int i, int numbytes ) {
Integer modifier;
char msg[] = new char[messagesize+1];
for( int j = 0; j < messagesize && ((i+j) < messagesize); j++ )
msg[j] = (char )b[i+j];
msg[messagesize] = 0;
String s = new String( msg );
System.out.println( s );
char xmsg[] = new char[3];
if( msg[10] == '+' )
modifier = new Integer( 1 );
else
modifier = new Integer( -1 );
// If we let data errors go through to the point of
// trying to initialize a java.Integer with non-integer data then we'll
// throw an exception which kills the thread, so catch them here
xmsg[0] = (char )msg[11];
xmsg[1] = (char )msg[12];
xmsg[2] = (char )msg[13];
if( xmsg[0] < '0' || xmsg[0] > '9' )
{
System.out.println( "data error " + xmsg[0] );
return( 1 );
}
if( xmsg[1] < '0' || xmsg[1] > '9' )
{
System.out.println( "data error " + xmsg[1] );
return( 1 );
}
if( xmsg[2] < '0' || xmsg[2] > '9' )
{
System.out.println( "data error " + xmsg[2] );
return( 1 );
}
String xstr = new String(xmsg);
System.out.println( "X = " + xstr );
Integer xMove = new Integer( xstr );
xMove = new Integer( xMove.intValue() * modifier.intValue());
char ymsg[] = new char[3];
if( msg[10] == '+' )
modifier = new Integer( 1 );
else
modifier = new Integer( -1 );
ymsg[0] = (char )msg[16];
ymsg[1] = (char )msg[17];
ymsg[2] = (char )msg[18];
if( ymsg[0] < '0' || ymsg[0] > '9' )
return( 1 );
if( ymsg[1] < '0' || ymsg[1] > '9' )
return( 1 );
if( ymsg[2] < '0' || ymsg[2] > '9' )
return( 1 );
String ystr= new String(ymsg);
System.out.println( "Y = " + ystr );
if( msg[15] == '+' )
modifier = new Integer( 1 );
else
modifier = new Integer( -1 );
Integer yMove = new Integer( ystr );
yMove = new Integer( yMove.intValue() * modifier.intValue());
if( b[i+5] == 'M' )
g.moveTheirs( xMove.intValue(), yMove.intValue());
if( b[i+5] == 'F' )
{
char id[] = new char[4];
for( int j = 0; j < 4; j++ )
id[j] = b[i+j];
String FromID = new String( id );
g.shotByLandedAt(FromID, xMove.intValue(),yMove.intValue());
}
if( b[i+5] == 'N' )
g.newEnemy( xMove.intValue(),yMove.intValue());
return( messagesize-1 );
}
}
/**
* Paint it.
*/
public void update(Graphics g) {
if( bGameGoing == true )
{
paintBorder( g );
paintShip( g, Theirs );
paintShip( g, Mine );
paintExplosions( g, txp );
paintExplosions( g, xp );
paintStatusStrings( g );
}
}
public void paintShip( Graphics g, Ship s ) {
Image shipImage;
Color MyColor = new Color( g.wServer, 0, 255, 0 );
Color TheirColor = new Color( g.wServer, 255, 0, 0 );
// Do everything having to do with position. We have to get loc and
// lastloc and reset lastloc right here together because we can get a
// keypress (which changes ship.loc) while this function is executing
// which will cause orphan/ghost ships to remain on the screen.
// Theoretically, we could still get a keypress amongst those four calls
// that would screw things up, but the chances are dramatically reduced.
int xLoc = s.getXLoc();
int yLoc = s.getYLoc();
int LastXLoc = s.getLastXLoc();
int LastYLoc = s.getLastYLoc();
s.setLastLoc();
g.clearRect(leftMargin + (LastXLoc*GridSize)-(ImageSize-GridSize)/2,
topMargin+(LastYLoc*GridSize)-(ImageSize-GridSize)/2,
ImageSize,ImageSize);
if( s == Mine )
{
g.setForeground( MyColor );
shipImage = shipImages[Mine.direction];
}
else
{
g.setForeground( TheirColor );
shipImage = shipImages[Theirs.direction];
}
g.drawImage( shipImage,leftMargin +(xLoc*GridSize)-
(ImageSize-GridSize)/2,topMargin+(yLoc*GridSize)-
(ImageSize-GridSize)/2 );
if( s == Mine )
{
g.setForeground( MyColor );
int sw = g.drawStringWidth( "Position: ", leftMargin + width +
GridSize+scoreMargin, topMargin+(height*6)/6);
if( LastXLoc != xLoc || LastYLoc != yLoc )
else
{
g.setForeground( TheirColor );
shipImage = shipImages[Theirs.direction];
}
g.drawImage( shipImage,leftMargin +(xLoc*GridSize)-
(ImageSize-GridSize)/2,topMargin+(yLoc*GridSize)-
(ImageSize-GridSize)/2 );
if( s == Mine )
{
g.setForeground( MyColor );
int sw = g.drawStringWidth( "Position: " , leftMargin +
width+GridSize+scoreMargin, topMargin+(height*6)/6);
if( LastXLoc != xLoc || LastYLoc != yLoc )
g.clearRect( leftMargin + width+GridSize+scoreMargin+
sw, topMargin+((height*6)/6)-statusHeight,
statusWidth-sw-scoreMargin, statusHeight );
g.drawStringWidth( "" + xLoc + ":" + yLoc,leftMargin +
width+GridSize+scoreMargin+sw,topMargin+(height*6)/6);
}
}
/** Paint it.***/
public void update(Graphics g) {
if( bGameGoing == true )
{
paintBorder( g );
paintShip( g, Theirs );
paintShip( g, Mine );
paintExplosions( g, txp );
paintExplosions( g, xp );
paintStatusStrings( g );
}
}
public void paintShip( Graphics g, Ship s ) {
Image shipImage;
Color MyColor = new Color( g.wServer, 0, 255, 0 );
Color TheirColor = new Color( g.wServer, 255, 0, 0 );
int xLoc = s.getXLoc();
int yLoc = s.getYLoc();
int LastXLoc = s.getLastXLoc();
int LastYLoc = s.getLastYLoc();
g.clearRect(leftMargin + (LastXLoc*GridSize)-(ImageSize-GridSize)/2,
topMargin+(LastYLoc*GridSize)-(ImageSize-GridSize)/2,
ImageSize,ImageSize);
if( s == Mine )
{
g.setForeground( MyColor );
shipImage = shipImages[Mine.direction];
}
else
{
g.setForeground( TheirColor );
shipImage = shipImages[Theirs.direction];
}
g.drawImage( shipImage,leftMargin +(xLoc*GridSize)-
(ImageSize-GridSize)/2,topMargin+(yLoc*GridSize)-
(ImageSize-GridSize)/2);
...
s.setLastLoc();
}
Copyright © 1995, Dr. Dobb's Journal