All Categories :
Java
Chapter 9
Java Socket Programming
CONTENTS
To demonstrate full Java client/server applet
connectivity, an applet server is necessary. This chapter initiates
the development of a Java HTTP server. Before beginning the server,
however, you need some background knowledge of socket programming.
This chapter begins with a socket overview and is followed by
an exploration of Java's socket classes. The remainder of the
chapter will delve into constructing a Java HTTP Web server and
a client/server applet.
After reading this chapter, you should be able to do the following:
- Understand the socket abstraction
- Know the different modes of socket operation
- Have a working knowledge of the HTTP protocol
- Be able to apply the Java socket classes
- Understand applet socket use and limitations
- Comprehend the HTTP Java server
The computers on the Internet are connected by the TCP/IP protocol.
In the 1980s, the Advanced Research Projects Agency (ARPA) of
the U.S. government funded the University of California at Berkeley
to provide a UNIX implementation of the TCP/IP protocol suite.
What was developed was termed the socket interface, although
you might hear it called the Berkeley-socket interface or just
Berkeley sockets. Today, the socket interface is the most widely
used method for accessing a TCP/IP network.
A socket is nothing more than a convenient abstraction. It represents
a connection point into a TCP/IP network, much like the electrical
sockets in your home provide a connection point for your appliances.
When two computers want to converse, they each use a socket. One
computer is termed the server-it opens a socket and listens for
connections. The other computer is termed the client; it calls
the server socket to start the connection. To establish a connection,
all that's needed is a destination address and a port number.
Each computer in a TCP/IP network has a unique address. Ports
represent individual connections within that address. This is
analogous to corporate mail-each person within a company shares
the same address, but a letter is routed within the company by
the person's name. Each port within a computer shares the same
address, but data is routed within each computer by the port number.
When a socket is created, it must be associated with a specific
port-this is known as binding to a port.
Sockets have two major modes of operation: connection-oriented
and connectionless. Connection-oriented sockets operate
like a telephone; they must establish a connection and a hang
up. Everything that flows between these two events arrives in
the same order it was sent. Connectionless sockets operate like
the mail-delivery is not guaranteed, and multiple pieces of mail
may arrive in a different order than they were sent.
Which mode to use is determined by an application's needs. If
reliability is important, then connection-oriented operation is
better. File servers need to have all their data arrive correctly
and in sequence. If some data was lost, the server's usefulness
would be invalidated. Some applications-a time server, for example-send
discrete chunks of data at regular intervals. If the data became
lost, the server would not want the network to retry until the
data was sent. By the time the data arrived, it would be too old
to have any accuracy. When you need reliability, be aware that
it does come with a price. Ensuring data sequence and correctness
requires extra processing and memory usage; this extra overhead
can slow down the response times of a server.
Connectionless operation uses the User Datagram Protocol (UDP).
A datagram is a self- contained unit that has all the information
needed to attempt its delivery. Think of it as an envelope-it
has a destination and return address on the outside and contains
the data to be sent on the inside. A socket in this mode does
not need to connect to a destination socket; it simply sends the
datagram. The UDP protocol promises only to make a best-effort
delivery attempt. Connectionless operation is fast and efficient,
but not guaranteed.
Connection-oriented operation uses the Transport Control Protocol
(TCP). A socket in this mode needs to connect to the destination
before sending data. Once connected, the sockets are accessed
using a streams interface: open-read-write-close. Everything sent
by one socket is received by the other end of the connection in
exactly the same order it was sent. Connection-oriented operation
is less efficient than connectionless, but it's guaranteed.
Sun Microsystems has always been a proponent of internetworking,
so it isn't surprising to find rich support for sockets in the
Java class hierarchy. In fact, the Java classes have significantly
reduced the skill needed to create a sockets program. Each transmission
mode is implemented in a separate set of Java classes. The connection-oriented
classes will be discussed first.
The connection-oriented classes within Java have both a client
and a server representative. The client half tends to be the simplest
to set up, so it will be covered first.
Listing 9.1 shows a simple client application. It requests an
HTML document from a server and displays the response to the console.
Listing 9.1. A simple socket client.
import java.io.*;
import java.net.*;
/**
* An application that opens a connection to a Web server and
reads
* a single Web page from the connection.
* NOTE: "merlin" is the name of my local machine.
*/
public class SimpleWebClient {
public static void main(String args[])
{
try
{
//
Open a client socket connection
Socket
clientSocket1 = new Socket("merlin", 80);
System.out.println("Client1:
" + clientSocket1);
//
Get a Web page
getPage(clientSocket1);
}
catch (UnknownHostException
uhe)
{
System.out.println("UnknownHostException:
" + uhe);
}
catch (IOException
ioe)
{
System.err.println("IOException:
" + ioe);
}
}
/**
* Request a Web page using the passed
client socket.
* Display the reply and close the client
socket.
*/
public static void getPage(Socket clientSocket)
{
try
{
//
Acquire the input and output streams
DataOutputStream
outbound = new DataOutputStream(
clientSocket.getOutputStream()
);
DataInputStream
inbound = new DataInputStream(
clientSocket.getInputStream()
);
//
Write the HTTP request to the server
outbound.writeBytes("GET
/ HTTP/1.0\r\n\r\n");
//
Read the response
String
responseLine;
while
((responseLine = inbound.readLine()) != null)
{
//
Display each line to the console
System.out.println(responseLine);
//
This code checks for EOF. There is a bug in the
//
socket close code under Win 95. readLine() will
//
not return null when the client socket is closed
//
by the server.
if
( responseLine.indexOf("</HTML>") != -1 )
break;
}
//
Clean up
outbound.close();
inbound.close();
clientSocket.close();
}
catch (IOException
ioe)
{
System.out.println("IOException:
" + ioe);
}
}
}
| Note |
The examples in this chapter are coded as applications so as to avoid security restrictions. Run the code from the command line java ClassName.
|
Recall that a client socket issues a connect to a listening server
socket. Client sockets are created and connected by using a constructor
from the Socket class. The following line creates a client socket
and connects it to a host:
Socket clientSocket = new Socket("merlin",
80);
The first parameter is the name of the host you want to connect
to; the second parameter is the port number. A host name specifies
only the destination computer. The port number is required to
complete the transaction and allow an individual application to
receive the call. In this case, 80 was specified, the well-known
port number for the HTTP protocol. Other well-known port numbers
are shown in Table 9.1. Port numbers are not mandated by any governing
body, but are assigned by convention-this is why they are said
to be "well known."
Table 9.1. Well-known port numbers.
| Service | Port
|
| echo | 7 |
| daytime | 13
|
| ftp | 21 |
| telnet | 23 |
| smtp | 25 |
| finger | 79 |
| http | 80 |
| pop3 | 110 |
Because the Socket class is connection oriented, it provides a
streams interface for reads and writes. Classes from the java.io
package should be used to access a connected socket:
DataOutputStream outbound = new DataOutputStream(
clientSocket.getOutputStream() );
DataInputStream inbound = new DataInputStream( clientSocket.getInputStream()
);
Once the streams are created, normal stream operations can be
performed:
outbound.writeBytes("GET / HTTP/1.0\r\n\r\n);
String responseLine;
while ( (responseLine = inbound.readLine()) != null)
{
System.out.println(responseLine);
}
The above code snippet requests a Web page and echoes the response
to the screen. When the program is done using the socket, the
connection needs to be closed:
outbound.close();
inbound.close();
clientSocket.close();
Notice that the socket streams are closed first. All socket streams
should be closed before the socket is closed. This application
is relatively simple, but all client programs follow the same
basic script:
- Create the client socket connection.
- Acquire read and write streams to the socket.
- Use the streams according to the server's protocol.
- Close the streams.
- Close the socket.
Using a server socket is only slightly more complicated, as explained
in the following section.
Server Sockets
Listing 9.2 is a partial listing of a simple server application.
The complete server example can be found on the CD-ROM in SimpleWebServer.java.
Listing 9.2. A simple server application.
/**
* An application that listens for connections and serves a simple
* HTML document.
*/
class SimpleWebServer {
public static void main(String args[])
{
ServerSocket serverSocket
= null;
Socket clientSocket
= null;
int connects =
0;
try
{
//
Create the server socket
serverSocket
= new ServerSocket(80, 5);
while
(connects < 5)
{
//
Wait for a connection
clientSocket
= serverSocket.accept();
//Service
the connection
ServiceClient(clientSocket);
connects++;
}
serverSocket.close();
}
catch (IOException
ioe)
{
System.out.println("Error
in SimpleWebServer: " + ioe);
}
}
public static void ServiceClient(Socket
client)
throws IOException
{
DataInputStream
inbound = null;
DataOutputStream
outbound = null;
try
{
//
Acquire the streams for IO
inbound
= new DataInputStream( client.getInputStream());
outbound
= new DataOutputStream( client.getOutputStream());
//
Format the output (response header and tiny HTML document)
StringBuffer
buffer = PrepareOutput();
String
inputLine;
while
((inputLine = inbound.readLine()) != null)
{
//
If end of HTTP request, send the response
if
( inputLine.equals("") )
{
outbound.writeBytes(buffer.toString());
break;
}
}
}
finally
{
//
Clean up
System.out.println("Cleaning
up connection: " + client);
outbound.close();
inbound.close();
client.close();
client.close();
}
}
Servers do not actively create connections. Instead, they passively
listen for a client connect request and then provide their services.
Servers are created with a constructor from the ServerSocket class.
The following line creates a server socket and binds it to port
80:
ServerSocket serverSocket = new ServerSocket(80,
5);
The first parameter is the port number on which the server should
listen. The second parameter is optional. The API documentation
indicates that this parameter is a listen time, but in traditional
sockets programming the listen function's second parameter is
the listen stack depth. As it turns out, this is also true for
the second constructor parameter. A server can receive connect
requests from many clients at the same time, but each call must
be processed one at a time. The listen stack is a queue
of unanswered connect requests. The above code instructs the socket
driver to maintain the last five connect requests. If the constructor
omits the listen stack depth, a default value of 50
is used.
Once the socket is created and listening for connections, incoming
connections are created and placed on the listen stack. The accept()
method is called to lift individual connections off the stack:
Socket clientSocket = serverSocket.accept();
This method returns a connected client socket used to converse
with the caller. No conversations are ever conducted over the
server socket itself. Instead, the server socket will spawn a
new socket in the accept()
method. The server socket is still open and queuing new connection
requests.
Like the client socket, the next step is to create an input and
output stream:
DataInputStream inbound = new DataInputStream(
clientSocket.getInputStream() );
DataOutputStream outbound = new DataOutputStream( clientSocket.getOutputStream()
);
Normal I/O operations can now be performed by using the newly
created streams. This server waits for the client to send a blank
line before sending its response. When the conversation is finished,
the server closes the streams and the client socket. At this point,
the server tries to accept more calls. What happens when there
are no calls waiting in the queue? The method will wait for one
to arrive. This behavior is known as blocking. The accept()
method will block the server thread from performing any other
tasks until a new call arrives. When five connects have been serviced,
the server exits by closing its server socket. Any queued calls
will be canceled.
All servers follow the same basic script:
- Create the server socket and begin listening.
- Call the accept() method
to get new connections.
- Create input and output streams for the returned socket.
- Conduct the conversation based on the agreed protocol.
- Close the client streams and socket.
- Go back to step 2, or continue to step 7.
- Close the server socket.
Figure 9.1 summarizes the steps needed for client/server
connection-oriented applications.
Figure 9.1: Client and server connection-oriented applications.
The application just presented is known as an iterative server
because the code accepts a client connection and completely processes
it before it will accept another connection. More complex servers
are concurrent. Instead of accepting connections and immediately
processing them, a concurrent server spawns a new thread
to process each new request, so it seems as though the server
is processing many requests simultaneously. All commercial Web
servers are concurrent servers.
Unlike connection-oriented classes, the datagram versions of the
client and server behave in nearly identical manners-the only
difference occurs in implementation. The same class is used for
both client and server halves. The following lines create client
and server datagram sockets:
DatagramSocket serverSocket = new DatagramSocket(
4545 );
DatagramSocket clientSocket = new DatagramSocket();
The server specifies its port using the lone constructor parameter
4545. Since the client will
call the server, the client can use any available port. The omitted
constructor parameter in the second call instructs the operating
system to assign the next available port number. The client could
have requested a specific port, but the call would fail if some
other socket had already bound itself to that port. It's better
not to specify a port unless the intent is to be a server.
Since streams can't be acquired for communication, how do you
talk to a DatagramSocket? The answer lies in the DatagramPacket
class.
Receiving Datagrams
The DatagramPacket class is used to receive and send data over
DatagramSocket classes. The packet class contains connection information
as well as the data. As was explained earlier, datagrams are self-contained
transmission units. The DatagramPacket class encapsulates these
units. The following lines receive data from a datagram socket:
DatagramPacket packet = new DatagramPacket(new
byte[512], 512);
clientSocket.receive(packet);
The constructor for the packet needs to know where to place the
received data. A 512-byte buffer was created and passed to the
constructor as the first parameter. The second constructor parameter
was the size of the buffer. Like the accept()
method in the ServerSocket class, the receive()
method will block until data is available.
Sending Datagrams
Sending datagrams is really very simple; all that's needed is
a complete address. Addresses are created and tracked by using
the InetAddress class. This class has no public constructors,
but it does contain several static methods that can be used to
create an instance of the class. The following list shows the
public methods that create InetAddress class instances:
| Public InetAddress Creation Methods |
InetAddress getByName(String host);
InetAddress[] getAllByName(String host);
InetAddress getLocalHost();
|
Getting the local host is useful for informational purposes, but
only the first two methods are actually used for sending packets.
Both getByName() and getAllByName()
require the name of the destination host. The first method merely
returns the first match it finds. The second method is needed
because a computer can have more than one address. When this occurs,
the computer is said to be multi-homed. The computer has
one name, but multiple ways to reach it.
All the creation methods are marked as static. They must be called
as follows:
InetAddress addr1 = InetAddress.getByName("merlin");
InetAddress addr2[] = InetAddress.getAllByName("merlin");
InetAddress addr3 = InetAddress.getLocalHost();
Any of these calls can throw an UnknownHostException. If a computer
is not connected to a Domain Name Server (DNS) or if the host
is really not found, an exception will be thrown. If a computer
does not have an active TCP/IP configuration, then getLocalHost()
is likely to fail with this exception as well.
Once an address is determined, datagrams can be sent. The following
lines transmit a String to a destination socket:
String toSend = "This is the data
to send!");
byte[] sendbuf = new byte[ toSend.length() ];
toSend.getBytes( 0, toSend.length(), sendbuf, 0 );
DatagramPacket sendPacket = new DatagramPacket( sendbuf, sendbuf.length,
addr, port);
clientSocket.send( sendPacket );
First, the string must be converted to a byte array. The getBytes()
method takes care of the conversion. Next, a new DatagramPacket
instance must be created. Notice the two extra parameters at the
end of the constructor. Since this will be a send packet, the
address and port of the destination must also be placed into the
packet. An applet may know the address of its server, but how
does a server know the address of its client? Remember that a
datagram is like an envelope-it has a return address. When any
packet is received, the return address can be extracted from the
packet by using getAddress()
and getPort(). This is how
a server would respond to a client packet:
DatagramPacket sendPacket = new DatagramPacket(
sendbuf, sendbuf.length,
recvPacket.getAddress(), recvPacket.getPort()
);
serverSocket.send( sendPacket );
Unlike connection-oriented operation, datagram servers are actually
less complicated than the datagram client.
Datagram Servers
The basic script for datagram servers is as follows:
- Create the datagram socket on a specific port.
- Call receive to wait for incoming packets.
- Respond to received packets according to the agreed protocol.
- Go back to step 2, or continue to step 5.
- Close the datagram socket.
Listing 9.3 shows a simple datagram echo server. It will echo
back any packets it receives.
Listing 9.3. A simple datagram echo server.
import java.io.*;
import java.net.*;
public class SimpleDatagramServer
{
public static void main(String[] args)
{
DatagramSocket
socket = null;
DatagramPacket
recvPacket, sendPacket;
try
{
socket
= new DatagramSocket(4545);
while
(socket != null)
{
recvPacket=
new DatagramPacket(new byte[512], 512);
socket.receive(recvPacket);
sendPacket
= new DatagramPacket(
recvPacket.getData(),
recvPacket.getLength(),
recvPacket.getAddress(),
recvPacket.getPort() );
socket.send(
sendPacket );
}
}
catch (SocketException
se)
{
System.out.println("Error
in SimpleDatagramServer: " + se);
}
catch (IOException
ioe)
{
System.out.println("Error
in SimpleDatagramServer: " + ioe);
}
}
}
Datagram Clients
The corresponding client uses the same process with one exception:
A client must initiate the conversation. The basic recipe for
datagram clients is as follows:
- Create the datagram socket on any available port.
- Create the address to send to.
- Send the data according to the server's protocol.
- Wait for receive data.
- Go to step 3 (send more data), 4 (wait for receive), or 6
(exit).
- Close the datagram socket.
Figure 9.2 summarizes the steps needed for client/server datagram
applications. The symmetry between client and server is evident
from this picture; compare Figure 9.2 with Figure 9.1.
Figure 9.2 : Client and server datagram applications.
Listing 9.4 shows a simple datagram client. It reads user input
strings and sends them to the echo server from Listing 9.3. The
echo server will send the data right back, and the client will
print the response to the console.
Listing 9.4. A simple datagram client.
import java.io.*;
import java.net.*;
public class SimpleDatagramClient
{
private DatagramSocket socket = null;
private DatagramPacket recvPacket, sendPacket;
private int hostPort;
public static void main(String[] args)
{
DatagramSocket
socket = null;
DatagramPacket
recvPacket, sendPacket;
try
{
socket
= new DatagramSocket();
InetAddress
hostAddress = InetAddress.getByName("merlin");
DataInputStream
userData = new DataInputStream( System.in );
while
(socket != null)
{
String
userString = userData.readLine();
if
(userString == null || userString.equals(""))
return;
byte
sendbuf[] = new byte[ userString.length() ];
userString.getBytes(0,
userString.length(), sendbuf, 0);
sendPacket
= new DatagramPacket(
sendbuf,
sendbuf.length, hostAddress, 4545 );
socket.send(
sendPacket );
recvPacket=
new DatagramPacket(new byte[512], 512);
socket.receive(recvPacket);
System.out.write(recvPacket.getData(),
0,
recvPacket.getLength());
System.out.print("\n");
}
}
catch (SocketException
se)
{
System.out.println("Error
in SimpleDatagramClient: " + se);
}
catch (IOException
ioe)
{
System.out.println("Error
in SimpleDatagramClient: " + ioe);
}
}
}
All the examples so far have been Java applications. Running these
in an applet presents an extra complication: security.
When writing applications, you don't need to be concerned with
security exceptions. This changes when the code under development
is executed from an applet. Netscape Navigator 2.0 uses very stringent
security measures where sockets are concerned. An applet may open
a socket only back to the host name from which it was loaded.
If any other connection is attempted, a SecurityException will
be thrown.
Datagram sockets don't open connections, so how is security ensured
for these sockets? When an inbound packet is received, the host
name is checked. If the packet did not originate from the server,
a SecurityException is immediately thrown. Obviously, sending
comes under the same scrutiny. If a datagram socket tries to send
to any destination except the server, a SecurityException is thrown.
These restrictions apply only to the address, not the port number.
Any port number on the host may be used.
All the socket techniques demonstrated so far will be developed
further in this chapter's project.
This project at first glance seems a bit ambitious, but writing
a rudimentary Web server is not as hard as it sounds. Client applets
need an HTTP Web server so they can open sockets. If an applet
is loaded into Netscape from a hard drive, then no socket activity
is allowed to take place. A simple solution is to write an HTTP
server application. Once written, additional server threads can
be added to provide all types of back-end connectivity. This project
will add a multipurpose datagram protocol that will be used for
live data in both Chapter 10, "Native
Methods and Java," and 11, "Building a Live Data Applet."
Before diving into the project, you need some background information
on the HTTP protocol. The Hypertext Transfer Protocol (HTTP) has
been in use on the World Wide Web since 1990. All applet-bearing
Web pages are sent over the net with HTTP. The server will support
a subset of version 1.0 in that only file requests will be handled.
As long as Netscape page requests can be fulfilled, the server
will have accomplished its goal.
HTTP uses a stream-oriented (TCP) socket connection. Typically,
port 80 is used, but other port numbers can be substituted. All
the protocol is sent in plain-text format. An example of a conversation
was demonstrated in Listings 9.1 and 9.2. The server listens on
port 80 for a client request, which takes this format:
GET FILE HTTP/1.0
The first word is referred to as the "method" of the
request. Table 9.2 lists all the request methods for HTTP/1.0.
Table 9.2. HTTP/1.0 request methods.
| Method | Use |
| GET | Retrieve a file |
| HEAD | Retrieve only file information
|
| POST | Send data to the server
|
| PUT | Send data to the server
|
| DELETE | Delete a resource |
| LINK | Link two resources |
| UNLINK | Unlink two resources
|
The second parameter of a request is a file path. Each of the
following URLs is followed by the request that will be formulated
and sent:
HTTP://www.qnet.com/
GET / HTTP/1.0
HTTP://www.qnet.com/index.html
GET /index.html HTTP/1.0
HTTP://www.qnet.com/classes/applet.html
GET /classes/applet.html HTTP/1.0
The request does not end until a blank line containing only a
carriage return (\r) and a line feed (\n) is received. After the
method line, a number of optional lines can be sent. Netscape
Navigator 2.0 will produce the following request:
GET / HTTP/1.0
Connection: Keep-Alive
User-Agent: Mozilla/2.0 (Win95; I)
Host: merlin
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Responses use a header similar to the request:
HTTP/1.0 200 OK
Content-type: text/html
Content-Length: 128
Like the request, the response header is not complete until a
blank line is sent containing only a carriage return and a line
feed. The first line contains a version identification string,
followed by a status code indicating the results of the request.
Table 9.3 lists all the defined status codes. The server will
send only two of these: 200 and 404. The text that follows the
status code is optional. It may be omitted, or, if present, it
might not match the definitions given in the table.
Table 9.3. HTTP response status codes.
| Status Code | Optional Text Description
|
| 200 | OK |
| 201 | Created
|
| 202 | Accepted
|
| 204 | No Content
|
| 300 | Multiple Choices
|
| 301 | Moved Permanently
|
| 302 | Moved Temporarily
|
| 304 | Not Modified
|
| 400 | Bad Request
|
| 401 | Unauthorized
|
| 403 | Forbidden
|
| 404 | Not Found
|
| 500 | Internal Server Error
|
| 501 | Not Implemented
|
| 502 | Bad Gateway
|
| 503 | Service Unavailable
|
Immediately after the response header, the requested file is sent.
When the file is completely transmitted, the socket connection
is closed. Each request-response pair consumes a new socket connection.
That's enough information to construct a basic Web server. Full
information on the HTTP protocol can be retrieved from HTTP://www.w3.org/.
The basic Web server will follow the construction of the SimpleWebServer
from Listing 9.2. Many improvements will have to be made to method
and response handling. The simple server does not parse or store
the request header as it arrives. The new Web server will have
to parse and store the requests for later processing. To do this,
you need a class to contain an HTTP request.
HTTPrequest Class
Listing 9.5 shows the complete HTTPrequest class. The class must
contain all the information that could be conveyed in a request
header.
Listing 9.5. The HTTPrequest class.
import java.io.*;
import java.util.*;
import java.net.*;
import NameValue;
/**
* This class maintains all the information from an HTTP request
*/
public class HTTPrequest
{
public String version;
public String method;
public String file;
public Socket clientSocket;
public DataInputStream inbound;
public NameValue headerpairs[];
/**
* Create an instance of this class
*/
public HTTPrequest()
{
version = null;
method = null;
file = null;
clientSocket =
null;
inbound = null;
headerpairs =
new NameValue[0];
}
/**
* Add a name/value pair to the internal
array
*/
public void addNameValue(String name,
String value)
{
try
{
NameValue
temp[] = new NameValue[ headerpairs.length + 1 ];
System.arraycopy(headerpairs,
0, temp, 0, headerpairs.length);
temp[
headerpairs.length ] = new NameValue(name, value);
headerpairs
= temp;
}
catch (NullPointerException
npe)
{
System.out.println("NullPointerException
while adding name-value: " + Ânpe);
}
}
/**
* Renders the contents of the class in
String format
*/
public String toString()
{
String s = method
+ " " + file + " " + version + "\n";
for (int x = 0;
x < headerpairs.length; x++ )
s += headerpairs[x] + "\n";
return s;
}
}
The NameValue class simply stores two strings: name and value.
You can find the source code for it on the CD-ROM in NameValue.java.
When a new pair needs to be added, a new array is allocated. The
new array receives a copy of the old array as well as the new
member. The old array is then replaced with the newly created
entity.
Two data fields in the class are not directly part of an HTTP
request. The clientSocket
member allows response routines to get an output stream, and the
inbound member allows easy
closure after a request has been processed. The remaining members
are all part of an HTTP request. The method toString()
allows class objects to be printed using "plus notation."
The following line will display the contents of a request by invoking
the toString() method:
System.out.println("Request: "
+ request);
Now that the request container is finished, it's time to populate
it.
BasicWebServer Class
This is the main class for the server. It can be broken down into
request and response routines. Since this is a server, the request
routines will be activated first. After some validation, the response
routines will be called. Listing 9.6 provides the routines to
parse an HTTP request.
Listing 9.6. HTTP request routines.
/**
* Read an HTTP request into a continuous
String.
* @param client a connected client stream
socket.
* @return a populated HTTPrequest instance
* @exception ProtocolException If not
a valid HTTP header
* @exception IOException
*/
public HTTPrequest GetRequest(Socket client)
throws IOException,
ProtocolException
{
DataInputStream
inbound = null;
HTTPrequest request
= null;
try
{
//
Acquire an input stream for the socket
inbound
= new DataInputStream(client.getInputStream());
//
Read the header into a String
String
reqhdr = readHeader(inbound);
//
Parse the string into an HTTPrequest instance
request
= ParseReqHdr(reqhdr);
//
Add the client socket and inbound stream
request.clientSocket
= client;
request.inbound
= inbound;
}
catch (ProtocolException
pe)
{
if
( inbound != null )
inbound.close();
throw
pe;
}
catch (IOException
ioe)
{
if
( inbound != null )
inbound.close();
throw
ioe;
}
return request;
}
/**
* Assemble an HTTP request header String
* from the passed DataInputStream.
* @param is the input stream to use
* @return a continuous String representing
the header
* @exception ProtocolException If a pre
HTTP/1.0 request
* @exception IOException
*/
private String readHeader(DataInputStream
is)
throws IOException,
ProtocolException
{
String command;
String line;
// Get the first
request line
if ( (command
= is.readLine()) == null )
command
= "";
command += "\n";
// Check for HTTP/1.0
signature
if (command.indexOf("HTTP/")
!= -1)
{
//
Retrieve any additional lines
while
((line = is.readLine()) != null && !line.equals(""))
command
+= line + "\n";
}
else
{
throw
new ProtocolException("Pre HTTP/1.0 request");
}
return command;
}
/**
* Parsed the passed request String and
populate an HTTPrequest.
* @param reqhdr the HTTP request as a
continous String
* @return a populated HTTPrequest instance
* @exception ProtocolException If name,value
pairs have no ':'
* @exception IOException
*/
private HTTPrequest ParseReqHdr(String
reqhdr)
throws IOException,
ProtocolException
{
HTTPrequest req
= new HTTPrequest();
// Break the request
into lines
StringTokenizer
lines = new StringTokenizer(reqhdr, "\r\n");
String currentLine
= lines.nextToken();
// Process the
initial request line
// into method,
file, version Strings
StringTokenizer
members = new StringTokenizer(currentLine, " \t");
req.method = members.nextToken();
req.file = members.nextToken();
if (req.file.equals("/"))
req.file = "/index.html";
req.version =
members.nextToken();
// Process additional
lines into name/value pairs
while ( lines.hasMoreTokens()
)
{
String
line = lines.nextToken();
//
Search for separating character
int
slice = line.indexOf(':');
//
Error if no separating character
if
( slice == -1 )
{
throw
new ProtocolException(
"Invalid
HTTP header: " + line);
}
else
{
//
Separate at the slice character into name, value
String
name = line.substring(0,slice).trim();
String
value = line.substring(slice + 1).trim();
req.addNameValue(name,
value);
}
}
return req;
}
The method readHeader() interrogates
the inbound socket stream searching for the blank line. If the
request is not in HTTP/1.0 format, this method will throw an exception.
Otherwise, the resulting String is passed to parseReqHdr()
for processing.
These routines will reject any improperly formatted requests,
including requests made in the older HTTP/0.9 format. Parsing
makes heavy use of the StringTokenizer class found in the java.util
package.
Normally, it would be preferable to close the inbound stream as
soon as the request has been completely read. If this is done,
then subsequent output attempts will fail with an IOException.
This is why the inbound stream is placed into the HTTPrequest
instance. When the output has been completely sent, both the output
and the input streams will be closed.
| Caution |
Do not be tempted to close an inbound stream after all input has been read. Closing the input stream will cause subsequent output attempts to fail with an IOException. Close both streams only after all socket operations are finished.
|
Currently, the server makes no use of the additional lines in
an HTTP request header. The HTTPrequest class does save them in
an array, however, so they can be used in future enhancements.
Wherever possible, the server has been written with future enhancements
in mind.
Once you've built the request, you need to form a response. Listing
9.7 presents the response routines used by the server.
Listing 9.7. HTTP response routines.
/**
* Respond to an HTTP request
* @param request the HTTP request to
respond to.
* @exception ProtocolException If unimplemented
request method
*/
private void implementMethod(HTTPrequest
request)
throws ProtocolException
{
try
{
if
(debug && level < 4)
System.out.println("DEBUG:
Servicing:\n" + request);
if
( (request.method.equals("GET") ) ||
(request.method.equals("HEAD")) )
ServicegetRequest(request);
else
{
throw
new ProtocolException("Unimplemented method: " + Ârequest.method);
}
}
catch (ProtocolException
pe)
{
sendNegativeResponse(request);
throw
pe;
}
}
/**
* Send a response header for the file
and the file itself.
* Handles GET and HEAD request methods.
* @param request the HTTP request to
respond to
*/
private void ServicegetRequest(HTTPrequest
request)
throws ProtocolException
{
try
{
if
(request.file.indexOf("..") != -1)
throw
new ProtocolException("Relative paths not supported");
String
fileToGet = "htdocs" + request.file;
FileInputStream
inFile = new FileInputStream(fileToGet);
if
(debug & level < 4)
{
System.out.print("DEBUG:
Sending file ");
System.out.print(fileToGet
+ " " + inFile.available());
System.out.println("
Bytes");
}
sendFile(request,
inFile);
inFile.close();
}
catch (FileNotFoundException
fnf)
{
sendNegativeResponse(request);
}
catch (ProtocolException
pe)
{
throw
pe;
}
catch (IOException
ioe)
{
System.out.println("IOException:
Unknown file length: " + ioe);
sendNegativeResponse(request);
}
}
/**
* Send a negative (404 NOT FOUND) response
* @param request the HTTP request to
respond to.
*/
private void sendNegativeResponse(HTTPrequest
request)
{
DataOutputStream
outbound = null;
try
{
//
Acquire the output stream
outbound
= new DataOutputStream(
request.clientSocket.getOutputStream());
//
Write the negative response header
outbound.writeBytes("HTTP/1.0
");
outbound.writeBytes("404
NOT_FOUND\r\n");
outbound.writeBytes("\r\n");
//
Clean up
outbound.close();
request.inbound.close();
}
catch (IOException
ioe)
{
System.out.println("IOException
while sending -rsp: " + ioe);
}
}
/**
* Send the passed file
* @param request the HTTP request instance
* @param inFile the opened input file
stream to send\
*/
private void sendFile(HTTPrequest request,
FileInputStream inFile)
{
DataOutputStream
outbound = null;
try
{
//
Acquire an output stream
outbound
= new DataOutputStream(
request.clientSocket.getOutputStream());
//
Send the response header
outbound.writeBytes("HTTP/1.0
200 OK\r\n");
outbound.writeBytes("Content-type:
text/html\r\n");
outbound.writeBytes("Content-Length:
" + inFile.available() + "\r\n");
outbound.writeBytes("\r\n");
//
Added to allow Netscape to process header properly
//
This is needed because the close is not recognized
sleep(500);
//
If not a HEAD request, send the file body.
//
HEAD requests solicit only a header response.
if
(!request.method.equals("HEAD"))
{
byte
dataBody[] = new byte[1024];
int
cnt;
while
((cnt = inFile.read(dataBody)) != -1)
outbound.write(dataBody,
0, cnt);
}
//
Clean up
outbound.flush();
outbound.close();
request.inbound.close();
}
catch (IOException
ioe)
{
System.out.println("IOException
while sending file: " + ioe);
}
}
Only GET and HEAD
requests are honored. The primary goal is to provide an applet
server, not a full-featured Web server. File requests are all
that's needed for applet loading, though additional handlers can
certainly be added for other request methods. The serviceGetRequest()
function handles all responses. When the input stream for a file
is acquired, the file is opened. At this point, the routine knows
whether the file exists and its size. Once a valid file is found,
the sendFile() function can
be called. The file is read and sent in 1K blocks. This keeps
memory usage down while seeking to balance the number of disk
accesses attempted. Negative responses are sent only for errors
occurring after the request has been built. As a consequence,
improperly formatted requests will generate no response.
The response routines rely on ProtocolExceptions to signal error
conditions. When one of these exceptions reaches the implementMethod()
function, a negative response is sent. Notice the catch clause
in serviceGetRequest(). The
ProtocolException must be caught and thrown again, or the following
IOException will catch the event. This is because ProtocolException
is a child class of IOException. If it had been placed after the
IOException, the compiler would have generated an error:
BasicWebServer.java:303: catch not reached.
The remainder of the BasicWebServer application can be found on
the CD-ROM. The
remaining code calls the input routine getRequest()
and then the output routine implementMethod()
for each client connection.
The next section develops a client applet that will be loaded
with the server just constructed. Another service thread will
be added to the server to conduct a datagram socket protocol with
the client.
Applets need to communicate with a server for a variety of applications.
What is needed is a generic protocol that any applet can use to
communicate with its server. This protocol should not be connection
oriented because of the additional load that would be placed on
a server. Datagrams present a lighter load and allow the same
socket to be used no matter how many actual connections are being
serviced. What is envisioned is a broadcast capability for data.
It isn't reasonable for applets to query a server every five seconds
to see whether data has changed. The server should be able to
send to all of its connections whenever the data changes. With
this in mind, the Datagram Transfer Protocol (DGTP) was developed.
The primary requirements of this protocol were as follows:
- Use datagrams to lessen server socket
use.
- Implement a hook-in and an unhook mechanism.
- Allow generic data of any type to be transferred.
- Allow any object type to use DGTP services.
- Provide a broadcast capability.
Figure 9.3 shows a client applet using DGTP to communicate with
a server. Notice how the HTTP data connection does not extend
to the applet. The browser spawns the applet from the data received
from the server.
Figure 9.3 : DGTP communication in the client/server model
The DGTP protocol uses a header much like HTTP; its basic methods
are REGISTER, UNREGISTER,
DATA, PING,
and PONG. The two register
methods accomplish hooking and unhooking. PING
and PONG are currently unused,
but could provide a mechanism to periodically check the connection
list. The DATA method facilitates
the transfer capability. To allow any object to use DGTP services,
a standard interface was developed. These interfaces specify the
set of functions that an object must use to communicate with DGTP
service threads. Listing 9.8 shows the client and server interfaces.
Listing 9.8. DGTP client and server interfaces.
public interface LiveDataNotify
{
public String getDestHost();
public int getDestPort();
public void recvNewData(byte[] newDataBlock);
public void connectRefused();
}
public interface LiveDataServer
{
public boolean ValidateRegistrant(ClientAddr
user);
public void NewRegistrant(ClientAddr user);
public void DropRegistrant(ClientAddr
user);
public void recvNewData(byte[] newDataBlock,
DatagramPacket fromWho);
}
Notice that both the client and the server have a method to receive
blocks of data. The client has methods to specify the destination
host and port, and the server has methods to validate and register
new connections. The DGTP client is covered first.
DGTP Client
Since listening for receive will block until there is data, the
registration requests will have to be sent using a different thread.
The first thing the client's run()
method does is to start the registration thread. At that point,
it can begin receiving data. Listing 9.9 displays a partial listing
of the DGTP client. The complete source code for the client and
the registration threads is on the CD-ROM in DGTPClient.java.
Listing 9.9. The DGTPClient class.
/**
* The runmethod for the client. Start
the register thread and
* begin listening for incoming packets.
*/
public void run()
{
DatagramPacket
packet = null;
try
{
regThread.start();
while
(socket != null)
{
packet
= new DatagramPacket(new byte[512], 512);
socket.receive(packet);
try
{
parsePacketData(packet);
}
catch
(ProtocolException pe)
{
System.out.println("ProtocolException:
" + pe);
}
}
}
catch (IOException
ioe)
{
System.out.println("IOException:
in DGTPClient: " + ioe);
}
}
/**
* Handle a DGTP incoming header
* @param packet the incoming packet to
parse
* @exception ProtocolException
* @exception IOException
*/
public void parsePacketData(DatagramPacket
packet)
throws IOException,
ProtocolException
{
String command
= null;
ByteArrayInputStream
barray = null;
DataInputStream
is = null;
barray = new ByteArrayInputStream(
packet.getData(),
0, packet.getLength() );
is = new DataInputStream(
barray );
command = readHeader(is);
StringTokenizer
lines, cmds;
lines = new StringTokenizer(command,
"\r\n");
cmds = new StringTokenizer(lines.nextToken(),
" \t");
String ver = cmds.nextToken();
String cmd = cmds.nextToken();
if ( cmd.equals("PING")
)
send("PONG"
+ cmds.nextToken());
else if ( cmd.equals("REGISTER")
)
{
lastResponse
= cmds.nextToken();
registered
= true;
if
( !lastResponse.equals("CONFIRM") )
{
dataClient.connectRefused();
socket.close();
socket
= null;
}
}
else if ( cmd.equals("UNREGISTER")
)
{
lastResponse
= cmds.nextToken();
if
( lastResponse.equals("CONFIRM") )
{
registered
= false;
socket.close();
socket
= null;
}
}
else if ( cmd.equals("DATA")
)
{
int
length = Integer.valueOf(cmds.nextToken()).intValue();
byte[]
data = new byte[length];
try
{
is.readFully(data);
dataClient.recvNewData(data);
}
catch
(EOFException eof)
{
throw
new ProtocolException(
"Server
packet too short: " + eof);
}
catch
(IOException ioe)
{
throw
new ProtocolException(
"While
reading server data: " + ioe);
}
}
else
{
throw
new ProtocolException(
"Unknown
DGTP command: " + cmd);
}
is.close();
}
/**
* Unregister the DGTPClient
*/
public void terminate()
{
unregThread =
new ClientUnregistration(this);
unregThread.start();
}
The read routines are largely the same as the HTTP server's. What
is significant is the translation of the packet data to stream
format. Once that is done, the header can be parsed in the same
manner as an HTTP request. To perform the translation, ByteArrayInputStream
is used; this class is extremely useful when working with byte
arrays. Once the array is in a stream format, it can be turned
into a DataInputStream-the same format the BasicWebServer used
to read its requests.
The terminate() function
spawns a new thread to send the UNREGISTER
command because the main client thread is blocked in a receive
call.
DGTP Server Class
Since DGTP is a datagram protocol, the server will be very similar
to the client. There are two main changes, the largest of which
occurs in the parsePacketData()
handler function. Listing 9.10 shows the data parse function for
the DGTPServer class. The complete source code can be found on
the CD-ROM in DGTPServer.java.
Listing 9.10. DGTPServer data parsing routine.
/**
* Process all incoming packets
* @param packet contains the DGTP request
* @exception ProtocolException
* @exception IOException
*/
public void ParsePacketData(DatagramPacket
packet)
throws IOException,
ProtocolException
{
String command
= null;
ByteArrayInputStream
barray = null;
DataInputStream
is = null;
String cmd = null;
barray = new ByteArrayInputStream(
packet.getData(),
0, packet.getLength() );
is = new DataInputStream(
barray );
command = readHeader(is);
try
{
StringTokenizer
lines = new StringTokenizer(command, "\r\n");
StringTokenizer
cmds = new StringTokenizer(lines.nextToken(), " \t");
String
ver = cmds.nextToken();
cmd
= cmds.nextToken();
if
( cmd.equals("PING") )
{
ClientAddr
addr = new ClientAddr(
packet.getAddress(),
packet.getPort());
send(addr,
"PONG" + cmds.nextToken());
}
else
if ( cmd.equals("REGISTER") )
{
ClientAddr
addr = new ClientAddr(
packet.getAddress(),
packet.getPort());
if
(!Clients.containsKey(addr))
{
if
( dataServer.ValidateRegistrant(addr) )
{
Clients.put(addr,
addr);
send(addr,
"REGISTER CONFIRM");
dataServer.NewRegistrant(addr);
}
else
{
send(addr,
"REGISTER DENIED");
}
}
else
{
send(addr,
"REGISTER CONFIRM");
}
}
else
if ( cmd.equals("UNREGISTER") )
{
int
port = Integer.valueOf(cmds.nextToken()).intValue();
dumpUser(
new ClientAddr(packet.getAddress(), port) );
}
else
if ( cmd.equals("DATA") )
{
int
length = Integer.valueOf(cmds.nextToken()).intValue();
byte[]
data = new byte[length];
try
{
is.readFully(data);
dataServer.recvNewData(data,
packet);
}
catch
(EOFException eof)
{
throw
new ProtocolException(
"Client
packet too short: " + eof);
}
catch
(IOException ioe)
{
throw
new ProtocolException(
"While
reading client data: " + ioe);
}
}
else
{
throw
new ProtocolException(
"Unknown
DGTP command: " + cmd);
}
}
catch (NoSuchElementException
ne)
{
throw
new ProtocolException(
"Command
arg mismatch: " + cmd);
}
is.close();
}
The changes occur when adding new users. The server thread will
receive a REGISTER request,
which it will pass to the interface object for validation. If
the interface object accepts the new user, a REGISTER
CONFIRM response is sent, and the interface object
is alerted to the addition. If the user is rejected, a REGISTER
DENIED response is sent. The second change is one
of omission. The run() method
for the server will not spawn a registration thread. Otherwise,
it is identical to the client's run()
method.
The server keeps track of user connections in a Hashtable. The
ClientAddr class object encapsulates the address and port as well
as providing a hash key. This allows the server to add a new user
quickly. The code for the REGISTER
method creates the address and checks to see whether it's already
present. Multiple REGISTER
requests may have been sent before the REGISTER
CONFIRM packet could travel back to the sender. If
the server doesn't have this connection yet, it adds the address
to the Clients list. Listing 9.11 shows the ClientAddr class.
Pay particular attention to the hashCode()
and equals() functions; they
allow the object to act as a hash key.
Listing 9.11. The ClientAddr class.
import java.net.InetAddress;
public class ClientAddr
{
public InetAddress address;
public int port;
ClientAddr(InetAddress addr, int hostPort)
{
address = addr;
port = hostPort;
}
public int hashCode()
{
int result = address.hashCode();
result += port;
return result;
}
public boolean equals(Object obj)
{
return (obj !=
null) && (obj instanceof ClientAddr) &&
(address.equals(((ClientAddr)obj).address))
&&
(port
== ((ClientAddr)obj).port);
}
}
Since this is a broadcast server, there is a varied array of send
methods embedded in the class. The DGTP server has two main send
routines:
sendData(ClientAddr dest, byte[] data,
int srcOffset, int length);
send(ClientAddr dest, String toSend);
The first routine sends the byte array as a DGTP DATA block. The
second routine sends the passed String as a DGTP command header.
All the remaining send routines call sendData()
to do the actual transmission. This is the code for one version
of sendToUsers():
public void sendToUsers(byte[] data,
int srcOffset, int length)
{
for (Enumeration e = Clients.elements();
e.hasMoreElements();)
sendData((ClientAddr)e.nextElement(),
data, srcOffset, length);
}
This routine uses an Enumeration object to loop through the client
hashtable and send to each member. All the remaining send methods
are variations on this theme. Some send to all users; some send
to a specific subset of users. These are all the public send methods:
Public Send Methods in DGTPServer
|
sendToUsers(String toSend);
sendToUsers(byte[] data, int Offset, int length);
sendToUsers(ClientAddr[] users, String toSend);
sendToUsers(ClientAddr[] users, byte[] data, int Offset, int length);
sendData(ClientAddr addr, String block);
sendData(ClientAddr dest, byte[] data, int srcOffset, int length);
send(ClientAddr dest, String toSend);
|
Now that the threads are in place, it's time to apply them in
the actual client/server applet.
The client applet will be simple in appearance. The emphasis here
will be on using the DGTP protocol. Figure 9.4 shows the applet
in action.
Figure 9.4 : A simple client applet display.
The purpose of this applet is to display the number of active
connections to this page. Whenever a new user connects, the display
will automatically update to reflect the new count. Likewise,
when a user disconnects, the count will update. Listing 9.12 shows
the client applet class.
Listing 9.12. A client applet.
import java.applet.*;
import java.awt.*;
import java.net.*;
import java.io.*;
import java.util.*;
import DGTPClient;
public class SimpleClientApplet extends Applet
implements LiveDataNotify
{
private boolean init = false;
DGTPClient ct = null;
int destPort;
String destHost = null;
String numUsers = "Unknown at this
time";
String users = null;
public void init()
{
if ( init == false
)
{
init
= true;
resize(500,500);
String
strPort = getParameter("PORT");
if
( strPort == null )
{
System.out.println("ERROR:
PORT parameter is missing");
strPort
= "4545";
}
destPort
= Integer.valueOf(strPort).intValue();
destHost
= getDocumentBase().getHost();
}
}
public void paint(Graphics g)
{
g.drawString("Registered
Users: " + getUsers(), 0, 100);
}
public String getDestHost()
{
return destHost;
}
public int getDestPort()
{
return destPort;
}
public synchronized void recvNewData(byte[]
newDataBlock)
{
users =
new String(newDataBlock, 0);
repaint();
}
public synchronized String getUsers()
{
if (users != null)
{
StringTokenizer
cmds = new StringTokenizer(users, " \t");
if
(cmds.nextToken().equals("CLIENTS"))
numUsers
= cmds.nextToken();
}
return numUsers;
}
public void start()
{
ct = new DGTPClient(this);
ct.start();
}
public void stop()
{
System.out.println("SimpleClientApplet.stop()");
ct.terminate();
}
public void connectRefused()
{
}
}
The applet layers a simple protocol on top of DGTP. Whenever the
server detects a change in the number of users, it sends a DATA
block with the following text:
CLIENTS number CRLF
The applet receives the new DATA block and converts it to a String
in recvNewData(). Note that
this routine as well as getUsers()
is marked as synchronized. This prevents the applet from attempting
to read the String while DGTP is updating it.
The applet uses an applet parameter to know which port the server
is monitoring. The following line reads the PORT parameter:
String strPort = getParameter("PORT");
The server host name is retrieved from the document itself:
destHost = getDocumentBase().getHost();
This is all the information needed to establish a server connection.
| Using gethost() over a dial-up connection
|
If you connect to the Internet through a dial-up account, then you might have trouble with this application because of a host name issue. Specifically, when a dial-in PPP connection is made, your computer is assigned an IP address by the provider. This address is displayed by the server when it is started. Users on the Internet can now reach your server by typing in your IP address:
HTTP://xxx.xxx.xxx.xxx/
This will access your server, and pages can be sent. The trouble arises when your applet attempts to receive data from your server. The call to getDocumentBase().getHost() will return the IP address that the user typed in to reach your server:
"xxx.xxx.xxx.xxx"
In reality, your service provider has already assigned your connection a name:
"dialup_xx.internet.service.provide.com"
When the server sends data to the applet, the host name on the data will be that of the service provider. Netscape will flag this as a security violation and raise the dreaded SecurityException. The solution is to enter the actual connection name into the initial URL, but determining this name is a problem. The easiest method I've found is to go ahead and use the IP address initially. When the exception is raised, open the Java console to discover the actual connection name. Use this name instead, and your applet will work wonderfully.
|
The PORT parameter needs
to be coded into the HTML applet file so the applet knows on which
port the server is listening. The HTML tag for this applet looks
like this:
<applet
codebase="/classes"
code="SimpleClientApplet.class"
width=500
height=500
>
<param name="PORT" value="4545">
</applet>
Adding the DGTP Server Thread
Because the BasicWebServer project was written in Java, it's trivial
to add an instance of the DGTPServer. The problem is that some
object needs to implement the LiveDataServer interface. The base
server class could be changed to add this behavior, but then it
would have to be rewritten any time you wanted a new service thread.
A better solution is to create a separate thread whose only purpose
is to spawn and communicate with the DGTPServer. To this end,
the NumUsersServer class was created. It really doesn't do much,
but it does create the needed interface and enable simple integration
with the Web server. Listing 9.13 shows the NumUsersServer.
Listing 9.13. The NumUsersServer class.
import java.lang.Thread;
import java.net.DatagramPacket;
import DGTPServer;
import LiveDataServer;
import ClientAddr;
public class NumUsersServer extends Thread
implements LiveDataServer
{
private DGTPServer servThread = null;
public NumUsersServer(int hostPort)
{
servThread = new
DGTPServer(this, hostPort);
}
public void run()
{
servThread.start();
while(true) yield();
}
public boolean ValidateRegistrant(ClientAddr
user)
{
return true;
}
public void NewRegistrant(ClientAddr user)
{
servThread.sendToUsers("CLIENTS
" + servThread.Clients.size());
}
public void DropRegistrant(ClientAddr
user)
{
servThread.sendToUsers("CLIENTS
" + servThread.Clients.size());
}
public void recvNewData(byte[] newDataBlock,
DatagramPacket fromWho)
{
System.out.println("Receive
data block...discarding");
}
}
The run() method starts the
DGTP server and then enters into an infinite while
loop. It calls the yield()
function to avoid interfering with other active threads.
The thread is now added to the BasicWebServer class in the start()
method:
// Create the server socket
serverSocket = new ServerSocket(HTTP_PORT, 5);
// Create and start any additional
// server thread services here
st = new NumUsersServer(4545);
st.start();
The project is now finished, so compile all the source code and
start the server. If you maintained the directory structure of
the CD-ROM, you should be able to start the server and connect
to it. The client applet classes are under htdocs/classes. The
default HTML document is in htdocs/index.html.
In this chapter, you have learned about socket abstraction as
well as the Java implementation of sockets. After some basic client/server
applications, a full HTTP server is undertaken. You should have
a working knowledge of HTTP and an appreciation for socket applet
security. The last part of the chapter introduces the DGTP protocol
for applet/server interaction. This protocol will be reused in
Chapter 11, "Building a Live Data
Applet," as the basis for a live data server, but first,
you must learn to work with native methods for database access.

Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.