Sockets for Servers - Java Network Programming, 4th Edition (2013)

Java Network Programming, 4th Edition (2013)

Chapter 9. Sockets for Servers

The previous 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 the Socket class discussed in the previous chapter is not 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. You can’t program that behavior with the Socket class alone.

For servers that accept connections, Java provides a ServerSocket class that represents server sockets. In essence, a server socket’s job is to sit by the phone and wait for incoming calls. More technically, a server socket runs on the server and listens for incoming TCP connections. Each server socket listens on a particular port on the server machine. When a client on a remote host attempts to connect to that port, the server wakes up, negotiates the connection between the client and the server, and returns a regular Socket object representing the socket between the two hosts. In other words, server sockets wait for connections while client sockets initiate connections. Once a ServerSocket 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.

Using ServerSockets

The ServerSocket class contains everything needed to write servers in Java. It has constructors that create new ServerSocket objects, methods that listen for connections on a specified port, methods that configure the various server socket options, and the usual miscellaneous methods such as toString().

In Java, the basic life cycle of a server program is this:

1. A new ServerSocket is created on a particular port using a ServerSocket() constructor.

2. 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.

3. 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.

4. The server and the client interact according to an agreed-upon protocol until it is time to close the connection.

5. The server, the client, or both close the connection.

6. The server returns to step 2 and waits for the next connection.

Let’s demonstrate with one of the simpler protocols, daytime. Recall from the Chapter 8 that a daytime server listens on port 13. When a client connects, the server sends the time in a human-readable format and closes the connection. For example, here’s a connection to the daytime server attime-a.nist.gov:

$ telnet time-a.nist.gov 13

Trying 129.6.15.28...

Connected to time-a.nist.gov.

Escape character is '^]'.

56375 13-03-24 13:37:50 50 0 0 888.8 UTC(NIST) *

Connection closed by foreign host.

Implementing your own daytime server is easy. First, create a server socket that listens on port 13:

ServerSocket server = new ServerSocket(13);

Next, accept a connection:

Socket connection = server.accept();

The accept() call blocks. That is, the program stops here and waits, possibly for hours or days, until a client connects on port 13. When a client does connect, the accept() method returns a Socket object.

Note that the connection is returned a java.net.Socket object, the same as you used for clients in the previous chapter. The daytime protocol requires the server (and only the server) to talk, so get an OutputStream from the socket. Because the daytime protocol requires text, chain this to an OutputStreamWriter:

OutputStream out = connection.getOutputStream();

Writer writer = new OutputStreamWriter(writer, "ASCII");

Now get the current time and write it onto the stream. The daytime protocol doesn’t require any particular format other than that it be human readable, so let Java pick for you:

Date now = new Date();

out.write(now.toString() +"\r\n");

Do note, however, the use of a carriage return/linefeed pair to terminate the line. This is almost always what you want in a network server. You should explicitly choose this rather than using the system line separator, whether explicitly with System.getProperty("line.separator") or implicitly via a method such as println().

Finally, flush the connection and close it:

out.flush();

connection.close();

You won’t always have to close the connection after just one write. Many protocols, dict and HTTP 1.1 for instance, allow clients to send multiple requests over a single socket and expect the server to send multiple responses. Some protocols such as FTP can even hold a socket open indefinitely. However, the daytime protocol only allows a single response.

If the client closes the connection while the server is still operating, the input and/or output streams that connect the server to the client throw an InterruptedIOException on the next read or write. In either case, the server should then get ready to process the next incoming connection.

Of course, you’ll want to do all this repeatedly, so you’ll put this all inside a loop. Each pass through the loop invokes the accept() method once. 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. For example:

ServerSocket server = new ServerSocket(port);

while (true) {

try (Socket connection = server.accept()) {

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() +"\r\n");

out.flush();

} catch (IOException ex) {

// problem with one client; don't shut down the server

System.err.println(ex.getMessage());

}

}

This is called an iterative server. There’s one big loop, and in each pass through the loop a single connection is completely processed. This works well for a very simple protocol with very small requests and responses like daytime, though even with this simple a protocol it’s possible for one slow client to delay other faster clients. Upcoming examples will address this with multiple threads or asynchronous I/O.

When exception handling is added, the code becomes somewhat more convoluted. It’s important to distinguish between exceptions that should probably shut down the server and log an error message, and exceptions that should just close that active connection. Exceptions within the scope of a particular connection should close that connection, but not affect other connections or shut down the server. Exceptions outside the scope of an individual request probably should shut down the server. To organize this, nest the try blocks:

ServerSocket server = null;

try {

server = new ServerSocket(port);

while (true) {

Socket connection = null;

try {

connection = server.accept();

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() +"\r\n");

out.flush();

connection.close();

} catch (IOException ex) {

// this request only; ignore

} finally {

try {

if (connection != null) connection.close();

} catch (IOException ex) {}

}

}

} catch (IOException ex) {

ex.printStackTrace();

} finally {

try {

if (server != null) server.close();

} catch (IOException ex) {}

}

Always close a socket when you’re finished with it. In Chapter 8, I said that a client shouldn’t rely on the other side of a connection to close the socket; that goes triple for servers. Clients time out or crash; users cancel transactions; networks go down in high-traffic periods; hackers launch denial-of-service attacks. For any of these or a hundred more reasons, you cannot rely on clients to close sockets, even when the protocol requires them to, which this one doesn’t.

Example 9-1 puts this all together. It uses Java 7’s try-with-resources to autoclose the sockets.

Example 9-1. A daytime server

import java.net.*;

import java.io.*;

import java.util.Date;

public class DaytimeServer {

public final static int PORT = 13;

public static void main(String[] args) {

try (ServerSocket server = new ServerSocket(PORT)) {

while (true) {

try (Socket connection = server.accept()) {

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() +"\r\n");

out.flush();

connection.close();

} catch (IOException ex) {}

}

} catch (IOException ex) {

System.err.println(ex);

}

}

}

The class has a single method, main(), which does all the work. The outer try block traps any IOExceptions that may arise while the ServerSocket object server is constructed on the daytime port. 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.

TIP

The command for stopping a program manually depends on your system; under Unix, Windows, and many other systems, Ctrl-C will do the job. If you are running the server in the background on a Unix system, stop it by finding the server’s process ID and killing it with the kill command (kill pid ).

When a client connects, accept() returns a Socket, which is stored in the local variable connection, and the program continues. It calls getOutputStream() to get the output stream associated with that Socket and then chains that output stream to a new OutputStreamWriter,out. A new Date object provides the current time. The content is sent to the client by writing its string representation on out with write().

Connecting from Telnet, you should see something like this:

$ telnet localhost 13

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

Sat Mar 30 16:15:10 EDT 2013

Connection closed by foreign host

TIP

If you run this program on Unix (including Linux and Mac OS X), 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.

Serving Binary Data

Sending binary, nontext data is not significantly harder. You just use an OutputStream that writes a byte array rather than a Writer that writes a String. Example 9-2 demonstrates with an iterative time server that 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). Once again, the current time is found by creating a new Date object. However, because Java’s 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 9-2. A time server

import java.io.*;

import java.net.*;

import java.util.Date;

public class TimeServer {

public final static int PORT = 37;

public static void main(String[] args) {

// The time protocol sets the epoch at 1900,

// the Date class at 1970. This number

// converts between them.

long differenceBetweenEpochs = 2208988800L;

try (ServerSocket server = new ServerSocket(PORT)) {

while (true) {

try (Socket 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();

} catch (IOException ex) {

System.err.println(ex.getMessage());

}

}

} catch (IOException ex) {

System.err.println(ex);

}

}

}

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.

Multithreaded Servers

