Java Socket服务器端编程

Published on 2017 - 05 - 03

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. 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 at time-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();
      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. 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 1 puts this all together. It uses Java 7’s try-with-resources to autoclose the sockets.

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.

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

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

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

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 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 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 to limit the potential resource usage. Fifty threads should be plenty. Example 4 shouldn’t crash no matter what load it’s under. It may start refusing connections, but it won’t crash.

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 4 is structured much like Example 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 5, the server spawns up to 500 threads.

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 a ServerSocket 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 6 demonstrates by adding logging to the daytime server.

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

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

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

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.

Reference