A
Web application
Now
let’s consider creating an application to run on the Web, which will show
Java in all its glory. Part of this application will be a Java program running
on the Web server, and the other part will be an applet
that’s downloaded to the browser. The applet collects information from
the user and sends it back to the application running on the Web server. The
task of the program will be simple: the applet will ask for the email address
of the user, and after verifying that this address is reasonably legitimate (it
doesn’t contain spaces, and it does contain an ‘@’ symbol)
the applet will send the email address to the Web server. The application
running on the server will capture the data and check a data file in which all
of the email addresses are kept. If that address is already in the file, it
will send back a message to that effect, which is displayed by the applet. If
the address isn’t in the file, it is placed in the list and the applet is
informed that the address was added successfully.
Traditionally,
the way to handle such a problem is to create an HTML
page with a text field and a “submit” button. The user can type
whatever he or she wants into the text field, and it will be submitted to the
server without question. As it submits the data, the Web page also tells the
server what to do with the data by mentioning the Common
Gateway Interface (CGI) program that the server should run after receiving this
data. This CGI program is typically written in either Perl or C (and sometimes
C++, if the server supports it), and it must handle everything. First it looks
at the data and decides whether it’s in the correct format. If not, the
CGI program must create an HTML page to describe the problem; this page is
handed to the server, which sends it back to the user. The user must then back
up a page and try again. If the data is correct, the CGI program opens the data
file and either adds the email address to the file or discovers that the
address is already in the file. In both cases it must format an appropriate
HTML page for the server to return to the user.
As
Java programmers, this seems like an awkward way for us to solve the problem,
and naturally, we’d like to do the whole thing in Java. First,
we’ll use a Java applet to take care of data validation at the client
site, without all that tedious Web traffic and page formatting. Then
let’s skip the Perl CGI script in favor of a Java application running on
the server. In fact, let’s skip the Web server altogether and simply make
our own network connection from the applet to the Java application on the server!
As
you’ll see, there are a number of issues that make this a more
complicated problem than it seems. It would be ideal to write the applet using
Java 1.1
but that’s hardly practical. At this writing, the number of users running
Java 1.1-enabled browsers is small, and although such browsers are now commonly
available, you’ll probably need to take into account that a significant
number of users will be slow to upgrade. So to be on the safe side, the applet
will be programmed using only Java 1.0
code. With this in mind, there will be no JAR files to combine
.class
files in the applet, so the applet should be designed to create as few
.class
files as possible to minimize download time.
Well,
it turns out the Web server (the one available to me when I wrote the example)
does
have Java in it, but only Java 1.0!
So the server application must also be written using Java 1.0.
The
server application
Now
consider the server application, which will be called
NameCollector.
What happens if more than one user at a time tries to submit their email
addresses? If
NameCollector
uses TCP/IP sockets, then it must use the multithreading approach shown earlier
to handle more than one client at a time. But all of these threads will try to
write to a single file where all the email addresses will be kept. This would
require a locking mechanism to make sure that more than one thread
doesn’t access the file at once. A semaphore will do the trick, but
perhaps there’s a simpler way.
If
we use datagrams instead, multithreading is unnecessary. A single datagram
socket will listen for incoming datagrams, and when one appears the program
will process the message and send the reply as a datagram back to whomever sent
the request. If the datagram gets lost, then the user will notice that no reply
comes and can then re-submit the request.
When
the server application receives a datagram and unpacks it, it must extract the
email address and check the file to see if that address is there already (and
if it isn’t, add it). And now we run into another problem. It turns out
that Java 1.0 doesn’t quite have the horsepower to easily manipulate the
file containing the email addresses (Java 1.1
does). However, the problem can be solved in C quite readily, and this will
provide an excuse to show you the easiest way to connect
a non-Java program to a Java program. A Runtime
object for a program has a method called exec( )
that will start up a separate program on the machine and return a Process
object. You can get an OutputStream
that connects to standard input for this separate program and an InputStream
that connects to standard output. All you need to do is write a program using
any language that takes its input from standard input and writes the output to
standard output. This is a convenient trick when you run into a problem that
can’t be solved easily or quickly enough in Java (or when you have legacy
code you don’t want to rewrite). You can also use Java’s
native
methods
(see Appendix A) but those are much more involved.
The
C program
The
job of this non-Java application (written in C because Java wasn’t
appropriate for CGI programming; if nothing else, the startup time is
prohibitive) is to manage the list of email addresses. Standard input will
accept an email address and the program will look up the name in the list to
see if it’s already there. If not, it will add it and report success, but
if the name is already there then it will report that. Don’t worry if you
don’t completely understand what the following code means; it’s
just one example of how you can write a program in another language and use it
from Java. The particular programming language doesn’t really matter as
long as it can read from standard input and write to standard output.
//: Listmgr.c
// Used by NameCollector.java to manage
// the email list file on the server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BSIZE 250
int alreadyInList(FILE* list, char* name) {
char lbuf[BSIZE];
// Go to the beginning of the list:
fseek(list, 0, SEEK_SET);
// Read each line in the list:
while(fgets(lbuf, BSIZE, list)) {
// Strip off the newline:
char * newline = strchr(lbuf, '\n');
if(newline != 0)
*newline = '\0';
if(strcmp(lbuf, name) == 0)
return 1;
}
return 0;
}
int main() {
char buf[BSIZE];
FILE* list = fopen("emlist.txt", "a+t");
if(list == 0) {
perror("could not open emlist.txt");
exit(1);
}
while(1) {
gets(buf); /* From stdin */
if(alreadyInList(list, buf)) {
printf("Already in list: %s", buf);
fflush(stdout);
}
else {
fseek(list, 0, SEEK_END);
fprintf(list, "%s\n", buf);
fflush(list);
printf("%s added to list", buf);
fflush(stdout);
}
}
} ///:~
This
assumes that the C compiler accepts
‘//’
style comments. (Many do, and you can also compile this program with a C++
compiler.) If yours doesn’t, simply delete those comments.
The
first function in the file checks to see whether the name you hand it as a
second argument (a pointer to a
char)
is
in the file. Here, the file is passed as a
FILE
pointer to an already-opened file (the file is opened inside
main( )).
The function
fseek( )
moves around in the file; here it is used to move to the top of the file.
fgets( )
reads a line from the file
list
into the buffer
lbuf,
not exceeding the buffer size
BSIZE.
This is inside a
while
loop
so that each line in the file is read. Next,
strchr( )
is used to locate the newline character so that it can be stripped off. Finally,
strcmp( )
is
used to compare the name you’ve passed into the function to the current
line int the file.
strcmp( )
returns zero if it finds a match. In this case the function exits and a one is
returned to indicate that yes, the name was already in the list. (Note that the
function returns as soon as it discovers the match, so it doesn’t waste
time looking at the rest of the list.) If you get all the way through the list
without a match, the function returns zero.
In
main( ),
the file is opened using
fopen( ).
The first argument is the file name and the second is the way to open the file;
a+
means “Append, and open (or create if the file does not exist) for update
at the end of the file.” The
fopen( )
function returns a
FILE
pointer which, if it’s zero, means that the open was unsuccessful. This
is dealt with by printing an error message with
perror( )
and terminating the program with
exit( ). Assuming
that the file was opened successfully, the program enters an infinite loop. The
function call
gets(buf)
gets a line from standard input (which will be connected to the Java program,
remember) and places it in the buffer
buf.
This is simply passed to the
alreadyInList( )
function, and if it’s already in the list,
printf( )
sends that message to standard output (where the Java program is listening).
fflush( )
is a way to flush the output buffer.
If
the name is not already in the list,
fseek( )
is used to move to the end of the list and
fprintf( )
“prints” the name to the end of the list. Then
printf( )
is used to indicate that the name was added to the list (again flushing
standard output) and the infinite loop goes back to waiting for a new name.
Remember
that you usually cannot compile this program on your computer and load it onto
the Web server machine, since that machine might use a different processor and
operating system. For example, my Web server runs on an Intel processor but it
uses Linux, so I must download the source code and compile using remote
commands (via telnet) with the C compiler that comes with the Linux distribution.
The
Java program
This
program will first start the C program above and make the necessary connections
to talk to it. Then it will create a datagram socket that will be used to
listen for datagram packets from the applet.
//: NameCollector.java
// Extracts email names from datagrams and stores
// them inside a file, using Java 1.02.
import java.net.*;
import java.io.*;
import java.util.*;
public class NameCollector {
final static int COLLECTOR_PORT = 8080;
final static int BUFFER_SIZE = 1000;
byte[] buf = new byte[BUFFER_SIZE];
DatagramPacket dp =
new DatagramPacket(buf, buf.length);
// Can listen & send on the same socket:
DatagramSocket socket;
Process listmgr;
PrintStream nameList;
DataInputStream addResult;
public NameCollector() {
try {
listmgr =
Runtime.getRuntime().exec("listmgr.exe");
nameList = new PrintStream(
new BufferedOutputStream(
listmgr.getOutputStream()));
addResult = new DataInputStream(
new BufferedInputStream(
listmgr.getInputStream()));
} catch(IOException e) {
System.err.println(
"Cannot start listmgr.exe");
System.exit(1);
}
try {
socket =
new DatagramSocket(COLLECTOR_PORT);
System.out.println(
"NameCollector Server started");
while(true) {
// Block until a datagram appears:
socket.receive(dp);
String rcvd = new String(dp.getData(),
0, 0, dp.getLength());
// Send to listmgr.exe standard input:
nameList.println(rcvd.trim());
nameList.flush();
byte[] resultBuf = new byte[BUFFER_SIZE];
int byteCount =
addResult.read(resultBuf);
if(byteCount != -1) {
String result =
new String(resultBuf, 0).trim();
// Extract the address and port from
// the received datagram to find out
// where to send the reply:
InetAddress senderAddress =
dp.getAddress();
int senderPort = dp.getPort();
byte[] echoBuf = new byte[BUFFER_SIZE];
result.getBytes(
0, byteCount, echoBuf, 0);
DatagramPacket echo =
new DatagramPacket(
echoBuf, echoBuf.length,
senderAddress, senderPort);
socket.send(echo);
}
else
System.out.println(
"Unexpected lack of result from " +
"listmgr.exe");
}
} catch(SocketException e) {
System.err.println("Can't open socket");
System.exit(1);
} catch(IOException e) {
System.err.println("Communication error");
e.printStackTrace();
}
}
public static void main(String[] args) {
new NameCollector();
}
} ///:~
The
first definitions in
NameCollector
should look familiar: the port is chosen, a datagram packet is created, and
there’s a handle to a
DatagramSocket.
The next three definitions concern the connection to the C program: a
Process
object is what comes back when the C program is fired up by the Java program,
and that
Process
object produces the
InputStream
and
OutputStream
objects representing, respectively, the standard output and standard input of
the C program. These must of course be “wrapped” as is usual with
Java IO, so we end up with a
PrintStream
and
DataInputStream. All
the work for this program happens inside the constructor. To start up the C
program, the current
Runtime
object is procured. This is used to call exec( ),
which returns the
Process
object. You can see that there are simple calls to produce the streams from the
Process
object:
getOutputStream( )
and
getInputStream( ).
From this point on, all you need to consider is sending data to the stream
nameList
and getting the results from
addResult. As
before, a
DatagramSocket
is connected to a port. Inside the infinite
while
loop, the program calls
receive( ),
which blocks until a datagram shows up. When the datagram appears, its contents
are extracted into the
String
rcvd
.
This is trimmed to remove white space at each end and sent to the C program in
the line:
nameList.println(rcvd.trim()); This
is only possible because Java’s
exec( )
provides access to any executable that reads from standard input and writes to
standard output. There are other ways to talk to non-Java code, which are
discussed in Appendix A.
Capturing
the result from the C program is slightly more complicated. You must call read( )
and provide a buffer where the results will be placed. The return value for
read( )
is the number of bytes that came from the C program, and if this value is -1 it
means that something is wrong. Otherwise, the
resultBuf
is turned into a
String
and the spaces are trimmed off. This string is then placed into a
DatagramPacket
as before and shipped back to the same address that sent the request in the
first place. Note that the sender’s address is part of the
DatagramPacket
we received.
Remember
that although the C program must be compiled on the Web server, the Java
program can be compiled anywhere since the resulting byte codes will be the
same regardless of the platform on which the program will be running.
The
NameSender applet
As
mentioned earlier, the applet must be written with Java 1.0
so that it will run on the largest number of browsers, so it’s best if
the number of classes produced is minimized. Thus, instead of using the
Dgram
class developed earlier, all of the datagram
manipulations will be placed in line. In addition, the applet needs a thread to
listen for the reply from the server, and instead of making this a separate
thread it’s integrated into the applet by implementing the Runnable
interface. This isn’t as easy to read, but it produces a one-class (and
one-server-hit) applet:
//: NameSender.java
// An applet that sends an email address
// as a datagram, using Java 1.02.
import java.awt.*;
import java.applet.*;
import java.net.*;
import java.io.*;
public class NameSender extends Applet
implements Runnable {
private Thread pl = null;
private Button send = new Button(
"Add email address to mailing list");
private TextField t = new TextField(
"type your email address here", 40);
private String str = new String();
private Label
l = new Label(), l2 = new Label();
private DatagramSocket s;
private InetAddress hostAddress;
private byte[] buf =
new byte[NameCollector.BUFFER_SIZE];
private DatagramPacket dp =
new DatagramPacket(buf, buf.length);
private int vcount = 0;
public void init() {
setLayout(new BorderLayout());
Panel p = new Panel();
p.setLayout(new GridLayout(2, 1));
p.add(t);
p.add(send);
add("North", p);
Panel labels = new Panel();
labels.setLayout(new GridLayout(2, 1));
labels.add(l);
labels.add(l2);
add("Center", labels);
try {
// Auto-assign port number:
s = new DatagramSocket();
hostAddress = InetAddress.getByName(
getCodeBase().getHost());
} catch(UnknownHostException e) {
l.setText("Cannot find host");
} catch(SocketException e) {
l.setText("Can't open socket");
}
l.setText("Ready to send your email address");
}
public boolean action (Event evt, Object arg) {
if(evt.target.equals(send)) {
if(pl != null) {
// pl.stop(); Deprecated in Java 1.2
Thread remove = pl;
pl = null;
remove.interrupt();
}
l2.setText("");
// Check for errors in email name:
str = t.getText().toLowerCase().trim();
if(str.indexOf(' ') != -1) {
l.setText("Spaces not allowed in name");
return true;
}
if(str.indexOf(',') != -1) {
l.setText("Commas not allowed in name");
return true;
}
if(str.indexOf('@') == -1) {
l.setText("Name must include '@'");
l2.setText("");
return true;
}
if(str.indexOf('@') == 0) {
l.setText("Name must preceed '@'");
l2.setText("");
return true;
}
String end =
str.substring(str.indexOf('@'));
if(end.indexOf('.') == -1) {
l.setText("Portion after '@' must " +
"have an extension, such as '.com'");
l2.setText("");
return true;
}
// Everything's OK, so send the name. Get a
// fresh buffer, so it's zeroed. For some
// reason you must use a fixed size rather
// than calculating the size dynamically:
byte[] sbuf =
new byte[NameCollector.BUFFER_SIZE];
str.getBytes(0, str.length(), sbuf, 0);
DatagramPacket toSend =
new DatagramPacket(
sbuf, 100, hostAddress,
NameCollector.COLLECTOR_PORT);
try {
s.send(toSend);
} catch(Exception e) {
l.setText("Couldn't send datagram");
return true;
}
l.setText("Sent: " + str);
send.setLabel("Re-send");
pl = new Thread(this);
pl.start();
l2.setText(
"Waiting for verification " + ++vcount);
}
else return super.action(evt, arg);
return true;
}
// The thread portion of the applet watches for
// the reply to come back from the server:
public void run() {
try {
s.receive(dp);
} catch(Exception e) {
l2.setText("Couldn't receive datagram");
return;
}
l2.setText(new String(dp.getData(),
0, 0, dp.getLength()));
}
} ///:~
The
UI for the applet is quite simple. There’s a
TextField
in which you type your email address, and a
Button
to send the email address to the server. Two
Labels
are used to report status back to the user.
By
now you can recognize the DatagramSocket,
InetAddress,
buffer, and DatagramPacket
as trappings of the network connection. Lastly, you can see the
run( )
method that implements the thread portion so the applet can listen for the
reply sent back by the server.
The
init( )
method sets up the GUI with the familiar layout tools, then creates the
DatagramSocket
that will be used both for sending and receiving datagrams.
The
action( )
method (remember, we’re confined to Java 1.0
now, so we can’t use any slick inner listener classes) watches only to
see if you press the “send” button. When the button is pressed, the
first action is to check the
Thread
pl
to see if it’s
null.
If it’s not
null,
there’s a live thread running. The first time the message is sent a
thread is started up to watch for the reply. Thus, if a thread is running, it
means this is not the first time the user has tried to send the message. The
pl
handle
is set to
null
and the old listener is interrupted. (This is the preferred approach, since stop( )
is deprecated in Java 1.2
as explained in the previous chapter.)
Regardless
of whether this is the first time the button was pressed, the text in
l2
is erased.
The
next group of statements checks the email name for errors. The String.indexOf( )
method is used to search for illegal characters, and if one is found it is
reported to the user. Note that all of this happens without any network
activity, so it’s fast and it doesn’t bog down the Internet.
Once
the name is verified, it is packaged into a datagram and sent to the host
address and port number in the same way that was described in the earlier
datagram example. The first label is changed to show you that the send has
occurred, and the button text is changed so that it reads
“re-send.” At this point, the thread is started up and the second
label informs you that the applet is waiting for a reply from the server.
The
run( )
method for the thread uses the
DatagramSocket
that lives in
NameSender
to receive( ),
which blocks until the datagram packet comes from the server. The resulting
packet is placed into
NameSender’s
DatagramPacket
dp
.
The data is retrieved from the packet and placed into the second label in
NameSender.
At this point, the thread terminates and becomes dead. If the reply
doesn’t come back from the server in a reasonable amount of time, the
user might become impatient and press the button again, thus terminating the
current thread (and, after re-sending the data, starting a new one). Because a
thread is used to listen for the reply, the user still has full use of the UI.
The
Web page
Of
course, the applet must go inside a Web page. Here is the complete Web page;
you can see that it’s intended to be used to automatically collect names
for my mailing list:
<HTML>
<HEAD>
<META CONTENT="text/html">
<TITLE>
Add Yourself to Bruce Eckel's Java Mailing List
</TITLE>
</HEAD>
<BODY LINK="#0000ff" VLINK="#800080" BGCOLOR="#ffffff">
<FONT SIZE=6><P>
Add Yourself to Bruce Eckel's Java Mailing List
</P></FONT>
The applet on this page will automatically add your email address to the mailing list, so you will receive update information about changes to the online version of "Thinking in Java," notification when the book is in print, information about upcoming Java seminars, and notification about the “Hands-on Java Seminar” Multimedia CD. Type in your email address and press the button to automatically add yourself to this mailing list. <HR>
<applet code=NameSender width=400 height=100>
</applet>
<HR>
If after several tries, you do not get verification it means that the Java application on the server is having problems. In this case, you can add yourself to the list by sending email to
<A HREF="mailto:Bruce@EckelObjects.com">
Bruce@EckelObjects.com</A>
</BODY>
The
applet tag is quite trivial, no different from the first one presented in
Chapter 13.
Problems
with this approach
This
certainly seems like an elegant approach. There’s no CGI programming and
so there are no delays while the server starts up a CGI program. The datagram
approach seems to produce a nice quick response. In addition, when Java 1.1
is available everywhere, the server portion can be written entirely in Java.
(Although it’s quite interesting to see how easy it is to connect to a
non-Java program using standard input and output.)
There
are problems, however. One problem is rather subtle: since the Java application
is running constantly on the server and it spends most of its time blocked in
the Datagram.receive( )
method, there
might
be some CPU hogging going on. At least, that’s the way it appeared on the
server where I was experimenting. On the other hand, there wasn’t much
else happening on that server, and starting the program using
“nice” (a Unix program to prevent a process from hogging the CPU)
or its equivalent could solve the problem if you have a more heavily-loaded
server. In any event, it’s worth keeping your eye on an application like
this – a blocked
receive( )
could hog the CPU.
The
second problem is a show stopper. It concerns firewalls. A firewall
is a machine that sits between your network and the Internet. It monitors all
traffic coming in from the Internet and going out to the Internet, and makes
sure that traffic conforms to what it expects.
Firewalls
are conservative little beasts. They demand strict conformance to all the
rules, and if you’re not conforming they assume that you’re doing
something sinful and shut you out (not quite so bad as the Spanish Inquisition,
but close). For example, if you are on a network behind a firewall and you
start connecting to the Internet using a Web browser, the firewall expects that
all your transactions will connect to the server using the accepted http port,
which is 80. Now along comes this Java applet
NameSender,
which is trying to send a datagram to port 8080, which is way outside the range
of the “protected” ports 0-1024. The firewall naturally assumes the
worst – that someone has a virus – and it doesn’t allow the
transaction to happen.
As
long as your customers have raw connections to the Internet (for example, using
a typical Internet service provider) there’s no problem, but you might
have some important customers dwelling behind firewalls, and they won’t
be able to use your program.
This
is rather disheartening after learning so much Java, because it would seem that
you must give up Java on the server and learn how to write CGI scripts in C or
Perl. But as it turns out, despair is not in order.
One
scenario is part of Sun’s grand scheme. If everything goes as planned,
Web servers will be equipped with servlet
servers
.
These will take a request from the client (going through the firewall-accepted
port 80) and instead of starting up a CGI program they will start up a Java
program called a
servlet.
This is a little application that’s designed to run only on the server. A
servlet server will automatically start up the servlet to handle the client
request, which means you can write all your programs in Java (further enabling
the “100 percent pure Java initiative”). It is admittedly an
appealing idea: once you’re comfortable with Java, you don’t have
to switch to a more primitive language to handle requests on the server.
Since
it’s only for handling requests on the server, the servlet API has no GUI
abilities. This fits quite well with
NameCollector.java,
which doesn’t have a GUI anyway.
At
this writing, a low-cost servlet server was available from
java.sun.com.
In addition, Sun is encouraging other Web server manufacturers to add servlet
capabilities to their servers.