Daytime and time are both very quick protocols. The server sends a few dozen bytes at most and then closes the connection. It’s plausible here to process each connection fully before moving on to the next one. Even in that case, though, it is possible that a slow or crashed client might hang the server for a few seconds until it notices the socket is broken. If the sending of data can take a significant amount of time even when client and server are behaving, you really don’t want each connection to wait for the next.

Old-fashioned 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, 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. By default, Java sets the length of this queue to 50, although it can vary from operating system to operating system. Some operating systems (not Solaris) have a maximum queue length. For instance, on FreeBSD, the default maximum queue length is 128. On these systems, the queue length for a Java server socket will be the largest operating-system allowed 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. 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. Whatever the queue size, though, you want to be able to empty it faster than new connections are coming in, even if it takes a while to process each connection.

The solution here is to give each connection its own thread, separate from the thread that accepts incoming connections into the queue. For instance, Example 9-3 is a daytime server that spawns a new thread to handle each incoming connection. This prevents one slow client from blocking all the other clients. This is a thread per connection design.

Example 9-3. A multithreaded daytime server

import java.net.*;

import java.io.*;

import java.util.Date;

public class MultithreadedDaytimeServer {

public final static int PORT = 13;

public static void main(String[] args) {

try (ServerSocket server = new ServerSocket(PORT)) {

while (true) {

try {

Socket connection = server.accept();

Thread task = new DaytimeThread(connection);

task.start();

} catch (IOException ex) {}

}

} catch (IOException ex) {

System.err.println("Couldn't start server");

}

}

private static class DaytimeThread extends Thread {

private Socket connection;

DaytimeThread(Socket connection) {

this.connection = connection;

}

@Override

public void run() {

try {

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() +"\r\n");

out.flush();

} catch (IOException ex) {

System.err.println(ex);

} finally {

try {

connection.close();

} catch (IOException e) {

// ignore;

}

}

}

}

}

Example 9-3 uses try-with-resources to autoclose the server socket. However, it deliberately does not use try-with-resources for the client sockets accepted by the server socket. This is because the client socket escapes from the try block into a separate thread. If you used try-with-resources, the main thread would close the socket as soon as it got to the end of the while loop, likely before the spawned thread had finished using it.

There’s actually a denial-of-service attack on this server though. Because Example 9-3 spawns a new thread for each connection, numerous roughly simultaneous incoming connections can cause it to spawn an indefinite number of threads. Eventually, the Java virtual machine will run out of memory and crash. A better approach is to use a fixed thread pool as described in Chapter 3 to limit the potential resource usage. Fifty threads should be plenty. Example 9-4 shouldn’t crash no matter what load it’s under. It may start refusing connections, but it won’t crash.

Example 9-4. A daytime server using a thread pool

import java.io.*;

import java.net.*;

import java.util.*;

import java.util.concurrent.*;

public class PooledDaytimeServer {

public final static int PORT = 13;

public static void main(String[] args) {

ExecutorService pool = Executors.newFixedThreadPool(50);

try (ServerSocket server = new ServerSocket(PORT)) {

while (true) {

try {

Socket connection = server.accept();

Callable<Void> task = new DaytimeTask(connection);

pool.submit(task);

} catch (IOException ex) {}

}

} catch (IOException ex) {

System.err.println("Couldn't start server");

}

}

private static class DaytimeTask implements Callable<Void> {

private Socket connection;

DaytimeTask(Socket connection) {

this.connection = connection;

}

@Override

public Void call() {

try {

Writer out = new OutputStreamWriter(connection.getOutputStream());

Date now = new Date();

out.write(now.toString() +"\r\n");

out.flush();

} catch (IOException ex) {

System.err.println(ex);

} finally {

try {

connection.close();

} catch (IOException e) {

// ignore;

}

}

return null;

}

}

}

Example 9-4 is structured much like Example 9-3. The single difference is that it uses a Callable rather than a Thread subclass, and rather than starting threads it submits these callables to an executor service preconfigured with 50 threads.

Writing to Servers with Sockets

In the examples so far, the server has only written to client sockets. It hasn’t read from them. Most protocols, however, require the server to do both. This isn’t hard. You’ll accept a connection as before, but this time ask for both an InputStream and an OutputStream. Read from the client using the InputStream and write to it using the OutputStream. The main trick is understanding the protocol: when to write and when to read.

The echo protocol, defined in RFC 862, is one of the simplest interactive TCP services. The client opens a socket to port 7 on the echo server and sends data. The server sends the data back. This continues until the client closes the connection. The echo protocol is useful for testing the network to make sure that data is not mangled by a misbehaving router or firewall. You can test echo with Telnet like this:

$ telnet rama.poly.edu 7

Trying 128.238.10.212...

Connected to rama.poly.edu.

Escape character is '^]'.

This is a test

This is a test

This is another test

This is another test

9876543210

9876543210

^]

telnet> close

Connection closed.

This sample is line oriented because that’s how Telnet works. It reads a line of input from the console, sends it to the server, then waits to read a line of output it gets back. However, the echo protocol doesn’t require this. It echoes each byte as it receives it. It doesn’t really care whether those bytes represent characters in some encoding or are divided into lines. Unlike many protocols, echo does not specify lockstep behavior where the client sends a request but then waits for the full server response before sending any more data.

Unlike daytime and time, in the echo protocol the client is responsible for closing the connection. This makes it even more important to support asynchronous operation with many threads because a single client can remain connected indefinitely. In Example 9-5, the server spawns up to 500 threads.

Example 9-5. An echo server

import java.nio.*;

import java.nio.channels.*;

import java.net.*;

import java.util.*;

import java.io.IOException;

public class EchoServer {

public static int DEFAULT_PORT = 7;

public static void main(String[] args) {

int port;

try {

port = Integer.parseInt(args[0]);

} catch (RuntimeException ex) {

port = DEFAULT_PORT;

}

System.out.println("Listening for connections on port " + port);

ServerSocketChannel serverChannel;

Selector selector;

try {

serverChannel = ServerSocketChannel.open();

ServerSocket ss = serverChannel.socket();

InetSocketAddress address = new InetSocketAddress(port);

ss.bind(address);

serverChannel.configureBlocking(false);

selector = Selector.open();

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

} catch (IOException ex) {

ex.printStackTrace();

return;

}

while (true) {

try {

selector.select();

} catch (IOException ex) {

ex.printStackTrace();

break;

}

Set<SelectionKey> readyKeys = selector.selectedKeys();

Iterator<SelectionKey> iterator = readyKeys.iterator();

while (iterator.hasNext()) {

SelectionKey key = iterator.next();

iterator.remove();

try {

if (key.isAcceptable()) {

ServerSocketChannel server = (ServerSocketChannel) key.channel();

SocketChannel client = server.accept();

System.out.println("Accepted connection from " + client);

client.configureBlocking(false);

SelectionKey clientKey = client.register(

selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ);

ByteBuffer buffer = ByteBuffer.allocate(100);

clientKey.attach(buffer);

}

if (key.isReadable()) {

SocketChannel client = (SocketChannel) key.channel();

ByteBuffer output = (ByteBuffer) key.attachment();

client.read(output);

}

if (key.isWritable()) {

SocketChannel client = (SocketChannel) key.channel();

ByteBuffer output = (ByteBuffer) key.attachment();

output.flip();

client.write(output);

output.compact();

}

} catch (IOException ex) {

key.cancel();

try {

key.channel().close();

} catch (IOException cex) {}

}

}

}

}

}

Closing Server Sockets

If you’re finished with a server socket, you should close it, especially if the 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 aServerSocket frees a port on the local host, allowing another server to bind to the port; it also breaks all currently open sockets that the ServerSocket has accepted.

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. Programmers often follow the same close-if-not-null pattern in a try-finally block that you’re already familiar with from streams and client-side sockets:

ServerSocket server = null;

