All Categories :
Java
Chapter 26
Java Socket Programming
by Stephen Ingram
CONTENTS
For full Java client/server applet connectivity, an applet server
is required. This chapter initiates the development of a Java
HTTP server. Before beginning the development of the server, however,
you need some background knowledge of socket programming. This
chapter begins with a socket overview followed by an exploration
of Java's socket classes. The remainder of the chapter focuses
on the construction of a Java HTTP Web server.
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 capable of applying 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 may 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, each
uses 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 server's destination
address and 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 process is known as binding to a port.
Socket Transmission Modes
Sockets have two major modes of operation: connection-oriented
and connectionless modes. Connection-oriented sockets operate
like a telephone: they must establish a connection and then 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.
The mode you use is determined by an application's needs. If reliability
is important, connection-oriented operation is better. File servers
must have all their data arrive correctly and in sequence. If
some data is lost, the server's usefulness is invalidated. Some
applications-time servers, for example-send discrete chunks of
data at regular intervals. If data were to get lost, the server
would not want the network to retry because by the time the resent
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 have 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 must 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 operation, 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. This chapter
discusses the connection-oriented classes 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 we cover it first.
Listing 26.1 shows a simple client application. It requests an
HTML document from a server and displays the response to the console.
Listing 26.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.
*/
public class SimpleWebClient {
public static void main(String args[])
{
try
{
// Open a client socket connection
Socket clientSocket1 = new Socket("www.javasoft.com", 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 to avoid security restrictions. Run the code from the command line java ClassName.
|
Recall that a client socket issues a connect call 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, port number 80
was specified, the well-known port number for the HTTP protocol.
Other well-known port numbers are shown in Table 26.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 26.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. The following code snippet requests a Web page and
echoes the response to the screen:
outbound.writeBytes("GET / HTTP/1.0\r\n\r\n);
String responseLine;
while ( (responseLine = inbound.readLine()) != null)
{
System.out.println(responseLine);
}
When the program is done using the socket, the connection must
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 than using
a client socket, as explained in the following section.
Server Sockets
Listing 26.2 is a partial listing of a simple server application.
The complete server
example can be found on the CD-ROM that accompanies this book
in SimpleWebServer.java.
Listing 26.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 second 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 preceding 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 spawns a new
socket in the accept() method.
The server socket is still open and queuing new connection requests.
As you do with 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 waits for one to
arrive. This behavior is known as blocking. The accept()
method blocks 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
are canceled.
Note |
The SimpleServer application produces no output. To exercise it, you can either use a browser or the SimpleClient application from Listing 26.1. Both cases require the current machine's name. You can substitute localhost if you are unsure of your machine name. Browsers should be pointed to http://localhost/.
|
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 26.1 summarizes the steps needed for client/server connection-oriented
applications.
Figure 26.1 : Client and server connection-oriented applications.
Iterative and Concurrent Servers
The client/server application just presented is known as an iterative
server because the code accepts a client connection and completely
processes it before it accepts another connection. More complex
servers are concurrent servers: 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 the client and server portions of connection-oriented classes,
the datagram versions of the client and server behave in nearly
identical manners-the only difference occurs in implementation.
For the datagram model, 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. Because the client
calls 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.
Because streams can't be acquired for communication, how do you
talk to a DatagramSocket
object? 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 must know where to place the received
data. A 512-byte buffer is created and passed to the constructor
as the first parameter. The second constructor parameter is the
size of the buffer. Like the accept()
method in the ServerSocket
class, the receive() method
blocks 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 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 to send 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("localhost");
InetAddress addr2[] = InetAddress.getAllByName("localhost");
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 is 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. Then a new DatagramPacket
instance must be created. Notice the two extra parameters at the
end of the constructor. Because 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 26.3 shows a simple datagram echo server. This server
echoes back any packets it
receives.
Listing 26.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 incoming data.
- Go to step 3 (send more data), step 4 (wait for incoming data),
or step 6 (exit).
- Close the datagram socket.
Figure 26.2 summarizes the steps needed for client/server datagram
applications. The symmetry between client and server is evident
in this figure; compare Figure 26.2 with Figure 26.1.
Figure 26.2 : Client and server datagram applications.
Listing 26.4 shows a simple datagram client. It reads user input
strings and sends them to the echo server from Listing 26.3. The
echo server sends the data right back, and the client prints the
response to the console.
Listing 26.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("localhost");
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.
Applet security and sockets |
When writing applications, you don't have to be concerned with security exceptions. This changes when the code under development is executed from an applet. Browsers use very stringent security measures where sockets are concerned. An applet can open a socket only back to the host name from which it was loaded. If any other connection is attempted, a SecurityException is 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.
|
Client applets need an HTTP Web server so that they can open sockets.
If an applet is loaded into a browser from a hard drive, no socket
activity is allowed to take place. This presents a significant
hurdle to Java client applet development and testing. 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.
HTTP Primer
Before diving into the project, you need some background information
about 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. Our server will support
a subset of HTTP version 1.0 in that only file requests will be
handled. As long as browser 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
26.1 and 26.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 26.2 lists all the request methods for HTTP/1.0.
Table 26.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 produces 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 26.3 lists all the defined status codes. The server sends
only two of these: 200 and
404. The text that follows
the status code is optional. It may be omitted; if it is present,
it might not match the definitions given in the table.
Table 26.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 for you to construct a basic Web server.
Full information on the HTTP protocol can be retrieved from this
URL:
HTTP://www.w3.org/
A Basic Web Server
The basic Web server follows the construction of the SimpleWebServer
from Listing 26.2. Many improvements will 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.
The HTTPrequest Class
Listing 26.5 shows the complete HTTPrequest
class. The class must contain all the information that could be
conveyed in a request header.
Listing 26.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 in NameValue.java
on the CD-ROM that accompanies this book. When you want to add
a new pair, 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; the inbound
member allows easy closure after a request has been processed.
The remaining members are all part of an HTTP request. The toString()
method allows class objects to be printed using "plus notation."
The following line displays 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.
The BasicWebServer Class
The BasicWebServer class
is the main class for the server. It can be divided into request
and response routines. Because this is a server, the request routines
are activated first. After some validation, the response routines
are called. Listing 26.6 shows the routines used to parse an HTTP
request.
Listing 26.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)
{
// Retreive 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 readHeader() method interrogates
the inbound socket stream searching for the blank line. If the
request is not of HTTP/1.0 format, this method throws an exception.
Otherwise, the resulting string is passed to parseReqHdr()
for processing.
These routines 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 is preferable to close the inbound stream as soon
as the request has been completely read. If this is done, 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 are closed.
Caution |
Do not be tempted to close an inbound stream after all input has been read. Closing the input stream causes 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
26.7 presents the response routines used by the server.
Listing 26.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 browsers 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 are needed for applet loading, although 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 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 happens because ProtocolException
is a child class of IOException.
If ProtocolException 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 that accompanies this book.
The remaining code calls the input routine getRequest()
and then the output routine implementMethod()
for each client connection.
The project is now finished; compile all the source code and start
the server. If you maintained the directory structure used on
the CD-ROM that accompanies this book, you should be able to start
the server and connect to it. The client applet classes are contained
under htdocs/classes. The
default HTML document is in htdocs/index.html.
In this chapter, you learned about the socket abstraction as well
as the Java implementation of sockets. Remember that socket use
requires at least two applications: a client and a server. The
server waits for a client application to call and request attention.
Multiple clients can make use of the same server-either at the
same time (concurrent server) or one at a time (iterative server).
Server behavior was demonstrated with the development of an iterative
Java HTTP server. You should now have a working knowledge of HTTP
and an appreciation for the limitations imposed by the socket
security model. Namely, an applet can only open a socket back
to the same server that loaded the applet.
Sockets provide a rich communications medium that allow your Java
applications to exploit a wired world.
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.