Chapter 12 -- Network Programming with Java
Chapter 12
Network Programming with Java
CONTENTS
Because you're reading this book, you probably don't need to be
told what the Internet is. On the other hand, it's easy to use
the World Wide Web and various high-level features of the Internet
and never think about the amazing kinds of hardware and protocol
standards that are humming away efficiently behind the scenes.
You could fill a library with all the books and papers that have
been written about how the Internet works, but luckily you don't
have to. Java programs typically access network resources at a
very high level of abstraction, and the gory details are largely
hidden from view.
The java.net package contains
the classes that make network programming so easy in Java. These
classes are described in Chapter 3, "An
Introduction to Java Classes," and in the Java API. The examples
in this chapter make heavy use of them.
The Internet started out around 1969 as a kind of science fair
project by the U.S. military, and it was known as the ARPANET.
That network no longer exists, but various protocols that were
invented for it are still in use. These protocols allow a collection
of physical networks to link up over gateways and become a sort
of internetwork, or Internet. These physical networks generally
are Ethernet LANs, dial-up lines, and so on. The Internet
is a logical network cobbled together out of various physical
networks that cooperate so they appear to the user as one large
network.
This vague definition leaves undefined the word cooperate,
which is where things get slightly complicated. The various protocols
that implement this cooperation have acronymic titles such as
IP (Internet Protocol), TCP (Transfer Control Protocol),
UDP (User Datagram Protocol), ICMP (Internet Control
Message Protocol), ARP (Address Resolution Protocol),
and so on. You certainly can tell it's a defense department project!
As a user of the Internet, you need only a basic understanding
of what IP, TCP, and UDP can do for you, as well as a nodding
acquaintance with DNS domain names. The other low-level protocols
are of interest mainly to network administrators, hackers, and
generally anybody with just too much time on their hands.
The Internet is the Frankenstein monster of packet-switching
networks. Packet switching means that data is sent down a wire
(or other medium) in little packets (or datagrams) with a destination
address and control fields slapped on the front. It's sort of
like the postal system; you have to address your letters correctly.
You've probably seen IP addresses hanging around on the Internet,
looking like 123.45.67.89
or something similar (this is called dotted quad notation).
These addresses are 32-bit numbers that identify a particular
network device on the Internet (your Ethernet card, for example),
via a mapping known as Address Resolution Protocol (ARP).
The Internet Protocol (IP) defines the basics of how datagrams
are addressed and routed, as well as who receives or forwards
them.
Whatever you might think of the post office, it at least tries
to get your letter delivered reliably. The IP protocol makes no
such guarantee. Nor can it; the underlying hardware is intentionally
made to be occasionally unreliable in the interests of speed and
economy of design. Packets can be dropped (intentionally!) without
notice; they may arrive out of sequence or with their data garbled.
Techno-geeks sum this up by saying that IP lives on the network
layer. In a conceptual tower of ISO networking layers, this
is the second floor; it assumes only that there is a way of sending
packets down the wire to a specific destination. It's up to the
higher layers to provide as little or as much error control and
recovery as necessary.
The next level up from the network layer is called the transport
layer. This layer defines end-to-end communications in two
flavors: UDP and TCP. An endpoint for communication is defined
by a socket, which is the conjunction of an IP address
and a port number. A port number is a positive integer
that identifies a logical "port" on your machine. These
ports are not physical entities; they simply distinguish data
arriving over a single physical connection so that it can be delivered
to any of hundreds of different "sockets" owned by the
various applications on the machine. This provides for a virtually
limitless number of end-to-end connections through a single network
interface.
The User Datagram Protocol (UDP) lets you send datagrams
of your own to another host and port number. Essentially, you
are just using IP, but with the addition of port numbers and with
the various details of IP headers and such hidden from view. Just
as with IP, there is no guarantee of delivery, error detection,
or sequencing of datagrams.
If you want to use UDP from Java, the java.net.DatagramSocket
and java.net.DatagramPacket
classes are what you need. Examples of their use follow.
The Transfer Control Protocol (TCP) slaps a reliable and
sequenced connection on top of the unreliable, unsequenced functionality
of IP. This is why you often will see the whole setup referred
to as TCP/IP. A TCP socket is again the conjunction
of an IP address with a port number. The data you send over a
TCP socket is divvied up into datagrams and stamped with sequence
numbers and error-detection codes. At the receiving end, the packets
are checked for errors and then lined up in sequence order. Protocol
Packets are resent as necessary, so that the whole stream of bytes
arrives intact at the receiving end. It's kind of like registered
mail.
Because of the reliable and sequenced nature of TCP sockets, they
often are called stream sockets; you can read and write
data in continuous streams of bytes without worrying about packets,
headers, and so on. Because streams figure so prominently in the
java.io classes, it's logical
to suppose that TCP sockets are a natural in Java-and they are!
Stream socket functionality in Java is provided by the classes
java.net.ServerSocket and
java.net.Socket.
Not really a protocol, but more like an entire subsystem of the
Internet, the Domain Name Service (DNS) makes it easier on the
carbon unit peripherals (humans!) when it comes to remembering
IP addresses. DNS is a collection of programs and protocols that
allow a central authority to assign symbolic names to Internet
hosts based on a hierarchy of domains and subdomains. It's a huge,
distributed database of nicknames-one for every Internet host
registered in a domain. Suppose that you work at Widgets Galore
Company, and your machine has the nickname wallaby.
Your DNS name will look like this:
wallaby.WidgetsGalore.com
The .com ending signifies
a commercial institution. Reading from right to left, you move
from the general to the specific in your DNS hierarchy: not just
any commercial interest, but one called WidgetsGalore.
Not any machine at Widgets Galore, but the one known as wallaby.
The left-most word is always the local host name for the machine
in question. The remaining words specify the domain of that machine.
There may be other machines in the world named wallaby,
but not in your domain.
The name servers and resolver libraries on the various hosts in
your domain know how to answer queries about wallaby; they return
the actual IP address of your machine-a 32-bit number. From the
user's point of view, this all happens more or less automatically,
which lets us poor humans forget about memorizing 32-bit IP addresses
and get on with memorizing important stuff, like 20-bit phone
numbers.
In Java, IP addresses are encapsulated in the class java.net.InetAddress,
whether they are given as symbolic DNS names or 32-bit numbers
in dotted quad form.
For many people, the World Wide Web is the Internet. It
certainly is the most painless and fun way to be a virtual tourist,
zooming around looking at pictures of people you've never met
and places you'd like to visit. It's also full of ways to search
for information on any topic whatsoever. Most importantly for
Java enthusiasts, the WWW is the natural habitat for Java applets.
Applets are embedded in Web pages using the APPLET
tag. When you tell your Web browser to load a certain Web page,
it opens a socket connection to a Web server specified in the
URL of the page. (A URL is a Uniform Resource Locator-the
address of a resource on the Web. For a general discussion of
URLs, see java.net.URL in
Chapter 3.) The Web server then writes
the contents of the page over the socket connection. Your browser
decodes the contents and then displays them according to their
type. If your browser understands applets, it reads the APPLET
tag to figure out what additional data it must request from the
Web server. This data consists of the Java class files defining
your applet, which your browser then must load and execute. A
Java-compatible Web browser knows how to be a Java virtual machine.
The language of the Web is HyperText Transfer Protocol (HTTP).
The browser sends HTTP requests, such as GET
and POST, to the server,
and the server responds with an HTTP header and body. The header
defines (among other things) the content type and content encoding
of the body. There are many standard types and encodings, which
are known as MIME types. The MIME type for standard Web pages
is text/html, which also
is known as HyperText Markup Language (html). I'm not going to
talk much about html because you know all about that anyway.
Java allows for flexible interaction with Web servers, including
the GET and POST
methods of HTTP. But the Web is more than hypertext alone; a URL
can specify any one of a number of different protocols to be used
for fetching a resource. These include FTP, Gopher, NNTP (Usenet
news), and so on. Better yet, you can teach Java about these or
any protocol because the way in which java.net.URL
objects interpret their contents is fully extensible, via the
classes java.net.URLStreamHandler
and java.net.ContentHandler
(and other related classes).
To open a socket in Java, you specify an IP address and a port
number. IP addresses in Java are represented by the class java.net.InetAddress,
which doesn't care whether you want to specify the dotted quad
or the DNS name of a host. You can get an InetAddress
for the fictional machine wallaby.WidgetsGalore.com,
for example, by using the static method getByName():
InetAddress wally = InetAddress.getByName("wallaby.WidgetsGalore.com");
On the other hand, you could give the string in dotted quad form:
InetAddress wally = InetAddress.getByName("123.45.67.89");
It really doesn't matter. To get the address of the local host
(the machine you're running on), you can pass the null
reference to getByName(),
or you can use InetAddress.getLocalHost().
These methods throw an UnknownHostException
if (you guessed it!) the host is not known to DNS. An InetAddress
object has a constant value; you can't change it, so just throw
it away when you're done with it.
An InetAddress alone does
not make a socket. You must have a port number. In a typically
asymmetric client/server application, only the server side needs
to worry about which port number it uses, because only the server
side needs to be found at a well-known socket location. So, you
can specify a port number or use the magic port number 0, which
means, give me any available port. If you ask for a port that
is already in use, you trigger an exception. Users generally should
avoid using ports 1 through 1024 because these are reserved for
system-based services.
The handiest sockets for use in Java are TCP stream sockets. For
this reason, they receive prime consideration in Java's naming
scheme, and they simply are called java.net.Socket.
A Socket's whole raison
d'etre is to be connected to another Socket,
so think about how this happens. In order to get a connection
happening between two machines, one of them has to ask first,
or initiate the connection. This machine is playing the role of
client, and the one who answers is a server (these distinctions
are a bit arbitrary-after the connection is established, it is
symmetrical).
ServerSocket
If the client asks for a connection, somebody had better be listening
on the server end. This somebody is a java.net.ServerSocket-an
object that creates a passive socket on your local host and then
sits and listens to a specific port. Listing 12.1 is a fragment
containing a very minimal server class, which accepts connections
and then does nothing with them. The main()
method calls the handle_connection()
method to do any actual work with the socket.
Listing 12.1. A minimal server.
public class SillyServer
{
// A bare-bones example: exception handling omitted!
public static void main( String args[] )
{
ServerSocket serv;
serv = new ServerSocket( 8081 );
while ( true ) {
Socket s;
s = serv.accept(); //
Wait for a connection
handle_connection( s ); //
Got one, now do something!
s.close();
}
}
}
This code fragment just listens forever on port number 8081 until
a connection is requested and then calls the method handle_connection()
to deal with the new connection (the name handle_connection()
is arbitrary here; it serves to describe a place to put your application-specific
code). The accept() method
blocks forever until a connection is received, at which point
it returns a new Socket object
representing the connection; you should close the Socket
when you're done with it. After accepting a connection, the server
socket returns to its listening state, and you can accept()
further connections on the same ServerSocket
as often as you want.
The infinite looping behavior in this server program is typical.
After all, you want servers to hang around and service any number
of requests. In case another request for a connection arrives
before you have a chance to accept it, it is kept waiting in a
queue of pending connections. You can specify the length of this
queue by an optional second argument to the ServerSocket
constructor. It typically defaults to five. When the queue is
full of unserviced connections, further connections are refused.
In practice, though, handling requests in a single thread is not
a good idea unless each connection can be handled very quickly.
Most real-life server programs should start a new thread for each
connection accepted.
A connected Socket always
knows the address and port of the remote socket it's connected
to; these are made available by the Socket
methods getInetAddress()
and getPort(). This allows
your server to reject certain connections out of hand; just close
the Socket if you don't like
the client's address.
Socket
On the client's end, you need to ask the server for a connection.
This is done with a java.net.Socket
object, which you construct yourself, as shown in Listing 12.2.
Listing 12.2. A minimal client.
import java.io.*;
import java.net.*;
public class SillyClient
{
Socket sock;
DataInputStream in;
DataOutputStream out;
public SillyClient( String server, int port ) throws
IOException
{
// Create a socket connected to the server:
sock = new Socket( server, port );
// Okay, now attach some streams to it.
in = new DataInputStream( sock.getInputStream()
);
out = new DataOutputStream( sock.getOutputStream()
);
}
}
Notice how the client needs to know the exact port number of the
server's socket as well as the IP address. You can specify the
address as a String or as
an InetAddress instance.
If the Socket constructor
succeeds (throws no exception), you're connected! Also, you never
specify which local port is to be used. For the client end, it
simply doesn't matter beforehand which port you are using. After
connecting, you can find out the local port number by calling
getLocalPort().
These are supposed to be stream sockets, so there had better be
a way to get a stream attached to them. The methods getInputStream()
and getOutputStream() do
just that. They return an object of class InputStream
or OutputStream, which allows
you to read or write using the given socket. A socket can have
a stream of either type or both attached to it; sockets provide
a two-way flow of information. When you're done with the stream,
it's good programming practice to close()
it before you close the associated socket. If you use a form of
BufferedOutputStream, you
might want to explicitly flush()
it on occasion; it certainly never hurts to do so.
For security reasons, applets usually are allowed to connect only
to sockets that live at the same IP address as their own class
files. This prevents various antisocial behaviors, but it also
puts a real crimp in what you can do from a typical Web browser
environment. An applet can find out the InetAddress
of its home machine by examining its getCodeBase()
URL. But keep your applets portable-don't hard code the IP address!
These restrictions don't apply to stand-alone programs (unless
your SecurityManager is quite
paranoid).
A Basic Client/Server Applet
You'll now do a simple client/server transaction. The server will
be a stand-alone program, listening on a specific port on the
machine wallaby.WidgetsGalore.com,
and the client will be an applet. The client calls up the server,
and the server prints a short sales report back to the client.
Then the transaction is complete. For this, you just need to flesh
out the SillyServer class
used earlier-call it SimpleServer,
as shown in Listing 12.3.
Listing 12.3. A simple server program.
import java.io.*;
import java.net.*;
public class SimpleServer
{
static final int DEFAULT_PORT =
8081;
static void handle_connection( Socket s ) throws IOException
{
PrintStream out = new PrintStream(s.getOutputStream());
DataInputStream in = new DataInputStream(new
FileInputStream( "sales.txt" ));
String line;
System.err.println( "Connection from
" + s.getInetAddress() );
while ( (line = in.readLine()) != null
)
out.println( line );
in.close();
out.close();
}
public static void main( String args[] )
{
int port;
ServerSocket serv;
if (args.length == 1)
port = new Integer(args[0]).intValue();
else
port = DEFAULT_PORT;
try {
serv = new ServerSocket( port
);
} catch ( IOException e ) {
System.err.println( "I/O
exception: " + e );
return;
}
System.err.println( "listening on
port " + port );
while ( true ) {
Socket s = null;
try {
s = serv.accept(); //
Wait for a connection
handle_connection(
s ); // Got
one, now do something!
} catch ( IOException e )
{
System.err.println(
"I/O exception: " + e );
} finally {
if (s!=null) try
{ s.close(); } catch (IOException e) {}
}
}
}
}
Notice that the server program still does everything sequentially
in a single thread. You can get away with this only because the
entire transaction lasts just a fraction of a second.
The corresponding applet, SimpleClient,
has a button named load, which you click to open a socket and
copy the data to a TextArea.
Listing 12.4 shows the important parts of the applet code.
Listing 12.4. A simple client applet.
public class SimpleClient extends Applet
{
String server;
int port;
Label title = new Label( "A Simple Client Applet",
Label.CENTER );
TextArea text = new TextArea();
Button load = new Button( "Load Sales Report"
);
public void init()
{
// Initialize the applet ...
}
void loadSales()
{
Socket s = null;
try {
s = new Socket( server, port
);
DataInputStream in = new DataInputStream(s.getInputStream());
String line;
while ( ( line = in.readLine()
) != null )
text.appendText(
line + "n" );
in.close();
} catch (IOException e) {
showStatus( "I/O Exception:
" + e );
} finally {
if (s!=null) try { s.close();
} catch (IOException e) {}
}
}
public boolean action(Event e, Object o)
{
if ( e.target == load ) {
loadSales();
return true;
}
return false;
}
}
Now, on the server end, you must provide a file sales.txt,
with any text you want in it. After the client-side user clicks
the Load Sales Report button, the contents of that file are transferred
over the socket and printed in the TextArea.
Because applets often are disallowed from opening local files,
this is a useful alternative.
In a more complicated application, you need to define a protocol
for client/server interaction over the stream socket. The basics
of how you connect and use the socket always remain the same,
however. It is worth pointing out that for large client/server
and distributed applications, packages are available to ease the
task. One such package is called Remote Method Invocation
(RMI), which provides a transparent way to call on the methods
of a remote Java object. Later, I'll discuss another approach
that is used in ChatApplet.java.
Stream sockets provide a reliable connection to a fixed destination
socket, but you pay for this convenience. A slight overhead is
involved in checking for errors and correctly sequencing the packets.
Moreover, for some applications, using stream sockets for every
transmission would be prohibitively complex. Consider the example
of a chat applet, which would be a member of a network of similar
applets. These applets are to send messages typed by the user
to all the members of the network. Using stream sockets, this
requires each applet to open a socket for every other applet in
the network, so in a network of N machines, there will be roughly
N ¥ N sockets open, occupying
just as many ports. This is quite wasteful and, in any case, the
nature of a chat network is discrete; messages are dispatched
in small quantities of text. For this purpose, datagrams are ideal.
A datagram or UDP socket occupies one port but can send datagrams
to any remote UDP socket. The datagram is sent immediately, and
the sending thread does not block waiting for its delivery, regardless
of whether anyone actually receives the packet. Short of using
some kind of IP multicasting or broadcasting protocol, this is
the most efficient way to reach a large number of peers with discrete
update-type information.
Although user datagrams are not checked for data errors, you can
choose to implement your own checksums. You also might want to
have a mechanism for verifying that a datagram has arrived.
DatagramSocket
The java.net.DatagramSocket
class represents a UDP socket on the local host. You can create
it on a specific port:
DatagramSocket ds = new DatagramSocket(
9876 );
or on any available port:
DatagramSocket ds = new DatagramSocket();
After the socket is created, you can send and receive datagram
packets using the send()
and the receive() methods.
The packets themselves are represented by java.net.DatagramPacket
objects, which you can construct from an array of bytes and addressing
information. The send() method
requires the DatagramPacket
to be fully constructed; it must be filled in with a correct data
buffer, data length, remote InetAddress,
and remote port. The receive()
method requires only a data buffer and a maximum length to receive.
The receive() method blocks
until a datagram arrives and then fills in the buffer of the specified
datagram. The actual length of the data received then is available
via getLength(). For example,
you can receive string data like this:
DatagramSocket sock = new DatagramSocket();
byte b[] = new byte[1000];
DatagramPacket p = new DatagramPacket( b, 1000 );
sock.receive( p ); // wait for a packet
byte data[] = p.getData();
int len = p.getLength();
String s = new String( data, 0, 0, len );
Similarly, data to be sent is constructed from an array of bytes.
Suppose that you want to send a string. You can use this code:
String message;
InetAddress remoteaddr;
int remoteport;
DatagramSocket sock = new DatagramSocket();
// Set the message, remoteaddr and remoteport ...
byte b[] = new byte[ message.length() ];
message.getBytes( 0, m.length(), b, 0 );
DatagramPacket p = new DatagramPacket(b, m.length(), remoteaddr,
remoteport);
try {
sock.send( p );
} catch (IOException e) {
// Uh-oh! Do something.
}
A Basic Datagram Application
This section looks at a very basic use of datagrams: the two-way
chat program DatagramChat.java,
as shown in Figure 12.1. The user is expected to enter the remote
host name and port number, so this program is not really very
user-friendly. However, this does avoid the complication of having
a central hookup server, which otherwise would be necessary in
order to let the chat windows find each other's sockets.
Figure 12.1 : A datagram chat applet.
After the user types a message in the input TextField
and then clicks Send, the message is sent in a DatagramPacket,
along with a leading byte with the value PRINT
or EchO (in this case, PRINT).
Listing 12.5 shows the code that the DatagramChat
class uses to send a message datagram.
Listing 12.5. Sending a datagram in class DatagramChat.
void sendMessage()
{
if ( remoteaddr==null ) {
status.setText( "You must specify
a remote host and port!" );
return;
}
String m = input.getText().trim();
if (m.length() < 1) return;
byte b[] = new byte[ m.length()+1 ];
b[0] = PRINT;
m.getBytes( 0, m.length(), b, 1 );
DatagramPacket p = new DatagramPacket(b, m.length()+1,
remoteaddr, remoteport);
try {
sock.send( p );
status.setText( "Message sent to
" + remoteaddr );
} catch (IOException e) {
status.setText( "Message send failed:
" + e.getMessage() );
}
}
The same window must be able to receive messages from the remote
peer. To accomplish this, it implements the Runnable
interface and has its run()
method execute in a separate Thread.
All the run() method does
is wait for messages and then respond-an infinite loop. The response
to a message is to print it in a TextArea,
and (if it is marked PRINT)
send back the same message, now marked EchO.
Messages marked EchO also
are printed to the TextArea,
but they are not returned to the sender. This means that the chat
window supports remote echoing; if you don't see the message in
the TextArea, chances are
the remote user doesn't see it either. Listing 12.6 shows the
run() method of the DatagramChat
class.
Listing 12.6. The run()
method of DatagramChat.
public void run()
{
byte b[] = new byte[MTU];
while ( sock != null ) {
try {
DatagramPacket p = new DatagramPacket(
b, MTU );
sock.receive( p );
byte data[] = p.getData();
int len = p.getLength();
byte op = data[0];
String s = new String( data,
0, 1, len-1 );
print( s );
if ( op==PRINT ) {
// Echo the packet
back to the sender
data[0] = EchO;
DatagramPacket
q = new DatagramPacket(data, len, p.getAddress(),
Âp.getPort());
sock.send(q);
}
} catch (IOException e) {}
}
}
The troublesome thing about this program is that it is hard to
run as an applet (it is provided as DatagramChatApplet.java).
The Web browser usually will not permit you to receive datagrams
in an applet (although my applet viewer lets me do this if I load
the applet locally). Of course, it's possible to run this as a
stand-alone application (java DatagramChat),
but that defeats much of the purpose of having applets at all.
For this reason, chat applets in Java will have to use a stream
socket connection to a server program running on their home
server-the Web server where the applet's class files reside.
This star-like configuration loads down the server and eats up
several port numbers. Not only that-it requires a carefully designed
protocol to make sure that client and server can accomplish essentially
asynchronous tasks in a sequential conversation and still recover
from unexpected conditions. Worse, it makes the response time
unacceptably slow if chat groups of a few dozen applets or more
exist. Nevertheless, any applet that wants to communicate with
other applets using sockets has to live with these restrictions.
The next section discusses one way to achieve this.
The program ChatApplet.java
on the book's accompanying CD-ROM is a more realistic attempt
at a chat applet (see Figure 12.2). It uses a stream socket to
talk to a central server program (ChatServer.java),
located on the same machine as the applet's code. The server program
coordinates connections to several clients, grouping them into
chat pools (different channels or rooms). The trans
package provides a uniform way for client and server to communicate
requests and commands to each other. The server's port number
defaults to 8081, but any port number can be given as the single
command-line argument. The applet itself is just a panel containing
a ChatWindow, in which you
can set the server name, port number, and initial screen name
by parameters in the APPLET
tag (server, port,
and name). It won't be necessary
to specify the server, because the applet figures out its home
server by using the getCodeBase()
method (don't forget to have the server running, though). The
ChatApplet class contains
a single instance of class ChatWindow,
which does all the work. The ChatWindow
class can also run as a stand-alone program.
Figure 12.2 : The ChatApplet.java applet.
The server program has most of the smarts about which chat pools
are defined and who is in them. The client applets just act as
a user interface to the server, which updates the pools at will.
Ideally, the smarts would be distributed somewhat more democratically
in a web of interconnected applets, but due to security restrictions
(discussed earlier), you are forced into the star configuration.
This limits the usefulness of the applet, because large numbers
of clients will bring the server to a crawl, and because the system
is not fault-tolerant.
A detailed discussion of the mechanics of these programs is really
beyond the scope of this chapter, but this section will go over
the basic idea of how the client/server transactions are performed.
The trans
Package
The trans package used by
ChatApplet.java and ChatServer.java
provides an artificial kind of remote procedure call functionality.
Transactions are sent back and forth over a stream socket in a
semi-asynchronous way. The transactions are instances of a subclass
of the abstract class Transaction.
The socket is encapsulated in an object of type TChannel.
This is a smart channel for initiating and responding to transactions.
Either side can be the client (or initiator) of a transaction
(depending only on the transaction type itself), so there is no
essential asymmetry built in.
Each Transaction object exists
only to be converted to and from its properties, which
are pairs of name/value strings written in a TProps
object, which is essentially a jazzed-up Hashtable
with better read and write
methods than the java.util.Properties
class has. The exact class of the transaction determines its type,
so the inheritance mechanism is used here basically as a way to
discriminate among the different types of transactions while treating
them on an equal footing as objects characterized by their properties.
The properties are accessed via the getProperty()
and setProperty() methods.
On creation, the constructor sets the properties to reflect the
transaction parameters. The transaction is sent by passing it
to the initiate() method
of an opened TChannel object.
The ChatServer, for example,
greets the client and asks for its screen name by initiating a
transaction of class Hello,
as Listing 12.7 shows.
Listing 12.7. How the ChatServer
says hello.
class ClientManager extends TChannel
{
// ...
}
class HandleClient extends Thread
{
ClientManager cm;
// ...
try {
Hello h = new Hello(); //
a Hello object has no initial props
cm.initiate(h); //
start the transaction
cm.name = h.name; //
this gets the result: a screen name
// ...
} catch ( IOException e ) {}
}
Initiating a transaction means that the following items are written
to the stream socket:
- An opcode byte (indicating REQ
(request) or ACK (acknowledge)
- A serial number (this serves to match replies to previous
requests)
- The name of the transaction class (here, it is Hello)
- The properties of the transaction (a TProps
object, written out)
The best way to write primitive Java types over a stream socket
is to use a DataOutputStream
attached to the socket (see Chapter 3 for
a full discussion of data I/O). The opcode is written using writeByte(),
the serial number with writeInt(),
and the various strings with writeUTF().
The receiving socket then can read the data using a DataInputStream,
in a parallel fashion.
The most important methods in a Transaction
class are its build() and
decode() methods. The build()
method is run on the receiving end, and it accepts a single TProps
object as an argument. These properties are the ones that were
written on the socket at the initiating end, and the build()
method's job is to take some action based on the values of these
properties. The build() method
can use setProperty() to
set some return properties as well (on entry to build(),
the receiver-side transaction's properties are guaranteed to be
empty).
When the build() method finishes,
the result properties are written back (even if they remain empty),
with an opcode of ACK (acknowledge).
It then is the job of the decode()
method to make sense of these return properties on the initiating
end. The result of a Hello
transaction is a screen name, for example. This means that the
build() and the decode()
method each take half the responsibility for the encoding and
decoding, whereas the constructor need not do anything at all.
Listing 12.8 shows the complete code of the Hello
class (here, ChatTransaction
extends Transaction).
Listing 12.8. The Hello Transaction
class.
import java.net.*;
import trans.TProps;
// A Hello transaction requests that the client identify itself.
public class Hello extends ChatTransaction
{
static String NAME = "name";
String name;
public Hello() {}
protected void build( TProps p )
{
setProperty( NAME, getWindow().screenname
);
}
protected void decode()
{
name = getProperty( NAME );
}
}
Even though the constructor is empty here, you still must declare
it, if only to make it public. The trans
package requires that all Transactions
be public classes with a public constructor that takes no arguments
(a null constructor). This is because the receiving TChannel
actually instantiates a new transaction of the correct type by
using the class descriptor's newInstance()
method:
// This code instantiates a Transaction
of class named in the String tClass:
try {
Class c = Class.forName( tClass );
t = (Transaction)c.newInstance();
} catch (Exception ex) {
throw new TransactionException();
}
Transactions appear synchronous to the initiating thread; the
initiate() method does not
return until the transaction's return properties have been read
and decoded. The same TChannel
can act as both initiator and receiver simultaneously, however,
and each received transaction has its build()
method executed in a separate thread. If this build()
method decides to initiate()
another transaction back to the sender, you have a subtransaction,
and that's okay, too. In this way, entire conversations of transactions
and subtransactions can be carried out more or less transparently.
The instigating thread doesn't see any of the subtransactions,
because it is blocked, waiting on the outcome of the entire process.
This multithreading requires that some care be taken in determining
which methods should be synchronized and which should not, in
order to avoid deadlock.
The downside of the approach is that there currently is no way
to provide a time-out period for transactions to complete. Also,
in the event of an error, there is no defined way to restore the
TChannel to a sane state.
It also would be nice to have a form of initiate()
that returns immediately, executing the transaction in another
thread and optionally notifying the caller by executing an agreed-upon
method call when the transaction is completed (this is best done
by defining an interface-for example, TransactionObserver).
Performing these improvements would be fairly straightforward
(after reading about multithreaded programs), so consider it an
exercise!
The main reason behind the phenomenal rise of Java is its potential
to become the lingua franca of interactive applications
on the Internet. The exponential growth of the World Wide Web
(WWW) has started a drive to redistribute the burden of computation,
which has until now rested almost completely on the Web server.
The CGI scripts so common today require no cleverness on the client
end, but this simplicity comes at a price. The variety and semantics
of CGI interaction are limited by the html form syntax and the
strictures of HTTP, and a single server is forced to think on
behalf of a potentially limitless number of clients. Java, on
the other hand, is a real programming language that loads once
and executes at the client end. This makes an enormous difference
in terms of efficiency, both from the client's point of view (faster
execution) and from the point of view of the network (lower bandwidth).
So, although Java will soon be powering telephones and stereos,
it is on the Web that Java finds its first natural home.
In this section, you'll see how Java programs can interact with
Web servers and also with existing CGI programs. Java can convert
URLs (Web addresses) into usable program objects. On a lower level,
Java can read the raw contents of a URL. Java applets can also
request certain actions of their host browser, such as displaying
a status line or loading a new document.
A URL refers to a specific resource on the Web: an html
file, image, animation, or whatever the case may be. URLs look
like this:
http://www.WidgetsGalore.com/~MrWidget/sales.html
URLs specify a protocol (http),
a server (www.WidgetsGalore.com),
and a path to the resource in question. In Java, URLs are encapsulated
in a constant object of class java.net.URL,
which is described in Chapter 3.
An applet can use a URL to load a new browser page, as shown in
this code:
class MyApplet extends Applet
{
// Other stuff ...
public void showNewPage( URL u )
{
getAppletContext().showDocument( u );
}
}
If you use framesets in your document, there is also an optional
second argument to specify the target frame or window.
Java URL objects have a getContent()
method for converting the contents of the URL into a Java object,
assuming that such a conversion is defined. A URL can reference
an image, and then getContent() returns
an Object whose runtime class
is some subclass of Image,
for example. The content type of most Web pages is text/html,
which has no defined conversion. You can use the URL object to
open an InputStream to read
the contents of the URL, however:
URL url = new URL("http://www.WidgetsGalore.com/~MrWidget/home.html");
DataInputStream in = new DataInputStream(url.openStream());
String line;
while ( ( line = in.readLine() ) != null )
System.out.println( line );
in.close();
When it is defined, the getContent()
conversion is performed by a ContentHandler
object. The following sections tell you how to define custom ContentHandlers
for your own types of objects.
The URL class may get a lot
of credit, but the URLConnection
class really does most of the work. A URLConnection
object manages an interaction with a Web server at a given URL.
When you call on a URL's
getContent() method, the
URL object creates a URLConnection
to do the dirty work. You can retrieve the URLConnection
associated with a URL by
calling the openConnection()
method of the URL. This can
give you access to the values of the HTTP header fields, which
tell you such things as the last modification date. It lets you
specify details of the interaction, such as retrieval conditional
on the last modification time (use setIfModifiedSince()
or set the If-Modified-Since
header field using setRequestProperty()).
A URLConnection object remains
unconnected until its connect()
method is called, which happens when some operation such as getContent()
requires that a connection be made. Before it is connected, you
are free to modify the fields that control the transaction. After
it is connected, this is an error. URLConnections that use the
HTTP protocol can perform the HTTP GET
and POST methods because
they are smart enough to wait for you to access their I/O streams
before connecting to the Web server and issuing a GET
or a POST request; basically,
the POST method is used only
if you actually write some output. You'll see how to do this soon.
The built-in functionality of URLConnections
is centered on the HTTP protocol, but a URL
object can use any protocol name that has an associated URLStreamHandler.
A URLStreamHandler object
must implement the abstract method openConnection(),
which returns a URLConnection
object given a URL. The URL knows where to get the stream handler
for a given protocol by consulting the URLStreamHandlerFactory.
You can define your own protocols by writing a class implementing
the URLStreamHandlerFactory
interface and then setting it to be the stream-handler factory
using the static method URL.setStreamHandlerFactory().
In practice, though, you're almost always better off using HTTP
and defining new content handlers corresponding to custom MIME
types. The exception is if you're trying to write a reusable set
of classes to perform FTP or some other such protocol that already
exists.
Caution |
Don't assume that your Java implementation supports a protocol type just because it is common; my implementation doesn't understand ftp or news (moreover, the file protocol completely ignores the server field and loads a local
file regardless). This isn't all that surprising; for example, a URLStreamHandler for the news protocol would have to produce a URLConnection object that knows how to talk to a news server and serve up a new Java object
corresponding to each news URL. This functionality is too specific to be part of a standard API.
|
How does a resource sitting on a Web server get converted into
a usable Java object? First, when the Web server returns the contents
of the corresponding URL, it includes header fields indicating
the MIME type and encoding of the content to follow. For a GIF
image, the MIME type is identified by the string image/gif.
For html, it is text/html.
When a URL or URLConnection
performs its getContent()
method to create a new object from a URL, it needs to find a content
handler for whatever MIME type the server returns. The java.net
package automatically parses the MIME type from the appropriate
HTTP header field and then passes it to a ContentHandlerFactory,
which returns the required handler or null
(or throws a runtime exception). If you want to know the MIME
type explicitly, the getContentType()
method is available in the class URLConnection.
A content handler is an object of class java.net.ContentHandler,
with a method getContent(),
which takes a URLConnection
and returns an Object. A
different content-handler class exists for each recognized MIME
type, because they each must do different things with the data
from the URL.
The correspondence between MIME types and ContentHandlers
is established by the ContentHandlerFactory.
A Java program has one ContentHandlerFactory:
an object implementing the interface java.net.ContentHandlerFactory,
which has been installed as the ContentHandlerFactory,
as shown in this code:
URLConnection.setContentHandlerFactory(
chf );
The ContentHandlerFactory
interface specifies one method; the createContentHandler()
method accepts a MIME type name string and returns a ContentHandler
object for that MIME type, or null
if one is not known. You never call on a ContentHandler
or ContentHandlerFactory
directly; they are there to be used by the higher level classes
URL and URLConnection,
and to be subclassed by programmers who want to add new MIME-type
capabilities to Java.
Now look at a functioning but simple example. Consider a Java
class called Colleague:
class Colleague
{
String name;
String phone;
String fax;
Date birthday;
Date nextMeeting;
}
To prepare for loading Colleague
objects from a URL using HTTP, you perform the following steps:
- Define a new MIME type to the HTTP server.
- Implement a ContentHandler
class for the type.
- Install a custom ContentHandlerFactory
that understands the new type.
Defining a New MIME Type
You can call the new MIME type anything, as long as it doesn't
mess up other MIME types on the same server. Call it application/java-Colleague
to avoid trouble. To tell the Web server about the new MIME type,
you have to add a local configuration directive. For ncSA httpd,
one of the most common servers, you put the following line in
an .htaccess file in the
same directories as your Colleague
URLs:
AddType application/java-Colleague .coll
The server then recognizes any URLs with the .coll
file name extension as belonging to the MIME type application/java-Colleague
and reports them as such. This won't work if your site disallows
per-directory access configuration for httpd.
Talk to your system administrator if you're not sure; she may
prefer to define the new type for you in a global configuration
file. Also, other servers provide this functionality in different
ways.
Writing a Custom ContentHandler
Listing 12.9 shows a very simple ContentHandler
for the class Colleague.
Listing 12.9. A Custom ContentHandler
class.
class ColleagueHandler extends ContentHandler
{
// Read from the URL and create a Colleague:
public Object getContent( URLConnection c ) throws
IOException
{
DataInputStream in = new DataInputStream(
c.getInputStream() );
Colleague coll = new Colleague();
String s;
coll.name = in.readLine();
coll.phone = in.readLine();
coll.fax = in.readLine();
try {
coll.birthday = new Date(
in.readLine() );
coll.nextMeeting = new Date(
in.readLine() );
} catch (Exception e) {
throw new IOException( e.getMessage()
);
}
return coll;
}
}
This class knows how to read from a URLConnection
and construct a new Colleague
object. It defines the format; there are five lines in a .coll
file, representing the name, phone, fax, birthday, and next meeting
date of that given colleague. For a real application, you should
use tagged fields (for example, Birthday:
Jun 5 1972). This makes it easier to define new versions
of your format without invalidating old URLs.
Using a Custom ContentHandlerFactory
Writing a content handler factory class is easy, but it's a good
idea to make it flexible (because you only get to install it once!).
You might want to make a class like the one shown in Listing 12.10,
which lets your program add new MIME types at will.
Listing 12.10. A custom ContentHandlerFactory.
class MyContentHandlerFactory implements
ContentHandlerFactory
{
Hashtable handlers = new Hashtable();
void addMimeType( String mimetype, ContentHandler
handler )
{
handlers.put( mimetype, handler );
}
public ContentHandler createContentHandler( String
mimetype )
{
return (ContentHandler)handlers.get( mimetype
);
}
}
This class is flexible, because other classes from the same package
can add new ContentHandlers
on-the-fly by calling addMimeType().
Listing 12.11 shows a little demo program which does this (ContentHandlerDemo.java).
Listing 12.11. ContentHandlerDemo.java: using a custom ContentHandler.
public class ContentHandlerDemo
{
public static void main(String args[])
{
if ( args.length != 1 ) {
System.err.println( "please
give a URL argument!" );
System.exit(1);
}
MyContentHandlerFactory chf = new MyContentHandlerFactory();
ColleagueHandler collHandler = new ColleagueHandler();
chf.addMimeType( "application/java-Colleague",
collHandler );
URLConnection.setContentHandlerFactory(
chf );
try {
URL url = new URL( args[0]
);
Colleague coll = (Colleague)url.getContent();
System.out.println("Loaded
colleague:");
System.out.println("name
= " + coll.name);
System.out.println("phone
= " + coll.phone);
System.out.println("fax
= " + coll.fax);
System.out.println("birthday
= " + coll.birthday);
System.out.println("next
meeting = " + coll.nextMeeting);
} catch ( Exception e ) {
System.err.println( "Exception:
" + e ) ;
System.exit(1);
}
}
}
To use this, define a colleague in a file. Fred.coll,
for example, could contain this text:
Fred Fredrickson
800-8001
805-8123
Jan 12 1970
Jul 26 1996 3:15 PM
Then, by giving the URL to Fred.coll,
the ContentHandlerDemo program
should be able to load the information from the server, as this
example shows (using the UNIX command prompt as an example):
wallaby$ java ContentHandlerDemo http://www.WidgetsGalore.com/~MrWidget/colleagues/ÂFred coll
Loaded colleague:
name = Fred Fredrickson
phone = 800-8001
fax = 805-8123
birthday = Mon Jan 12 00:00:00 PST 1970
next meeting = Fri Jul 26 15:15:00 PDT 1996
If you can't get this working, make sure that your file is in
a location where your Web server expects to find it. Also make
sure that you have defined a new MIME type to the server and provided
the correct URL on the command line.
Tip |
If an HTTP error occurs, the server returns an error screen of type text/html. Be warned that there is no predefined ContentHandler for this type, so you might want to detect this by explicitly examining the content type of the
URLConnection object (if you don't, the java.net package throws a rather nondescriptive runtime exception (ClassNotFoundException) of its own.
|
The GET method is the usual
HTTP method for retrieving a URL, so you actually are using it
every time you follow a hypertext link that isn't a Submit button
on an html form. Some URLs expect to be fed additional information,
however, which is appended onto the URL after a question mark.
This so-called query information is useful for passing argument
values to executable CGI scripts, such as WWW pages interfacing
to databases or search engines. The query information is a sequence
of attribute names and values, as this code shows:
http://www.myserver.edu/searchquery.cgi?text=My+Favorite+Foods&name=Fred
This query information encodes the name/value pairs "text=My
Favorite Foods" and "name=Fred".
Any CGI application that expects to receive query information
must be able to decode this information. The encoding is a standard
one, in which space characters are mapped to + and any troublesome
characters (control codes, +,
%, and so on) are mapped
to %xx, where xx
is the hexadecimal value of the character. Separating name/value
pairs by ampersands (&)
is the usual thing to do when there is more than one pair.
To interact with such a CGI program, you can use the java.net.URLEncoder
class to encode the URL. Somewhat surprisingly, the URLEncoder
class maps & and =
to their hexadecimal code equivalents, so if you want to use the
syntax described in the preceding paragraph, you need to encode
each value separately and concatenate the results with ampersands
and equal signs yourself. The CGI program must retrieve the information
from the environment variable QUERY_STRING
and reverse the encoding process described earlier. Look at some
established CGI scripts if you're not sure how this works.
To use the information returned by the CGI script, you can read
an InputStream furnished
by URL.openStream() or URLConnection.getInputStream().
Alternatively, you can furnish a content handler for the MIME
type of the object returned and just do a getContent()
on the encoded URL. To avoid extensibility problems, you might
not want to provide content handlers for common MIME types such
as text/html (unless you
are writing a Web browser in Java!).
The POST method is the method
used when submitting an html form. Generally, it enables you to
write to a URLConnection
so that whatever you write is fed to the referenced object (usually
a CGI program) on its standard input stream. Because of the way
HTTP transactions work, you should write and close the output
stream before you read the input or do anything else that would
cause the URLConnection to
connect. The server can require that the content length of the
input be specified along with the request, and this can be done
only before connecting. This also lets the URLConnection
object realize what you're up to and then specify the POST
method rather than GET.
Listing 12.12 shows a simple example of how to use the POST
method to interface with a CGI program. The PostMethodDemo
applet posts the contents of a TextArea
to a little CGI program called wordcount.cgi
and then shows the result. You have to call this applet by its
full URL, because it uses its code base to construct a URL to
the CGI program, which should be in the same directory. Depending
on your server configuration, you might not be allowed to execute
CGI programs in user directories. Then you should move wordcount.cgi
to your cgi-bin directory
on the Web server and modify the applet source to supply the correct
URL.
Listing 12.12. PostMethodDemo.java:
using the POST
method.
import java.io.*;
import java.net.*;
import java.awt.*;
import java.applet.Applet;
public class PostMethodDemo extends Applet
{
Label title = new Label( "An Applet using the
POST method", Label.CENTER );
TextArea text = new TextArea();
Button load = new Button( "Post!" );
public void init()
{
// Initialize the applet
setLayout( new BorderLayout() ); add(
"North", title );
add( "Center", text );
Panel p = new Panel();
p.add( load );
add( "South", p );
validate();
}
void postText()
{
try {
String s;
URL u = new URL( getCodeBase(),
"wordcount.cgi" );
URLConnection c = u.openConnection();
PrintStream out = new PrintStream(c.getOutputStream());
out.print( text.getText().trim()
);
out.close();
DataInputStream in = new DataInputStream(c.getInputStream());
text.setText("");
while ( ( s = in.readLine()
) != null )
text.appendText(
s + "n" );
in.close();
} catch (IOException e) {
text.appendText( "Exception:
" + e + "n" ); return;
}
}
public boolean action(Event e, Object o)
{
if ( e.target == load ) {
postText();
return true;
}
return false;
}
}
The CGI program wordcount.cgi
is just a simple Perl script that counts words and lines. You
should make sure that it has world execute permissions and that
it finds your local Perl compiler correctly (try running it by
hand!). Notice that the first thing it prints is a header line
indicating the content type, text/plain,
followed by a blank line. The blank line is mandatory, and it
is not part of the content. It separates the head information
from the content body:
#!/usr/bin/perl
$words = 0;
$lines = 0;
print "Content-type: text/plainnn";
while (<>) {
$lines++;
$words += s/S+//g;
}
print "You entered $words words on $lines lines.n";
Using the POST method doesn't
prevent you from also passing query information along with the
URL if you choose, just as with the GET
method (of course, this depends on the CGI program being smart
enough to look for the query string).
In this chapter, you've explored the various networking capabilities
of Java. Several levels of functionality are available: the entire
gamut from plain sockets (both TCP and UDP), to manipulating URLs
and URL connections on the Web, right up to fetching Java objects
from URLs by using content handlers. Though applets are hobbled
in their connectivity by security restrictions, there is still
quite a bit that can be done. Stand-alone programs can avoid the
problem entirely, and new Java environments are being developed
that should make the security interface more user-configurable.
Java complements and enhances existing CGI programs, which are
still appropriate for a number of database lookup tasks. Java
is also ideal for writing stand-alone server programs to service
socket requests, and could even serve as a CGI language itself.
In Chapter 13, "General Purpose Classes,"
you'll learn about the utility classes provided in Java. These
general-purpose classes provide some of the handiest data structures
that every programmer needs. It's important to understand what
they can do so that you can use them to save time and trouble
in solving your own programming problems.
Next
Previous
Contents
|