try {

server = new ServerSocket(port);

// ... work with the server socket

} finally {

if (server != null) {

try {

server.close();

} catch (IOException ex) {

// ignore

}

}

}

You can improve this slightly by using the noargs ServerSocket() constructor, which does not throw any exceptions and does not bind to a port. Instead, you call the bind() method to bind to a socket address after the ServerSocket() object has been constructed:

ServerSocket server = new ServerSocket();

try {

SocketAddress address = new InetSocketAddress(port);

server.bind(address);

// ... work with the server socket

} finally {

try {

server.close();

} catch (IOException ex) {

// ignore

}

}

In Java 7, ServerSocket implements AutoCloseable so you can take advantage of try-with-resources instead:

try (ServerSocket server = new ServerSocket(port)) {

// ... work with the server socket

}

After a server socket has been closed, it cannot be reconnected, even to the same port.

The isClosed() method returns true if the ServerSocket has been closed, false if it hasn’t:

public boolean isClosed()

ServerSocket objects that were created with the noargs ServerSocket() constructor and not yet bound to a port are not considered to be closed. Invoking isClosed() on these objects returns false. The isBound() method tells you whether the ServerSocket has been bound to a port:

public boolean isBound()

As with the isBound() method of the Socket class discussed in the Chapter 8, the name is a little misleading. isBound() returns true if the ServerSocket has ever been bound to a port, even if it’s currently closed. If you need to test whether a ServerSocket is open, you must check both that isBound() returns true and that isClosed() returns false. For example:

public static boolean isOpen(ServerSocket ss) {

return ss.isBound() && !ss.isClosed();

}

Logging

Servers run unattended for long periods of time. It’s often important to debug what happened when in a server long after the fact. For this reason, it’s advisable to store server logs for at least some period of time.

What to Log

There are two primary things you want to store in your logs:

§ Requests

§ Server errors

Indeed, servers often keep two different logfiles for these two different items. The audit log usually contains one entry for each connection made to the server. Servers that perform multiple operations per connection may have one entry per operation instead. For instance, a dict server might log one entry for each word a client looks up.

The error log contains mostly unexpected exceptions that occurred while the server was running. For instance, any NullPointerException that happens should be logged here because it indicates a bug in the server you’ll need to fix. The error log does not contain client errors, such as a client that unexpectedly disconnects or sends a malformed request. These go into the request log. The error log is exclusively for unexpected exceptions.

The general rule of thumb for error logs is that every line in the error log should be looked at and resolved. The ideal number of entries in an error log is zero. Every entry in this log represents a bug to be investigated and resolved. If investigation of an error log entry ends with the decision that that exception is not really a problem, and the code is working as intended, remove the log statement. Error logs that fill up with too many false alarms rapidly become ignored and useless.

For the same reason, do not keep debug logs in production. Do not log every time you enter a method, every time a condition is met, and so on. No one ever looks at these logs. They just waste space and hide real problems. If you need method-level logging for debugging, put it in a separate file, and turn it off in the global properties file when running in production.

More advanced logging systems provide log analysis tools that enable you to do things like show only messages with priority INFO or higher, or only show messages that originated from a certain part of the code. These tools make it more feasible to keep a single logfile or database, perhaps even share one log among many different binaries or programs. Nonetheless, the principle still applies that a log record no one will ever look at is worthless at best and more often than not distracting or confusing.

Do not follow the common antipattern of logging everything you can think of just in case someone might need it someday. In practice, programmers are terrible at guessing in advance which log messages they might need for debugging production problems. Once a problem occurs, it is sometimes obvious what messages you need; but it is rare to be able to anticipate this in advance. Adding “just in case” messages to logfiles usually means that when a problem does occur, you’re frantically hunting for the relevant messages in an even bigger sea of irrelevant data.

How to Log

Many legacy programs dating back to Java 1.3 and earlier still use third-party logging libraries such as log4j or Apache Commons Logging, but the java.util.logging package available since Java 1.4 suffices for most needs. Choosing it avoids a lot of complex third-party dependencies.

Although you can load a logger on demand, it’s usually easiest to just create one per class like so:

private final static Logger auditLogger = Logger.getLogger("requests");

Loggers are thread safe, so there’s no problem storing them in a shared static field. Indeed, they almost have to be because even if the Logger object were not shared between threads, the logfile or database would be. This is important in highly multithreaded servers.

This example outputs to a log named “requests.” Multiple Logger objects can output to the same log, but each logger always logs to exactly one log. What and where the log is depends on external configuration. Most commonly it’s a file, which may or may not be named “requests”; but it can be a database, a SOAP service running on a different server, another Java program on the same host, or something else.

Once you have a logger, you can write to it using any of several methods. The most basic is log(). For example, this catch block logs an unexpected runtime exception at the highest level:

catch (RuntimeException ex) {

logger.log(Level.SEVERE, "unexpected error " + ex.getMessage(), ex);

}

Including the exception instead of just a message is optional but customary when logging from a catch block.

There are seven levels defined as named constants in java.util.logging.Level in descending order of seriousness:

§ Level.SEVERE (highest value)

§ Level.WARNING

§ Level.INFO

§ Level.CONFIG

§ Level.FINE

§ Level.FINER

§ Level.FINEST (lowest value)

I use info for audit logs and warning or severe for error logs. Lower levels are for debugging only and should not be used in production systems. Info, severe, and warning all have convenience helper methods that log at that level. For example, this statement logs a hit including the date and the remote address:

logger.info(new Date() + " " + connection.getRemoteSocketAddress());

You can use any format that’s convenient for the individual log records. Generally, each record should contain a timestamp, the client address, and any information specific to the request that was being processed. If the log message represents an error, include the specific exception that was thrown. Java fills in the location in the code where the message was logged automatically, so you don’t need to worry about that.

Example 9-6 demonstrates by adding logging to the daytime server.

Example 9-6. A daytime server that logs requests and errors

import java.io.*;

import java.net.*;

import java.util.Date;

import java.util.concurrent.*;

import java.util.logging.*;

public class LoggingDaytimeServer {

public final static int PORT = 13;

private final static Logger auditLogger = Logger.getLogger("requests");

private final static Logger errorLogger = Logger.getLogger("errors");

public static void main(String[] args) {

ExecutorService pool = Executors.newFixedThreadPool(50);

try (ServerSocket server = new ServerSocket(PORT)) {

while (true) {

try {

Socket connection = server.accept();

Callable<Void> task = new DaytimeTask(connection);

pool.submit(task);

} catch (IOException ex) {

errorLogger.log(Level.SEVERE, "accept error", ex);

} catch (RuntimeException ex) {

errorLogger.log(Level.SEVERE, "unexpected error " + ex.getMessage(), ex);

}

}

} catch (IOException ex) {

errorLogger.log(Level.SEVERE, "Couldn't start server", ex);

} catch (RuntimeException ex) {

errorLogger.log(Level.SEVERE, "Couldn't start server: " + ex.getMessage(), ex);

}

}

private static class DaytimeTask implements Callable<Void> {

private Socket connection;

DaytimeTask(Socket connection) {

this.connection = connection;

}

@Override

public Void call() {

try {

Date now = new Date();

// write the log entry first in case the client disconnects

auditLogger.info(now + " " + connection.getRemoteSocketAddress());

Writer out = new OutputStreamWriter(connection.getOutputStream());

out.write(now.toString() +"\r\n");

out.flush();

} catch (IOException ex) {

// client disconnected; ignore;

} finally {

try {

connection.close();

} catch (IOException ex) {

// ignore;

}

}

return null;

}

}

}

As well as logging, Example 9-6 has also added catch blocks for RuntimeException that cover most of the code and all of the network connections. This is strongly advisable in network servers. The last thing you want is for your entire server to fall down just because one request went down an unplanned code path and threw an IllegalArgumentException. Usually when this happens that request is going to fail, but you can continue processing other requests. If you’re even more careful, you can send the client the appropriate error response. In HTTP, this would be a 500 internal server error.

