|
The Developer's Resource & Community Site
|
Java Network Programming, 2nd Edition: Sockets for Servers
Reproduced with kind permision of O'Reilly & Associates: www.oreilly.com
Contents:
Sockets for
Servers
In this chapter: The
ServerSocket Class Some
Useful Servers
The last chapter discussed sockets from the standpoint of
clients: programs that open a Socket to a server that's listening for
connections. However, client sockets themselves aren't enough; clients aren't
much use unless they can talk to a server, and if you think about it, the
sockets we discussed in the last chapter aren't sufficient for writing
servers. To create a Socket, you need to know the
Internet host to which you want to connect. When you're writing a server, you
don't know in advance who will contact you, and even if you did, you wouldn't
know when that host wanted to contact you. In other words, servers are like
receptionists who sit by the phone and wait for incoming calls. They don't
know who will call or when, only that when the phone rings, they have to pick
it up and talk to whoever is there. We can't program that behavior with the
Socket class alone. Granted, there's no reason that
clients written in Java have to talk to Java servers--in fact, a client
doesn't care what language the server was written in or what platform it runs
on. However, if Java didn't let us write servers, there would be a glaring
hole in its capabilities.
Fortunately, there's no such hole. Java provides a
ServerSocket class to allow programmers to write servers.
Basically, a server socket's job is to sit by the phone and wait for incoming
calls. More technically, a ServerSocket runs on the
server and listens for incoming TCP connections. Each
ServerSocket listens on a particular port on the server
machine. When a client Socket on a remote host
attempts to connect to that port, the server wakes up, negotiates the
connection between the client and the server, and opens a regular
Socket between the two hosts. In other words, server
sockets wait for connections while client sockets initiate connections. Once
the server Socket has set up the connection, the server uses a regular
Socket object to send data to the client. Data always
travels over the regular socket.
The ServerSocket Class
The ServerSocket class contains
everything you need to write servers in Java. It has constructors that create
new ServerSocket objects, methods that listen for
connections on a specified port, and methods that return a
Socket object when a connection is made so that you can
send and receive data. In addition, it has methods to set various options and
the usual miscellaneous methods such as toString(
).
The basic life cycle of a server is:
- A new ServerSocket is
created on a particular port using a ServerSocket(
) constructor.
- The ServerSocket listens for
incoming connection attempts on that port using its
accept( ) method. accept( )
blocks until a client attempts to make a connection, at which point
accept( ) returns a Socket
object connecting the client and the server.
- Depending on the type of server, either the
Socket's getInputStream( )
method, getOutputStream( ) method, or both are
called to get input and output streams that communicate with the client.
- The server and the client interact according to an
agreed-upon protocol until it is time to close the connection.
- The server, the client, or both close the connection.
- The server returns to step 2 and waits for the next
connection.
If step 4 is likely to take a long or indefinite amount of time,
traditional Unix servers such as wu-ftpd create a new process to handle each
connection so that multiple clients can be serviced at the same time. Java
programs should spawn a thread to interact with the client so that the server
can be ready to process the next connection sooner. A thread places a far
smaller load on the server than a complete child process. In fact, the
overhead of forking too many processes is why the typical Unix FTP server
can't handle more than roughly 400 connections without slowing to a crawl. On
the other hand, if the protocol is simple and quick and allows the server to
close the connection when it's through, then it will be more efficient for the
server to process the client request immediately without spawning a
thread.
The operating system stores incoming connection requests
addressed to a particular port in a first-in, first-out queue. The default
length of the queue is normally 50, though this can vary from operating system
to operating system. Some operating systems (though not Solaris) have a
maximum queue length, typically five. On these systems, the queue length will
be the largest possible value less than or equal to 50. After the queue fills
to capacity with unprocessed connections, the host refuses additional
connections on that port until slots in the queue open up. Many (though not
all) clients will try to make a connection multiple times if their initial
attempt is refused. Managing incoming connections and the queue is a service
provided by the operating system; your program does not need to worry about
it. Several ServerSocket constructors allow you to
change the length of the queue if its default length isn't large enough;
however, you won't be able to increase the queue beyond the maximum size that
the operating system supports:
The Constructors
There are three public ServerSocket
constructors:
public ServerSocket(int port) throws IOException, BindException
public ServerSocket(int port, int queueLength)
throws IOException, BindException
public ServerSocket(int port, int queueLength, InetAddress bindAddress)
throws IOException
These constructors let you specify the port, the length of the
queue used to hold incoming connection requests, and the local network
interface to bind to. They pretty much all do the same thing, though some use
default values for the queue length and the address to bind to. Let's explore
these in order.
public ServerSocket(int port) throws IOException, BindException
This constructor creates a server Socket on the port specified
by the argument. If you pass 0 for the port number, the system selects an
available port for you. A port chosen for you by the system is sometimes
called an anonymous port since you don't know its
number. For servers, anonymous ports aren't very useful because clients need
to know in advance which port to connect to; however, there are a few
situations (which we will discuss later) in which an anonymous port might be
useful.
For example, to create a server Socket that would be used by an
HTTP server on port 80, you would write:
try {
ServerSocket httpd = new ServerSocket(80);
}
catch (IOException e) {
System.err.println(e);
}
The constructor throws an IOException
(specifically, a BindException) if the Socket
cannot be created and bound to the requested port. An
IOException when creating a
ServerSocket almost always means one of two things.
Either another server socket, possibly from a completely different program, is
already using the requested port, or you're trying to connect to a port from 1
to 1023 on Unix without root (superuser) privileges.
You can use this constructor to write a variation on the
PortScanner programs of the previous chapter. Example 11-1 checks for ports on the local machine by attempting to create
ServerSocket objects on them and seeing on which ports
that fails. If you're using Unix and are not running as root, this program
works only for ports 1,024 and above.
Example 11-1: Look for Local Ports
import java.net.*;
import java.io.*;
public class LocalPortScanner {
public static void main(String[] args) {
for (int port = 1; port <= 65535; port++) {
try {
// the next line will fail and drop into the catch block if
// there is already a server running on the port
ServerSocket server = new ServerSocket(port);
}
catch (IOException e) {
System.out.println("There is a server on port " + port + ".");
} // end try
} // end for
}
}
Here's the output I got when running LocalPortScanner on my NT workstation:
D:\JAVA\JNP2\examples\11> java LocalPortScanner
There is a server on port 135.
There is a server on port 1025.
There is a server on port 1026.
There is a server on port 1027.
There is a server on port 1028.
public ServerSocket(int port, int queueLength) throws IOException, BindException
This constructor creates a ServerSocket on the specified port with a queue length of our choosing. If the machine has multiple network interfaces or IP addresses, then it listens on this port on all those interfaces and IP addresses. The queueLength argument sets the length of the queue for incoming connection requests--that is, how many incoming connections can be stored at one time before the host starts refusing connections. Some operating systems have a maximum queue length, typically five. If you try to expand the queue past that maximum number, the maximum queue length is used instead. If you pass 0 for the port number, the system selects an available port.
For example, to create a server Socket on port 5,776 that would
hold up to 100 incoming connection requests in the queue, you would write: try {
ServerSocket httpd = new ServerSocket(5776, 100);
}
catch (IOException e) {
System.err.println(e);
}
The constructor throws an IOException
(specifically, a BindException) if the Socket
cannot be created and bound to the requested port. An
IOException when creating a
ServerSocket almost always means one of two things.
Either the specified port is already in use, or you do not have root
privileges on Unix and you're trying to connect to a port from 1 to 1,023.
public ServerSocket(int port, int queueLength, InetAddress
bindAddress) throws BindException, IOException
This constructor, which is available only in Java 1.1 and later,
creates a ServerSocket on the specified port with
the specified queue length. This ServerSocket binds
only to the specified local IP address. This constructor is useful for servers
that run on systems with several IP addresses (a common practice at web server
farms) because it allows you to choose the address to which you'll listen.
That is, this ServerSocket listens only for
incoming connections on the specified address; it won't listen for connections
that come in through the host's other addresses. The other constructors bind
to all local IP addresses by default.
For example, metalab.unc.edu is a particular SPARCstation
in North Carolina. It's connected to the Internet with the IP address
152.2.254.81. The same SPARCstation is also called www.gigabit-ethernet.org, but with a different IP address
(152.2.254.82). To create a server Socket that listens on port 5,776 of
metalab.unc.edu but not on port 5,776 of www.gigabit-ethernet.org, you would write: try {
ServerSocket httpd = new ServerSocket(5776, 10,
InetAddress.getHostByName("metalab.unc.edu"));
}
catch (IOException e) {
System.err.println(e);
}
The constructor throws an IOException
(again, really a BindException) if the Socket
cannot be created and bound to the requested port. A
BindException when creating a
ServerSocket almost always means one of two things.
Either the specified port is already in use, or you do not have root
privileges on Unix and you're trying to connect to a port from 1 to 1,023.
Accepting and Closing Connections
A ServerSocket generally operates in
a loop that repeatedly accepts connections. Each pass through the loop invokes
the accept( ) method. This returns a
Socket object representing the connection between the
remote client and the local server. Interaction with the client takes place
through this Socket object. When the transaction is
finished, the server should invoke the Socket
object's qclose( ) method and get ready to process
the next incoming connection. However, when the server needs to shut down and
not process any further incoming connections, you should invoke the
ServerSocket object's qclose( )
method.
public Socket accept( ) throws IOException
When server setup is done and you're ready to accept a
connection, call the ServerSocket's
accept( ) method. This method "blocks": it stops the flow
of execution and waits until a client connects. When a client does connect,
the accept( ) method returns a
Socket object. You use the streams returned by this
Socket's getInputStream( ) and
getOutputStream( ) methods to communicate with the
client. For example: ServerSocket server = new ServerSocket(5776);
while (true) {
Socket connection = server.accept( );
OutputStreamWriter out
= new OutputStreamWriter(connection.getOutputStream( ));
out.write("You've connected to this server. Bye-bye now.\r\n");
connection.close( );
}
If you don't want your program to halt while it waits for a
connection, put the call to accept( ) in a separate
thread.
When you add exception handling, the code becomes somewhat more
convoluted. It's important to distinguish between exceptions thrown by the
ServerSocket, which
should probably shut down the server and log an error message, and exceptions
thrown by a Socket,
which should just close that active connection. Exceptions thrown by
the accept( ) method are an intermediate case that
can go either way. To do this, you'll need to nest your
try blocks. Finally, most servers will want to make sure
that all sockets they accept are closed when they're finished. Even if the
protocol specifies that clients are responsible for closing connections,
clients do not always strictly adhere to the protocol. The call to
qclose( ) also has to be wrapped in a
try block that catches an
IOException. However, if you do catch an
IOException when closing the socket, ignore it. It just
means that the client closed the Socket before the server could. Here's a
slightly more realistic example: try {
ServerSocket server = new ServerSocket(5776);
while (true) {
Socket connection = server.accept( );
try {
OutputStreamWriter out
= new OutputStreamWriter(connection.getOutputStream( ));
out.write("You've connected to this server. Bye-bye now.\r\n");
connection.close( );
}
catch (IOException e) {
// This tends to be a transitory error for this one connection;
// e.g. the client broke the connection early. Consequently,
// we don't want to break the loop or print an error message.
// However, you might choose to log this exception in an error log.
}
finally {
// Most servers will want to guarantee that sockets are closed
// when complete.
try {
if (connection != null) connection.close( );
}
catch (IOException e) {}
}
}
catch (IOException e) {
System.err.println(e);
}
Example 11-2 implements a simple daytime server, as per RFC 867. Since this server
just sends a single line of text in response to each connection, it processes
each connection immediately. More complex servers should spawn a thread to
handle each request. In this case, the overhead of spawning a thread would be
greater than the time needed to process the request.
NOTE: If you run this program on a Unix box, you need
to run it as root in order to connect to port 13. If you don't want to or
can't run it as root, change the port number to something above 1024, say
1313.
Example 11-2: A Daytime Server
import java.net.*;
import java.io.*;
import java.util.Date;
public class DaytimeServer {
DEFAULT_PORT DEFAULT_PORT = 13;
public static void main(String[] args) {
int port = DEFAULT_PORT;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
if (port < 0 || port >= 65536) {
System.out.println("Port must between 0 and 65535");
return;
}
}
catch (NumberFormatException e) {
// use default port
}
}
try {
ServerSocket server = new ServerSocket(port);
Socket connection = null;
while (true) {
try {
connection = server.accept( );
OutputStreamWriter out
= new OutputStreamWriter(connection.getOutputStream( ));
Date now = new Date( );
out.write(now.toString( ) +"\r\n");
out.flush( );
connection.close( );
}
catch (IOException e) {}
finally {
try {
if (connection != null) connection.close( );
}
catch (IOException e) {}
}
} // end while
} // end try
catch (IOException e) {
System.err.println(e);
} // end catch
} // end main
} // end DaytimeServer
Example 11-2 is straightforward. The first three lines import the usual packages,
java.io and java.net, as
well as java.util.Date so we can get the time.
There is a single DEFAULT_PORT field
(i.e., a constant) in the class DEFAULT_PORT, which
is set to the well-known port for a daytime server (port 13). The class has a
single method, main( ), which does all the work. If
the port is specified on the command-line, then it's read from
args[0]. Otherwise, the default port is used.
The outer try block traps any
IOExceptions that may arise while the
ServerSocket server is constructed on the daytime port or
when it accepts connections. The inner try block
watches for exceptions thrown while the connections are accepted and
processed. The accept( ) method is called within an
infinite loop to watch for new connections; like many servers, this program
never terminates but continues listening until an exception is thrown or you
stop it manually.[1]
When a client makes a connection, accept(
) returns a Socket, which is stored in the
local variable connection, and the program
continues. We call getOutputStream( ) to get the
output stream associated with that Socket and then
chain that output stream to a new
OutputStreamWriter, out. To
get the current Date, we construct a new Date
object and send it to the client by writing its string representation on
out with write( ).
Finally, after the data is sent or an exception has been thrown,
we close connection inside the
finally block. Always close a Socket when you're finished
with it. In the previous chapter, we said that a client shouldn't rely on the
other side of a connection to close the socket. That goes triple for servers.
Clients can time out or crash; users can cancel transactions; networks can go
down in high-traffic periods. For any of these or a dozen more reasons, you
cannot rely on clients to close sockets, even when the protocol requires them
to (which it doesn't in this case).
Sending binary, nontext data is not significantly harder. Example 11-3 demonstrates with a time server. This follows the time protocol
outlined in RFC 868. When a client connects, the server sends a 4-byte,
big-endian, unsigned integer specifying the number of seconds that have passed
since 12:00 A.M., January 1, 1900 GMT (the epoch). The current time can be
retrieved simply by creating a new Date object.
However, since the Date class counts milliseconds
since 12:00 A.M., January 1, 1970 GMT rather than seconds since 12:00 A.M.,
January 1, 1900 GMT, some conversion is necessary.
Example 11-3: A Time Server import java.net.*;
import java.io.*;
import java.util.Date;
public class TimeServer {
DEFAULT_PORT DEFAULT_PORT = 37;
public static void main(String[] args) {
int port = DEFAULT_PORT;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
if (port < 0 || port >= 65536) {
System.out.println("Port must between 0 and 65535");
return;
}
}
catch (NumberFormatException e) {}
}
// The time protocol sets the epoch at 1900,
// the java Date class at 1970. This number
// converts between them.
long differenceBetweenEpochs = 2208988800L;
try {
ServerSocket server = new ServerSocket(port);
while (true) {
Socket connection = null;
try {
connection = server.accept( );
OutputStream out = connection.getOutputStream( );
Date now = new Date( );
long msSince1970 = now.getTime( );
long secondsSince1970 = msSince1970/1000;
long secondsSince1900 = secondsSince1970
+ differenceBetweenEpochs;
byte[] time = new byte[4];
time[0]
= (byte) ((secondsSince1900 & 0x00000000FF000000L) >> 24);
time[1]
= (byte) ((secondsSince1900 & 0x0000000000FF0000L) >> 16);
time[2]
= (byte) ((secondsSince1900 & 0x000000000000FF00L) >> 8);
time[3] = (byte) (secondsSince1900 & 0x00000000000000FFL);
out.write(time);
out.flush( );
} // end try
catch (IOException e) {
} // end catch
finally {
if (connection != null) connection.close( );
}
} // end while
} // end try
catch (IOException e) {
System.err.println(e);
} // end catch
} // end main
} // end TimeServer
As with the TimeClient of the
previous chapter, most of the effort here goes into working with a data format
(32-bit unsigned integers) that Java doesn't natively support.
public void qclose( ) throws IOException
If you're finished with a server socket, you should close it,
especially if your program is going to continue to run for some time. This
frees up the port for other programs that may wish to use it. Closing a
ServerSocket should not be confused with closing a
Socket. Closing a ServerSocket
frees a port on the local host, allowing another server to bind to the port;
closing a Socket breaks the connection between the
local and the remote hosts.
Server sockets are closed automatically when a program dies, so
it's not absolutely necessary to close them in programs that terminate shortly
after the ServerSocket is no longer needed.
Nonetheless, it doesn't hurt. For example, the main loop of the
LocalPortScanner program might be better written like
this so that it doesn't temporarily occupy most of the ports on the
system:
for (int port = 1; port <= 65535; port++) {
try {
// the next line will fail and drop into the catch block if
// there is already a server running on the port
ServerSocket server = new ServerSocket(port);
server.close( );
}
catch (IOException e) {
System.out.println("There is a server on port " + port + ".");
} // end try
} // end for
The get Methods
The ServerSocket class provides two
getter methods to tell you the local address and port occupied by the server
socket. These are useful if you've opened a server Socket on an anonymous port
and/or an unspecified network interface. This would be the case, for one
example, in the data connection of an FTP session.
public InetAddress getInetAddress( )
This method returns the address being used by the server (the
local host). If the local host has a single IP address (as most do), then this
is the address returned by InetAddress.getLocalHost(
). If the local host has more than one IP address, then the specific
address returned is one of the host's IP addresses. You can't predict which
address you will get. For example: try {
ServerSocket httpd = new ServerSocket(80);
InetAddress ia = httpd.getInetAddress( );
}
catch (IOException e) {
}
public int getLocalPort( )
The ServerSocket constructors allow
you to listen on an unspecified port by passing 0 for the port number. This
method lets you find out what port you're listening on. You might use this in
a peer-to-peer multisocket program where you already have a means to inform
other peers of your location. Or a server might spawn several smaller servers
to perform particular operations. The well-known server could inform clients
what ports they can find the smaller servers on. Of course, you can also use
getLocalPort( ) to find a non-anonymous port, but
why would you need to? Example 11-4 demonstrates.
Example 11-4: A Random Port
import java.net.*;
import java.io.*;
public class RandomPort {
public static void main(String[] args) {
try {
ServerSocket server = new ServerSocket(0);
System.out.println("This server runs on port "
+ server.getLocalPort( ));
}
catch (IOException e) {
System.err.println(e);
}
}
}
Here's the output of several runs: D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1154
D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1155
D:\JAVA\JNP2\examples\11> java RandomPort
This server runs on port 1156
At least on this VM, the ports aren't really random; but they
are at least indeterminate until runtime.
Socket Options
The only Socket option supported for server sockets is
SO_TIMEOUT. SO_TIMEOUT is the amount of time, in milliseconds, that
accept( ) waits for an incoming connection before
throwing a java.io.InterruptedIOException. If
SO_TIMEOUT is 0, then accept( ) will never time
out. The default is to never time out.
Using SO_TIMEOUT is rather rare. You might need it if you were
implementing a complicated and secure protocol that required multiple
connections between the client and the server where some responses needed to
occur within a fixed amount of time. Most servers are designed to run for
indefinite periods of time and therefore use the default timeout value, which
is 0 (never time out).
public void setSoTimeout(int timeout) throws SocketException
The setSoTimeout( ) method sets the
SO_TIMEOUT field for this server Socket object. The countdown starts when
accept( ) is invoked. When the timeout expires,
accept( ) throws an
InterruptedIOException. You should set this option before
calling accept( ); you cannot change the timeout
value while accept( ) is waiting for a connection.
The timeout argument must be greater than or equal
to zero; if it isn't, the method throws an
IllegalArgumentException. For example:
try {
ServerSocket server = new ServerSocket(2048);
server.setSoTimeout(30000); // block for no more than 30 seconds
try {
Socket s = server.accept( );
// handle the connection
// ...
}
catch (InterruptedIOException e) {
System.err.println("No connection within 30 seconds");
}
finally {
server.close( );
}
catch (IOException e) {
System.err.println("Unexpected IOException: " + e);
}
public int getSoTimeout( ) throws IOException
The getSoTimeout( ) method returns this server socket's current SO_TIMEOUT value. For example: public void printSoTimeout(ServerSocket server) {
int timeout = server.getSoTimeOut( );
if (timeout > 0) {
System.out.println(server + " will time out after "
+ timeout + "milliseconds.");
}
else if (timeout == 0) {
System.out.println(server + " will never time out.");
}
else {
System.out.println("Impossible condition occurred in " + server);
System.out.println("Timeout cannot be less than zero." );
}
}
The Object Methods
jServerSocket overrides only one of
the standard methods from java.lang.Object,
toString( ). Thus, equality comparisons test for strict
identity, and server sockets are problematic in hash tables. Normally, this
isn't a large problem.
public String toString( )
A String returned by
ServerSocket's toString( )
method looks like this:
ServerSocket[addr=0.0.0.0,port=0,localport=5776]
In current implementations, addr is
always 0.0.0.0 and port is always 0. Presumably,
these may become something more interesting in the future. The
localport is the local port on which the server is
listening for connections.
Implementation
The ServerSocket class provides two
methods for changing the default implementation of server sockets. I'll
describe them only briefly here, since they're primarily intended for
implementers of Java virtual machines rather than application programmers.
public static synchronized void setSocketFactory (SocketImpl
Factory fac) throws IOException
This method sets the system's server
SocketImplFactory, which is the factory used to
create ServerSocket objects. This is not the same
factory that is used to create client Socket
objects, though the syntax is similar; you can have one factory for
Socket objects and a different factory for
ServerSocket objects. You can set this factory only once
in a program, however. A second attempt to set the
SocketImplFactory throws a
SocketException.
Protected final void implAccept(Socket s) throws
IOException
Subclasses of ServerSocket use this
method to implement accept( ). You pass an
unconnected Socket object to
implaccept( ). (Doing this requires you to subclass
Socket as well since the standard
java.net.Socket class doesn't provide a means to create
unconnected sockets.) When the method returns, the
Socket argument s is connected
to a client.
Some Useful Servers
This section shows several servers you can build with server
sockets. It starts with a server you can use to test client responses and
requests, much as you use Telnet to test server behavior. Then we present
three different HTTP servers, each with a different special purpose and each
slightly more complex than the previous one.
Client Tester
In the previous chapter, you learned how to use Telnet to
experiment with servers. There's no equivalent program to test clients, so
let's create one. Example 11-5 is a program called ClientTester that runs
on a port specified on the command-line, shows all data sent by the client, and
allows you to send a response to the client by typing it on the command line.
For example, you can use this program to see the commands that Netscape
Navigator sends to a server.
NOTE: Clients are rarely as forgiving about unexpected
server responses as servers are about unexpected client responses. If at all
possible, try to run the clients that connect to this program on a Unix
system or some other platform that is moderately crash-proof. Don't run them
on a Mac or Windows 98, which are less stable.
This program uses two threads: one to handle input from the
client and the other to send output from the server. Using two threads allows
the program to handle input and output simultaneously: it can be sending a
response to the client while receiving a request--or, more to the point, it
can send data to the client while waiting for the client to respond. This is
convenient because different clients and servers talk in unpredictable ways.
With some protocols, the server talks first; with others, the client talks
first. Sometimes the server sends a one-line response; often, the response is
much larger. Sometimes the client and the server talk at each other
simultaneously. Other times, one side of the connection waits for the other to
finish before it responds. The program must be flexible enough to handle all
these cases. Example 11-5 shows the code.
Example 11-5: A Client Tester
import java.net.*;
import java.io.*;
import com.macfaq.io.SafeBufferedReader; // from Chapter 4
public class ClientTester {
public static void main(String[] args) {
int port;
try {
port = Integer.parseInt(args[0]);
}
catch (Exception e) {
port = 0;
}
try {
ServerSocket server = new ServerSocket(port, 1);
System.out.println("Listening for connections on port "
+ server.getLocalPort( ));
while (true) {
Socket connection = server.accept( );
try {
System.out.println("Connection established with "
+ connection);
Thread input = new InputThread(connection.getInputStream( ));
input.start( );
Thread output
= new OutputThread(connection.getOutputStream( ));
output.start( );
// wait for output and input to finish
try {
input.join( );
output.join( );
}
catch (InterruptedException e) {
}
}
catch (IOException e) {
System.err.println(e);
}
finally {
try {
if (connection != null) connection.close( );
}
catch (IOException e) {}
}
}
}
catch (IOException e) {
e.printStackTrace( );
}
}
}
class InputThread extends Thread {
InputStream in;
public InputThread(InputStream in) {
this.in = in;
}
public void run( ) {
try {
while (true) {
int i = in.read( );
if (i == -1) break;
System.out.write(i);
}
}
catch (SocketException e) {
// output thread closed the socket
}
catch (IOException e) {
System.err.println(e);
}
try {
in.close( );
}
catch (IOException e) {
}
}
}
class OutputThread extends Thread {
Writer out;
public OutputThread(OutputStream out) {
this.out = new OutputStreamWriter(out);
}
public void run( ) {
String line;
BufferedReader in
= new SafeBufferedReader(new InputStreamReader(System.in));
try {
while (true) {
line = in.readLine( );
if (line.equals(".")) break;
out.write(line +"\r\n");
out.flush( );
}
}
catch (IOException e) {
}
try {
out.close( );
}
catch (IOException e) {
}
}
}
The client tester application is split into three classes:
ClientTester, InputThread, and
OutputThread. The
ClientTester class reads the port from the command-line,
opens a ServerSocket on that port, and listens for
incoming connections. Only one connection is allowed at a time, because this
program is designed for experimentation, and a slow human being has to provide
all responses. Consequently, we set an unusually short queue length of 1.
Further connections will be refused until the first one has been closed.
An infinite while loop waits for
connections with the accept( ) method. When a
connection is detected, its InputStream is used to
construct a new InputThread, and its
OutputStream is used to construct a new
OutputThread. After starting these threads, we wait for
them to finish by calling their join( )
methods.
The InputThread is contained almost
entirely in the run( ) method. It has a single
field, in, which is the
InputStream from which data will be read. Data is read
from in one byte at a time. Each
byte read is written on
System.out. The run( ) method
ends when the end of stream is encountered or an
IOException is thrown. The most likely exception here is
a SocketException thrown because the corresponding
OutputThread closed the connection.
The OutputThread reads input from the
local user sitting at the terminal and sends that data to the client. Its
constructor has a single argument, an output stream for sending data to the
client. OutputThread reads input from the user on
System.in, which is chained to an instance of the
SafeBufferedReader class developed in Chapter 4, Java I/O. The
OutputStream that was passed to the constructor is
chained to an OutputStreamWriter for convenience.
The run( ) method for
OutputThread reads lines from the
SafeBufferedReader, and copies them onto the
OutputStreamWriter, which sends them to the client. A
period typed on a line by itself signals the end of user input. When this
occurs, run( ) exits the loop and
out is closed. This has the effect of also closing the
socket, so that a SocketException is thrown in the
input thread, which also exits.
For example, here's the output when Netscape Communicator 4.6
for Windows connected to this server:
D:\JAVA\JNP2\examples\11> java ClientTester 80
Listening for connections on port 80
Connection established with
Socket[addr=localhost/127.0.0.1,port=1033,localport=80]
GET / HTTP 1.0
Connection: Keep-Alive
User-Agent: Mozilla/4.6 [en] (WinNT; I)
Host: localhost
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Encoding: gzip
Accept-Language: en
Accept-Charset: iso-8859-1,*,utf-8
<html><body><h1>Hello Client!</h2></body></html>
.
|