Not every exception automatically turns into an error log entry. For example, if a client disconnects while you’re writing the time, that’s an IOException. However, it’s not a bug or a server error, so it isn’t written to the error log. In some situations, you might want to log it in the audit log, or a third location. However, remember the golden rule of logging: if no one’s going to look at it, don’t log it. Unless you really plan to investigate and do something about client disconnects, don’t bother to record them.

By default, the logs are just output to the console. For example, here’s the output from the preceding server when I connected to it a few times in quick succession:

Apr 13, 2013 8:54:50 AM LoggingDaytimeServer$DaytimeTask call

INFO: Sat Apr 13 08:54:50 EDT 2013 /0:0:0:0:0:0:0:1:56665

Apr 13, 2013 8:55:08 AM LoggingDaytimeServer$DaytimeTask call

INFO: Sat Apr 13 08:55:08 EDT 2013 /0:0:0:0:0:0:0:1:56666

Apr 13, 2013 8:55:16 AM LoggingDaytimeServer$DaytimeTask call

INFO: Sat Apr 13 08:55:16 EDT 2013 /0:0:0:0:0:0:0:1:56667

You’ll want to configure the runtime environment such that logs go to a more permanent destination. Although you can specify this in code, it’s usually advisable to set this up in a configuration file so log locations can be changed without recompiling.

The java.util.logging.config.file system property points to a file in the normal properties format that controls the logging. You set this property by passing the -Djava.util.logging.config.file=_filename_ argument when launching the virtual machine. For instance, in Mac OS X, it might be set in the VMOptions in the Info.plist file:

<key>Java</key>

<dict>

<key>VMOptions</key>

<array>

<string>-Djava.util.logging.config.file=/opt/daytime/logging.properties

</string>

</array>

</dict>

Example 9-7 is a sample logging properties file that specifies:

§ Logs should be written to a file.

§ The requests log should be in /var/logs/daytime/requests.log at level Info.

§ The errors log should be in /var/logs/daytime/requests.log at level Severe.

§ Limit the log size to about 10 megabytes, then rotate.

§ Keep two logs: the current one and the previous one.

§ Use the basic text formatter (not XML).

§ Each line of the logfile should be in the form level message timestamp.

Example 9-7. A logging properties file

handlers=java.util.logging.FileHandler

java.util.logging.FileHandler.pattern = /var/logs/daytime/requests.log

java.util.logging.FileHandler.limit = 10000000

java.util.logging.FileHandler.count = 2

java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

java.util.logging.FileHandler.append = true

java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

requests.level = INFO

audit.level = SEVERE

Here’s some typical log output (note that it looks like the timestamp is doubled in request messages because the log message also includes the current time; this would not typically be the case for a server whose purpose was anything other than serving the current time):

SEVERE: Couldn't start server [Sat Apr 13 10:07:01 EDT 2013]

INFO: Sat Apr 13 10:08:05 EDT 2013 /0:0:0:0:0:0:0:1:57275

[Sat Apr 13 10:08:05 EDT 2013]

INFO: Sat Apr 13 10:08:06 EDT 2013 /0:0:0:0:0:0:0:1:57276

[Sat Apr 13 10:08:06 EDT 2013]

TIP

The one thing I don’t like about the Java Logging API is that it doesn’t give you an easy way to specify by configuration alone that different messages belong in different logs. For instance, you can’t easily separate your error and your audit log. It can be done, but it requires you to define a new subclass of FileHandler for each separate log so you can assign it a new file.

Finally, once you’ve configured your servers with logging, don’t forget to look in them, especially the error logs. There’s no point to a logfile no one ever looks at. You’ll also want to plan for and implement log rotation and retention policies. Hard drives get bigger every year, but it’s still possible for a high-volume server to fill up a filesystem with log data if you aren’t paying attention. Murphy’s law says this is most likely to happen at 4:00 A.M. on New Year’s Day when you’re on vacation halfway around the world.

Constructing Server Sockets

There are four public ServerSocket constructors:

public ServerSocket(int port) throws BindException, IOException

public ServerSocket(int port, int queueLength)

throws BindException, IOException

public ServerSocket(int port, int queueLength, InetAddress bindAddress)

throws IOException

public ServerSocket() throws IOException

These constructors 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.

For example, to create a server socket that would be used by an HTTP server on port 80, you would write:

ServerSocket httpd = new ServerSocket(80);

To create a server socket that would be used by an HTTP server on port 80 and queues up to 50 unaccepted connections at a time:

ServerSocket httpd = new ServerSocket(80, 50);

If you try to expand the queue past the operating system’s maximum queue length, the maximum queue length is used instead.

By default, if a host has multiple network interfaces or IP addresses, the server socket listens on the specified port on all the interfaces and IP addresses. However, you can add a third argument to bind only to one particular local IP address. That is, the server socket only listens for incoming connections on the specified address; it won’t listen for connections that come in through the host’s other addresses.

For example, login.ibiblio.org is a particular Linux box in North Carolina. It’s connected to the Internet with the IP address 152.2.210.122. The same box has a second Ethernet card with the local IP address 192.168.210.122 that is not visible from the public Internet, only from the local network. If, for some reason, you wanted to run a server on this host that only responded to local connections from within the same network, you could create a server socket that listens on port 5776 of 192.168.210.122 but not on port 5776 of 152.2.210.122, like so:

InetAddress local = InetAddress.getByName("192.168.210.122");

ServerSocket httpd = new ServerSocket(5776, 10, local);

In all three constructors, you can pass 0 for the port number so the system will select an available port for you. A port chosen by the system like this is sometimes called an anonymous port because you don’t know its number in advance (though you can find out after the port has been chosen). This is often useful in multisocket protocols such as FTP. In passive FTP the client first connects to a server on the well-known port 21, so the server has to specify that port. However, when a file needs to be transferred, the server starts listening on any available port. The server then tells the client what other port it should connect to for data using the command connection already open on port 21. Thus, the data port can change from one session to the next and does not need to be known in advance. (Active FTP is similar except the client listens on an ephemeral port for the server to connect to it, rather than the other way around.)

All these constructors throw 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 (including Linux and Mac OS X) without root (superuser) privileges.

You can take advantage of this to write a variation on the LowPortScanner program of the previous chapter. Rather than attempting to connect to a server running on a given port, you instead attempt to open a server on that port. If it’s occupied, the attempt will fail. Example 9-8 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 1024 and above.

Example 9-8. Look for local ports

import java.io.*;

import java.net.*;

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 ex) {

System.out.println("There is a server on port " + port + ".");

}

}

}

}

Here’s the output I got when running LocalPortScanner on my Windows workstation:

D:\JAVA\JNP4\examples\9>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.

Constructing Without Binding

The noargs constructor creates a ServerSocket object but does not actually bind it to a port, so it cannot initially accept any connections. It can be bound later using the bind() methods:

public void bind(SocketAddress endpoint) throws IOException

public void bind(SocketAddress endpoint, int queueLength) throws IOException

The primary use for this feature is to allow programs to set server socket options before binding to a port. Some options are fixed after the server socket has been bound. The general pattern looks like this:

ServerSocket ss = new ServerSocket();

// set socket options...

SocketAddress http = new InetSocketAddress(80);

ss.bind(http);

You can also pass null for the SocketAddress to select an arbitrary port. This is like passing 0 for the port number in the other constructors.

Getting Information About a Server Socket

The ServerSocket class provides two getter methods that 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), this is the address returned by InetAddress.getLocalHost(). If the local host has more than one IP address, the specific address returned is one of the host’s IP addresses. You can’t predict which address you will get. For example:

ServerSocket httpd = new ServerSocket(80);

InetAddress ia = httpd.getInetAddress();

If the ServerSocket has not yet bound to a network interface, this method returns null:

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 on what ports they can find the smaller servers. Of course, you can also use getLocalPort() to find a nonanonymous port, but why would you need to?Example 9-9 demonstrates.

Example 9-9. A random port

import java.io.*;

import java.net.*;

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 ex) {

System.err.println(ex);

}

}

}

Here’s the output of several runs:

$ java RandomPort

This server runs on port 1154

D:\JAVA\JNP4\examples\9>java RandomPort

This server runs on port 1155

D:\JAVA\JNP4\examples\9>java RandomPort

This server runs on port 1156

At least on this system, the ports aren’t truly random, but they are indeterminate until runtime.

If the ServerSocket has not yet bound to a port, getLocalPort() returns –1.

As with most Java objects, you can also just print out a ServerSocket using its toString() method. A String returned by a ServerSocket’s toString() method looks like this:

ServerSocket[addr=0.0.0.0,port=0,localport=5776]

addr is the address of the local network interface to which the server socket is bound. This will be 0.0.0.0 if it’s bound to all interfaces, as is commonly the case. port is always 0. The localport is the local port on which the server is listening for connections. This method is sometimes useful for debugging, but not much more. Don’t rely on it.

Socket Options

Socket options specify how the native sockets on which the ServerSocket class relies send and receive data. For server sockets, Java supports three options:

§ SO_TIMEOUT

§ SO_REUSEADDR

§ SO_RCVBUF

It also allows you to set performance preferences for the socket’s packets.

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, accept() will never time out. The default is to never time out.

Setting SO_TIMEOUT is uncommon. You might need it if you were implementing a complicated and secure protocol that required multiple connections between the client and the server where responses needed to occur within a fixed amount of time. However, most servers are designed to run for indefinite periods of time and therefore just use the default timeout value, 0 (never time out). If you want to change this, the setSoTimeout() method sets the SO_TIMEOUT field for this server socket object:

public void setSoTimeout(int timeout) throws SocketException

public int getSoTimeout() throws IOException

The countdown starts when accept() is invoked. When the timeout expires, accept() throws a SocketTimeoutException, a subclass of IOException. You need to 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(port)) {

server.setSoTimeout(30000); // block for no more than 30 seconds

try {

Socket s = server.accept();

// handle the connection

// ...

} catch (SocketTimeoutException ex) {

System.err.println("No connection within 30 seconds");

}

} catch (IOException ex) {

System.err.println("Unexpected IOException: " + e);

}

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." );

}

}

SO_REUSEADDR

The SO_REUSEADDR option for server sockets is very similar to the same option for client sockets, discussed in the previous chapter. It determines whether a new socket will be allowed to bind to a previously used port while there might still be data traversing the network addressed to the old socket. As you probably expect, there are two methods to get and set this option:

public boolean getReuseAddress() throws SocketException

public void setReuseAddress(boolean on) throws SocketException

The default value is platform dependent. This code fragment determines the default value by creating a new ServerSocket and then calling getReuseAddress():

ServerSocket ss = new ServerSocket(10240);

System.out.println("Reusable: " + ss.getReuseAddress());

On the Linux and Mac OS X boxes where I tested this code, server sockets were reusable by default.

SO_RCVBUF

The SO_RCVBUF option sets the default receive buffer size for client sockets accepted by the server socket. It’s read and written by these two methods:

public int getReceiveBufferSize() throws SocketException

public void setReceiveBufferSize(int size) throws SocketException

Setting SO_RCVBUF on a server socket is like calling setReceiveBufferSize() on each individual socket returned by accept() (except that you can’t change the receive buffer size after the socket has been accepted). Recall from the previous chapter that this option suggests a value for the size of the individual IP packets in the stream. Faster connections will want to use larger buffers, although most of the time the default value is fine.

You can set this option before or after the server socket is bound, unless you want to set a receive buffer size larger than 64K. In that case, you must set the option on an unbound ServerSocket before binding it. For example:

ServerSocket ss = new ServerSocket();

int receiveBufferSize = ss.getReceiveBufferSize();

if (receiveBufferSize < 131072) {

ss.setReceiveBufferSize(131072);

}

ss.bind(new InetSocketAddress(8000));

//...

Class of Service

As you learned in the previous chapter, different types of Internet services have different performance needs. For instance, live streaming video of sports needs relatively high bandwidth. On the other hand, a movie might still need high bandwidth but be able to tolerate more delay and latency. Email can be passed over low-bandwidth connections and even held up for several hours without major harm.

Four general traffic classes are defined for TCP:

§ Low cost

§ High reliability

§ Maximum throughput

§ Minimum delay

These traffic classes can be requested for a given Socket. For instance, you can request the minimum delay available at low cost. These measures are all fuzzy and relative, not guarantees of service. Not all routers and native TCP stacks support these classes.

The setPerformancePreferences() method expresses the relative preferences given to connection time, latency, and bandwidth for sockets accepted on this server:

public void setPerformancePreferences(int connectionTime, int latency,

int bandwidth)

For instance, by setting connectionTime to 2, latency to 1, and bandwidth to 3, you indicate that maximum bandwidth is the most important characteristic, minimum latency is the least important, and connection time is in the middle:

ss.setPerformancePreferences(2, 1, 3);

Exactly how any given VM implements this is implementation dependent. The underlying socket implementation is not required to respect any of these requests. They only provide a hint to the TCP stack about the desired policy. Many implementations including Android ignore these values completely.

HTTP Servers

This section shows several different HTTP servers you can build with server sockets, each with a different special purpose and each slightly more complex than the previous one.

HTTP is a large protocol. As you saw in Chapter 5, a full-featured HTTP server must respond to requests for files, convert URLs into filenames on the local system, respond to POST and GET requests, handle requests for files that don’t exist, interpret MIME types, and much, much more. However, many HTTP servers don’t need all of these features. For example, many sites simply display an “under construction” message. Clearly, Apache is overkill for a site like this. Such a site is a candidate for a custom server that does only one thing. Java’s network class library makes writing simple servers like this almost trivial.

Custom servers aren’t useful only for small sites. High-traffic sites like Yahoo! are also candidates for custom servers because a server that does only one thing can often be much faster than a general-purpose server such as Apache or Microsoft IIS. It is easy to optimize a special-purpose server for a particular task; the result is often much more efficient than a general-purpose server that needs to respond to many different kinds of requests. For instance, icons and images that are used repeatedly across many pages or on high-traffic pages might be better handled by a server that read all the image files into memory on startup and then served them straight out of RAM, rather than having to read them off disk for each request. Furthermore, this server could avoid wasting time on logging if you didn’t want to track the image requests separately from the requests for the pages in which they were included.

Finally, Java isn’t a bad language for full-featured web servers meant to compete with the likes of Apache or IIS. Even if you believe CPU-intensive Java programs are slower than CPU-intensive C and C++ programs (something I very much doubt is true in modern VMs), most HTTP servers are limited by network bandwidth and latency, not by CPU speed. Consequently, Java’s other advantages, such as its half-compiled/half-interpreted nature, dynamic class loading, garbage collection, and memory protection really get a chance to shine. In particular, sites that make heavy use of dynamic content through servlets, PHP pages, or other mechanisms can often run much faster when reimplemented on top of a pure or mostly pure Java web server. Indeed, there are several production web servers written in Java, such as the Eclipse Foundation’s Jetty. Many other web servers written in C now include substantial Java components to support the Java Servlet API and Java Server Pages. These largely replaced traditional CGIs, ASPs, and server-side includes, mostly because the Java equivalents are faster and less resource intensive. I’m not going to explore these technologies here because they easily deserve a book of their own. I refer interested readers to Jason Hunter’s Java Servlet Programming (O’Reilly). However, it is important to note that servers in general and web servers in particular are one area where Java really is competitive with C for real-world performance.

A Single-File Server

Our investigation of HTTP servers begins with a server that always sends out the same file, no matter what the request. It’s called SingleFileHTTPServer and is shown in Example 9-10. The filename, local port, and content encoding are read from the command line. If the port is omitted, port 80 is assumed. If the encoding is omitted, ASCII is assumed.

Example 9-10. An HTTP server that serves a single file

import java.io.*;

import java.net.*;

import java.nio.charset.Charset;

import java.nio.file.*;

import java.util.concurrent.*;

import java.util.logging.*;

public class SingleFileHTTPServer {

private static final Logger logger = Logger.getLogger("SingleFileHTTPServer");

private final byte[] content;

private final byte[] header;

private final int port;

private final String encoding;

public SingleFileHTTPServer(String data, String encoding,

String mimeType, int port) throws UnsupportedEncodingException {

this(data.getBytes(encoding), encoding, mimeType, port);

}

public SingleFileHTTPServer(

byte[] data, String encoding, String mimeType, int port) {

this.content = data;

this.port = port;

this.encoding = encoding;

String header = "HTTP/1.0 200 OK\r\n"

+ "Server: OneFile 2.0\r\n"

+ "Content-length: " + this.content.length + "\r\n"

+ "Content-type: " + mimeType + "; charset=" + encoding + "\r\n\r\n";

this.header = header.getBytes(Charset.forName("US-ASCII"));

}

public void start() {

ExecutorService pool = Executors.newFixedThreadPool(100);

try (ServerSocket server = new ServerSocket(this.port)) {

logger.info("Accepting connections on port " + server.getLocalPort());

logger.info("Data to be sent:");

logger.info(new String(this.content, encoding));

while (true) {

try {

Socket connection = server.accept();

pool.submit(new HTTPHandler(connection));

} catch (IOException ex) {

logger.log(Level.WARNING, "Exception accepting connection", ex);

} catch (RuntimeException ex) {

logger.log(Level.SEVERE, "Unexpected error", ex);

}

}

} catch (IOException ex) {

logger.log(Level.SEVERE, "Could not start server", ex);

}

}

private class HTTPHandler implements Callable<Void> {

private final Socket connection;

HTTPHandler(Socket connection) {

this.connection = connection;

}

@Override

public Void call() throws IOException {

try {

OutputStream out = new BufferedOutputStream(

connection.getOutputStream()

);

InputStream in = new BufferedInputStream(

connection.getInputStream()

);

// read the first line only; that's all we need

StringBuilder request = new StringBuilder(80);

while (true) {

int c = in.read();

if (c == '\r' || c == '\n' || c == -1) break;

request.append((char) c);

}

// If this is HTTP/1.0 or later send a MIME header

if (request.toString().indexOf("HTTP/") != -1) {

out.write(header);

}

out.write(content);

out.flush();

} catch (IOException ex) {

logger.log(Level.WARNING, "Error writing to client", ex);

} finally {

connection.close();

}

return null;

}

}

public static void main(String[] args) {

// set the port to listen on

int port;

try {

port = Integer.parseInt(args[1]);

if (port < 1 || port > 65535) port = 80;

} catch (RuntimeException ex) {

port = 80;

}

String encoding = "UTF-8";

if (args.length > 2) encoding = args[2];

try {

Path path = Paths.get(args[0]);;

byte[] data = Files.readAllBytes(path);

String contentType = URLConnection.getFileNameMap().getContentTypeFor(args[0]);

SingleFileHTTPServer server = new SingleFileHTTPServer(data, encoding,

contentType, port);

server.start();

} catch (ArrayIndexOutOfBoundsException ex) {

System.out.println(

"Usage: java SingleFileHTTPServer filename port encoding");

} catch (IOException ex) {

logger.severe(ex.getMessage());

}

}

}

The constructors set up the data to be sent along with an HTTP header that includes information about content length and content encoding. The header and the body of the response are stored in byte arrays in the desired encoding so that they can be blasted to clients very quickly.

The SingleFileHTTPServer class holds the content to send, the header to send, and the port to bind to. The start() method creates a ServerSocket on the specified port, then enters an infinite loop that continually accepts connections and processes them.

Each incoming socket is processed by a runnable Handler object that is submitted to a thread pool. Thus, one slow client can’t starve other clients. Each Handler gets an InputStream from it which it reads the client request. It looks at the first line to see whether it contains the stringHTTP. If it sees this string, the server assumes that the client understands HTTP/1.0 or later and therefore sends a MIME header for the file; then it sends the data. If the client request doesn’t contain the string HTTP, the server omits the header, sending the data by itself. Finally, the handler closes the connection.

The main() method just reads parameters from the command line. The name of the file to be served is read from the first command-line argument. If no file is specified or the file cannot be opened, an error message is printed and the program exits. Assuming the file can be read, its contents are read into the byte array data using the Path and Files classes introduced in Java 7. The URLConnection class makes a reasonable guess about the content type of the file, and that guess is stored in the contentType variable. Next, the port number is read from the second command-line argument. If no port is specified or if the second argument is not an integer from 1 to 65,535, port 80 is used. The encoding is read from the third command-line argument, if present. Otherwise, UTF-8 is assumed. Then these values are used to construct a SingleFileHTTPServerobject and start it.

The main() method is only one possible interface. You could easily use this class as part of some other program. If you added a setter method to change the content, you could easily use it to provide simple status information about a running server or system. However, that would raise some additional issues of thread safety that Example 9-10 doesn’t have to address because the data is immutable.

Here’s what you see when you connect to this server via Telnet (the specifics depend on the exact server and file):

% telnet macfaq.dialup.cloud9.net 80

Trying 168.100.203.234...

Connected to macfaq.dialup.cloud9.net.

Escape character is '^]'.

GET / HTTP/1.0

HTTP/1.0 200 OK

Server: OneFile 2.0

Content-length: 959

Content-type: text/html; charset=UTF-8

&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"&gt;

&lt;HTML&gt;

&lt;HEAD&gt;

&lt;TITLE&gt;Under Construction&lt;/TITLE&gt;

&lt;/HEAD&gt;

&lt;BODY&gt;

...

A Redirector

Redirection is another simple but useful application for a special-purpose HTTP server. In this section, you develop a server that redirects users from one website to another—for example, from cnet.com to www.cnet.com. Example 9-11 reads a URL and a port number from the command line, opens a server socket on the port, and redirects all requests that it receives to the site indicated by the new URL using a 302 FOUND code. In this example, I chose to use a new thread rather than a thread pool for each connection. This is perhaps a little simpler to code and understand but somewhat less efficient.

Example 9-11. An HTTP redirector

import java.io.*;

import java.net.*;

import java.util.*;

import java.util.logging.*;

public class Redirector {

private static final Logger logger = Logger.getLogger("Redirector");

private final int port;

private final String newSite;

public Redirector(String newSite, int port) {

this.port = port;

this.newSite = newSite;

}

public void start() {

try (ServerSocket server = new ServerSocket(port)) {

logger.info("Redirecting connections on port "

+ server.getLocalPort() + " to " + newSite);

while (true) {

try {

Socket s = server.accept();

Thread t = new RedirectThread(s);

t.start();

} catch (IOException ex) {

logger.warning("Exception accepting connection");

} catch (RuntimeException ex) {

logger.log(Level.SEVERE, "Unexpected error", ex);

}

}

} catch (BindException ex) {

logger.log(Level.SEVERE, "Could not start server.", ex);

} catch (IOException ex) {

logger.log(Level.SEVERE, "Error opening server socket", ex);

}

}

private class RedirectThread extends Thread {

private final Socket connection;

RedirectThread(Socket s) {

this.connection = s;

}

public void run() {

try {

Writer out = new BufferedWriter(

new OutputStreamWriter(

connection.getOutputStream(), "US-ASCII"

)

);

Reader in = new InputStreamReader(

new BufferedInputStream(

connection.getInputStream()

)

);

// read the first line only; that's all we need

StringBuilder request = new StringBuilder(80);

while (true) {

int c = in.read();

if (c == '\r' || c == '\n' || c == -1) break;

request.append((char) c);

}

String get = request.toString();

String[] pieces = get.split("\\w*");

String theFile = pieces[1];

// If this is HTTP/1.0 or later send a MIME header

if (get.indexOf("HTTP") != -1) {

out.write("HTTP/1.0 302 FOUND\r\n");

Date now = new Date();

out.write("Date: " + now + "\r\n");

out.write("Server: Redirector 1.1\r\n");

out.write("Location: " + newSite + theFile + "\r\n");

out.write("Content-type: text/html\r\n\r\n");

out.flush();

}

// Not all browsers support redirection so we need to

// produce HTML that says where the document has moved to.

out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");

out.write("<BODY><H1>Document moved</H1>\r\n");

out.write("The document " + theFile

+ " has moved to\r\n<A HREF=\"" + newSite + theFile + "\">"

+ newSite + theFile

+ "</A>.\r\n Please update your bookmarks<P>");

out.write("</BODY></HTML>\r\n");

out.flush();

logger.log(Level.INFO,

"Redirected " + connection.getRemoteSocketAddress());

} catch(IOException ex) {

logger.log(Level.WARNING,

"Error talking to " + connection.getRemoteSocketAddress(), ex);

} finally {

try {

connection.close();

} catch (IOException ex) {}

}

}

}

public static void main(String[] args) {

int thePort;

String theSite;

try {

theSite = args[0];

// trim trailing slash

if (theSite.endsWith("/")) {

theSite = theSite.substring(0, theSite.length() - 1);

}

} catch (RuntimeException ex) {

System.out.println(

"Usage: java Redirector http://www.newsite.com/ port");

return;

}

try {

thePort = Integer.parseInt(args[1]);

} catch (RuntimeException ex) {

thePort = 80;

}

Redirector redirector = new Redirector(theSite, thePort);

redirector.start();

}

}

In order to start the redirector on port 80 and redirect incoming requests to http://www.cafeconleche.org/, type:

D:\JAVA\JNP4\examples\09&gt; java Redirector http://www.cafeconleche.org/

Redirecting connections on port 80 to http://www.cafeconleche.org/

If you connect to this server via Telnet, this is what you’ll see:

% <userinput moreinfo="none">telnet macfaq.dialup.cloud9.net 80

Trying 168.100.203.234...

Connected to macfaq.dialup.cloud9.net.

Escape character is '^]'.

GET / HTTP/1.0

HTTP/1.0 302 FOUND

Date: Sun Mar 31 12:38:42 EDT 2013

Server: Redirector 1.1

Location: http://www.cafeconleche.org/

Content-type: text/html

&lt;HTML&gt;&lt;HEAD&gt;&lt;TITLE&gt;Document moved&lt;/TITLE&gt;&lt;/HEAD&gt;

&lt;BODY&gt;&lt;H1&gt;Document moved&lt;/H1&gt;

The document / has moved to

&lt;A HREF="http://www.cafeconleche.org/"&gt;http://www.cafeconleche.

org/&lt;/A&gt;.

Please update your bookmarks&lt;P&gt;&lt;/BODY&gt;&lt;/HTML&gt;

Connection closed by foreign host.

If, however, you connect with a web browser, you should be sent to http://www.cafeconleche.org/ with only a slight delay. You should never see the HTML added after the response code; this is only provided to support very old browsers that don’t do redirection automatically, as well as a few security paranoids who have configured their browsers not to redirect automatically.

The main() method provides a very simple interface that reads the URL of the new site to redirect connections to and the local port to listen on. It uses this information to construct a Redirector object. Then it invokes start(). If the port is not specified, Redirector listens on port 80. If the site is omitted, Redirector prints an error message and exits.

The start() method of Redirector binds the server socket to the port, prints a brief status message, and then enters an infinite loop in which it listens for connections. Every time a connection is accepted, the resulting Socket object is used to construct a RedirectThread. ThisRedirectThread is then started. All further interaction with the client takes place in this new thread. The start() method then simply waits for the next incoming connection.

The run() method of RedirectThread does most of the work. It begins by chaining a Writer to the Socket’s output stream and a Reader to the Socket’s input stream. Both input and output are buffered. Then the run() method reads the first line the client sends. Although the client will probably send a whole MIME header, you can ignore that. The first line contains all the information you need. The line looks something like this:

GET /directory/filename.html HTTP/1.0

It is possible that the first word will be POST or PUT instead or that there will be no HTTP version. The second “word” is the file the client wants to retrieve. This must begin with a slash (/). Browsers are responsible for converting relative URLs to absolute URLs that begin with a slash; the server does not do this. The third word is the version of the HTTP protocol the browser understands. Possible values are nothing at all (pre-HTTP/1.0 browsers), HTTP/1.0, or HTTP/1.1.

To handle a request like this, Redirector ignores the first word. The second word is attached to the URL of the target server (stored in the field newSite) to give a full redirected URL. The third word is used to determine whether to send a MIME header; MIME headers are not used for old browsers that do not understand HTTP/1.0. If there is a version, a MIME header is sent; otherwise, it is omitted.

Sending the data is almost trivial. The Writer out is used. Because all the data you send is pure ASCII, the exact encoding isn’t too important. The only trick here is that the end-of-line character for HTTP requests is \r\n–a carriage return followed by a linefeed.

The next lines each send one line of text to the client. The first line printed is:

HTTP/1.0 302 FOUND

This is an HTTP/1.0 response code that tells the client to expect to be redirected. The second line is a Date: header that gives the current time at the server. This line is optional. The third line is the name and version of the server; this line is also optional but is used by spiders that try to keep statistics about the most popular web servers. The next line is the Location: header, which is required for this response type. It tells the client where it is being redirected to. Last is the standard Content-type: header. You send the content type text/html to indicate that the client should expect to see HTML. Finally, a blank line is sent to signify the end of the header data.

Everything after this will be HTML, which is processed by the browser and displayed to the user. The next several lines print a message for browsers that do not support redirection, so those users can manually jump to the new site. That message looks like:

<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>

<BODY><H1>Document moved</H1>

The document / has moved to

<A HREF="http://www.cafeconleche.org/">http://www.cafeconleche.org/</A>.

Please update your bookmarks<P></BODY></HTML>

Finally, the connection is closed and the thread dies.

A Full-Fledged HTTP Server

Enough special-purpose HTTP servers. This next section develops a full-blown HTTP server, called JHTTP, that can serve an entire document tree, including images, applets, HTML files, text files, and more. It will be very similar to the SingleFileHTTPServer, except that it pays attention to the GET requests. This server is still fairly lightweight; after looking at the code, we’ll discuss other features you might want to add.

Because this server may have to read and serve large files from the filesystem over potentially slow network connections, you’ll change its approach. Rather than processing each request as it arrives in the main thread of execution, you’ll place incoming connections in a pool. Separate instances of a RequestProcessor class will remove the connections from the pool and process them. Example 9-12 shows the main JHTTP class. As in the previous two examples, the main() method of JHTTP handles initialization, but other programs can use this class to run basic web servers.

Example 9-12. The JHTTP web server

import java.io.*;

import java.net.*;

import java.util.concurrent.*;

import java.util.logging.*;

public class JHTTP {

private static final Logger logger = Logger.getLogger(

JHTTP.class.getCanonicalName());

private static final int NUM_THREADS = 50;

private static final String INDEX_FILE = "index.html";

private final File rootDirectory;

private final int port;

public JHTTP(File rootDirectory, int port) throws IOException {

if (!rootDirectory.isDirectory()) {

throw new IOException(rootDirectory

+ " does not exist as a directory");

}

this.rootDirectory = rootDirectory;

this.port = port;

}

public void start() throws IOException {

ExecutorService pool = Executors.newFixedThreadPool(NUM_THREADS);

try (ServerSocket server = new ServerSocket(port)) {

logger.info("Accepting connections on port " + server.getLocalPort());

logger.info("Document Root: " + rootDirectory);

while (true) {

try {

Socket request = server.accept();

Runnable r = new RequestProcessor(

rootDirectory, INDEX_FILE, request);

pool.submit(r);

} catch (IOException ex) {

logger.log(Level.WARNING, "Error accepting connection", ex);

}

}

}

}

public static void main(String[] args) {

// get the Document root

File docroot;

try {

docroot = new File(args[0]);

} catch (ArrayIndexOutOfBoundsException ex) {

System.out.println("Usage: java JHTTP docroot port");

return;

}

// set the port to listen on

int port;

try {

port = Integer.parseInt(args[1]);

if (port < 0 || port > 65535) port = 80;

} catch (RuntimeException ex) {

port = 80;

}

try {

JHTTP webserver = new JHTTP(docroot, port);

webserver.start();

} catch (IOException ex) {

logger.log(Level.SEVERE, "Server could not start", ex);

}

}

}

The main() method of the JHTTP class sets the document root directory from args[0]. The port is read from args[1] or 80 is used for a default. Then a new JHTTP object is constructed and started. JHTTP creates a thread pool to handle requests and repeatedly accepts incoming connections. You submit one RequestProcessor thread per incoming connection into the pool.

Each connection is handled by the run() method of the RequestProcessor class shown in Example 9-13. It gets input and output streams from the socket and chains them to a reader and a writer. The reader reads the first line of the client request to determine the version of HTTP that the client supports—you want to send a MIME header only if this is HTTP/1.0 or later—and the requested file. Assuming the method is GET, the file that is requested is converted to a filename on the local filesystem. If the file requested is a directory (i.e., its name ends with a slash), you add the name of an index file. You use the canonical path to make sure that the requested file doesn’t come from outside the document root directory. Otherwise, a sneaky client could walk all over the local filesystem by including .. in URLs to walk up the directory hierarchy. This is all you’ll need from the client, although a more advanced web server, especially one that logged hits, would read the rest of the MIME header the client sends.

Next, the requested file is opened and its contents are read into a byte array. If the HTTP version is 1.0 or later, you write the appropriate MIME headers on the output stream. To figure out the content type, you call theURLConnection.getFileNameMap().getContentTypeFor(fileName) method to map file extensions such as .html onto MIME types such as text/html. The byte array containing the file’s contents is written onto the output stream and the connection is closed. If the file cannot be found or opened, you send the client a 404 response instead. If the client sends a method you don’t support, such as POST, you send back a 501 error. If an exception occurs, you log it, close the connection, and continue.

Example 9-13. The runnable class that handles HTTP requests

import java.io.*;

import java.net.*;

import java.nio.file.Files;

import java.util.*;

import java.util.logging.*;

public class RequestProcessor implements Runnable {

private final static Logger logger = Logger.getLogger(

RequestProcessor.class.getCanonicalName());

private File rootDirectory;

private String indexFileName = "index.html";

private Socket connection;

public RequestProcessor(File rootDirectory,

String indexFileName, Socket connection) {

if (rootDirectory.isFile()) {

throw new IllegalArgumentException(

"rootDirectory must be a directory, not a file");

}

try {

rootDirectory = rootDirectory.getCanonicalFile();

} catch (IOException ex) {

}

this.rootDirectory = rootDirectory;

if (indexFileName != null) this.indexFileName = indexFileName;

this.connection = connection;

}

@Override

public void run() {

// for security checks

String root = rootDirectory.getPath();

try {

OutputStream raw = new BufferedOutputStream(

connection.getOutputStream()

);

Writer out = new OutputStreamWriter(raw);

Reader in = new InputStreamReader(

new BufferedInputStream(

connection.getInputStream()

),"US-ASCII"

);

StringBuilder requestLine = new StringBuilder();

while (true) {

int c = in.read();

if (c == '\r' || c == '\n') break;

requestLine.append((char) c);

}

String get = requestLine.toString();

logger.info(connection.getRemoteSocketAddress() + " " + get);

String[] tokens = get.split("\\s+");

String method = tokens[0];

String version = "";

if (method.equals("GET")) {

String fileName = tokens[1];

if (fileName.endsWith("/")) fileName += indexFileName;

String contentType =

URLConnection.getFileNameMap().getContentTypeFor(fileName);

if (tokens.length > 2) {

version = tokens[2];

}

File theFile = new File(rootDirectory,

fileName.substring(1, fileName.length()));

if (theFile.canRead()

// Don't let clients outside the document root

&& theFile.getCanonicalPath().startsWith(root)) {

byte[] theData = Files.readAllBytes(theFile.toPath());

if (version.startsWith("HTTP/")) { // send a MIME header

sendHeader(out, "HTTP/1.0 200 OK", contentType, theData.length);

}

// send the file; it may be an image or other binary data

// so use the underlying output stream

// instead of the writer

raw.write(theData);

raw.flush();

} else { // can't find the file

String body = new StringBuilder("<HTML>\r\n")

.append("<HEAD><TITLE>File Not Found</TITLE>\r\n")

.append("</HEAD>\r\n")

.append("<BODY>")

.append("<H1>HTTP Error 404: File Not Found</H1>\r\n")

.append("</BODY></HTML>\r\n").toString();

if (version.startsWith("HTTP/")) { // send a MIME header

sendHeader(out, "HTTP/1.0 404 File Not Found",

"text/html; charset=utf-8", body.length());

}

out.write(body);

out.flush();

}

} else { // method does not equal "GET"

String body = new StringBuilder("<HTML>\r\n")

.append("<HEAD><TITLE>Not Implemented</TITLE>\r\n")

.append("</HEAD>\r\n")

.append("<BODY>")

.append("<H1>HTTP Error 501: Not Implemented</H1>\r\n")

.append("</BODY></HTML>\r\n").toString();

if (version.startsWith("HTTP/")) { // send a MIME header

sendHeader(out, "HTTP/1.0 501 Not Implemented",

"text/html; charset=utf-8", body.length());

}

out.write(body);

out.flush();

}

} catch (IOException ex) {

logger.log(Level.WARNING,

"Error talking to " + connection.getRemoteSocketAddress(), ex);

} finally {

try {

connection.close();

}

catch (IOException ex) {}

}

}

private void sendHeader(Writer out, String responseCode,

String contentType, int length)

throws IOException {

out.write(responseCode + "\r\n");

Date now = new Date();

out.write("Date: " + now + "\r\n");

out.write("Server: JHTTP 2.0\r\n");

out.write("Content-length: " + length + "\r\n");

out.write("Content-type: " + contentType + "\r\n\r\n");

out.flush();

}

}

This server is functional but still rather austere. Here are a few features that could be added:

§ A server administration interface

§ Support for the Java Servlet API

§ Support for other request methods, such as POST, HEAD, and PUT

§ Support for multiple document roots so individual users can have their own sites

Finally, spend a little time thinking about ways to optimize this server. If you really want to use JHTTP to run a high-traffic site, there are a couple of things that can speed this server up. The first thing to do is implement smart caching. Keep track of the requests you’ve received and store the data from the most frequently requested files in a Map so that they’re kept in memory. Use a low-priority thread to update this cache. You can also try using nonblocking I/O and channels instead of threads and streams. We’ll explore this possibility in Chapter 11.