A generic framework for a flexible, multi-threaded server : Server « Network Protocol « Java






A generic framework for a flexible, multi-threaded server

  
/*
 * Copyright (c) 2000 David Flanagan.  All rights reserved.
 * This code is from the book Java Examples in a Nutshell, 2nd Edition.
 * It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
 * You may study, use, and modify it for any non-commercial purpose.
 * You may distribute it non-commercially as long as you retain this notice.
 * For a commercial use license, or to purchase the book (recommended),
 * visit http://www.davidflanagan.com/javaexamples2.
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

/**
 * This class is a generic framework for a flexible, multi-threaded server. It
 * listens on any number of specified ports, and, when it receives a connection
 * on a port, passes input and output streams to a specified Service object
 * which provides the actual service. It can limit the number of concurrent
 * connections, and logs activity to a specified stream.
 */
public class Server {
  /**
   * A main() method for running the server as a standalone program. The
   * command-line arguments to the program should be pairs of servicenames and
   * port numbers. For each pair, the program will dynamically load the named
   * Service class, instantiate it, and tell the server to provide that
   * Service on the specified port. The special -control argument should be
   * followed by a password and port, and will start special server control
   * service running on the specified port, protected by the specified
   * password.
   */
  public static void main(String[] args) {
    try {
      if (args.length < 2) // Check number of arguments
        throw new IllegalArgumentException("Must specify a service");

      // Create a Server object that uses standard out as its log and
      // has a limit of ten concurrent connections at once.
      Server s = new Server(System.out, 10);

      // Parse the argument list
      int i = 0;
      while (i < args.length) {
        if (args[i].equals("-control")) { // Handle the -control arg
          i++;
          String password = args[i++];
          int port = Integer.parseInt(args[i++]);
          // add control service
          s.addService(new Control(s, password), port);
        } else {
          // Otherwise start a named service on the specified port.
          // Dynamically load and instantiate a Service class
          String serviceName = args[i++];
          Class serviceClass = Class.forName(serviceName);
          Service service = (Service) serviceClass.newInstance();
          int port = Integer.parseInt(args[i++]);
          s.addService(service, port);
        }
      }
    } catch (Exception e) { // Display a message if anything goes wrong
      System.err.println("Server: " + e);
      System.err.println("Usage: java Server "
          + "[-control <password> <port>] "
          + "[<servicename> <port> ... ]");
      System.exit(1);
    }
  }

  // This is the state for the server
  Map services; // Hashtable mapping ports to Listeners

  Set connections; // The set of current connections

  int maxConnections; // The concurrent connection limit

  ThreadGroup threadGroup; // The threadgroup for all our threads

  PrintWriter logStream; // Where we send our logging output to

  /**
   * This is the Server() constructor. It must be passed a stream to send log
   * output to (may be null), and the limit on the number of concurrent
   * connections.
   */
  public Server(OutputStream logStream, int maxConnections) {
    setLogStream(logStream);
    log("Starting server");
    threadGroup = new ThreadGroup(Server.class.getName());
    this.maxConnections = maxConnections;
    services = new HashMap();
    connections = new HashSet(maxConnections);
  }

  /**
   * A public method to set the current logging stream. Pass null to turn
   * logging off
   */
  public synchronized void setLogStream(OutputStream out) {
    if (out != null)
      logStream = new PrintWriter(out);
    else
      logStream = null;
  }

  /** Write the specified string to the log */
  protected synchronized void log(String s) {
    if (logStream != null) {
      logStream.println("[" + new Date() + "] " + s);
      logStream.flush();
    }
  }

  /** Write the specified object to the log */
  protected void log(Object o) {
    log(o.toString());
  }

  /**
   * This method makes the server start providing a new service. It runs the
   * specified Service object on the specified port.
   */
  public synchronized void addService(Service service, int port)
      throws IOException {
    Integer key = new Integer(port); // the hashtable key
    // Check whether a service is already on that port
    if (services.get(key) != null)
      throw new IllegalArgumentException("Port " + port
          + " already in use.");
    // Create a Listener object to listen for connections on the port
    Listener listener = new Listener(threadGroup, port, service);
    // Store it in the hashtable
    services.put(key, listener);
    // Log it
    log("Starting service " + service.getClass().getName() + " on port "
        + port);
    // Start the listener running.
    listener.start();
  }

  /**
   * This method makes the server stop providing a service on a port. It does
   * not terminate any pending connections to that service, merely causes the
   * server to stop accepting new connections
   */
  public synchronized void removeService(int port) {
    Integer key = new Integer(port); // hashtable key
    // Look up the Listener object for the port in the hashtable
    final Listener listener = (Listener) services.get(key);
    if (listener == null)
      return;
    // Ask the listener to stop
    listener.pleaseStop();
    // Remove it from the hashtable
    services.remove(key);
    // And log it.
    log("Stopping service " + listener.service.getClass().getName()
        + " on port " + port);
  }

  /**
   * This nested Thread subclass is a "listener". It listens for connections
   * on a specified port (using a ServerSocket) and when it gets a connection
   * request, it calls the servers addConnection() method to accept (or
   * reject) the connection. There is one Listener for each Service being
   * provided by the Server.
   */
  public class Listener extends Thread {
    ServerSocket listen_socket; // The socket to listen for connections

    int port; // The port we're listening on

    Service service; // The service to provide on that port

    volatile boolean stop = false; // Whether we've been asked to stop

    /**
     * The Listener constructor creates a thread for itself in the
     * threadgroup. It creates a ServerSocket to listen for connections on
     * the specified port. It arranges for the ServerSocket to be
     * interruptible, so that services can be removed from the server.
     */
    public Listener(ThreadGroup group, int port, Service service)
        throws IOException {
      super(group, "Listener:" + port);
      listen_socket = new ServerSocket(port);
      // give it a non-zero timeout so accept() can be interrupted
      listen_socket.setSoTimeout(600000);
      this.port = port;
      this.service = service;
    }

    /***********************************************************************
     * This is the polite way to get a Listener to stop accepting
     * connections
     **********************************************************************/
    public void pleaseStop() {
      this.stop = true; // Set the stop flag
      this.interrupt(); // Stop blocking in accept()
      try {
        listen_socket.close();
      } // Stop listening.
      catch (IOException e) {
      }
    }

    /**
     * A Listener is a Thread, and this is its body. Wait for connection
     * requests, accept them, and pass the socket on to the addConnection
     * method of the server.
     */
    public void run() {
      while (!stop) { // loop until we're asked to stop.
        try {
          Socket client = listen_socket.accept();
          addConnection(client, service);
        } catch (InterruptedIOException e) {
        } catch (IOException e) {
          log(e);
        }
      }
    }
  }

  /**
   * This is the method that Listener objects call when they accept a
   * connection from a client. It either creates a Connection object for the
   * connection and adds it to the list of current connections, or, if the
   * limit on connections has been reached, it closes the connection.
   */
  protected synchronized void addConnection(Socket s, Service service) {
    // If the connection limit has been reached
    if (connections.size() >= maxConnections) {
      try {
        // Then tell the client it is being rejected.
        PrintWriter out = new PrintWriter(s.getOutputStream());
        out.print("Connection refused; "
            + "the server is busy; please try again later.\n");
        out.flush();
        // And close the connection to the rejected client.
        s.close();
        // And log it, of course
        log("Connection refused to "
            + s.getInetAddress().getHostAddress() + ":"
            + s.getPort() + ": max connections reached.");
      } catch (IOException e) {
        log(e);
      }
    } else { // Otherwise, if the limit has not been reached
      // Create a Connection thread to handle this connection
      Connection c = new Connection(s, service);
      // Add it to the list of current connections
      connections.add(c);
      // Log this new connection
      log("Connected to " + s.getInetAddress().getHostAddress() + ":"
          + s.getPort() + " on port " + s.getLocalPort()
          + " for service " + service.getClass().getName());
      // And start the Connection thread to provide the service
      c.start();
    }
  }

  /**
   * A Connection thread calls this method just before it exits. It removes
   * the specified Connection from the set of connections.
   */
  protected synchronized void endConnection(Connection c) {
    connections.remove(c);
    log("Connection to " + c.client.getInetAddress().getHostAddress() + ":"
        + c.client.getPort() + " closed.");
  }

  /** Change the current connection limit */
  public synchronized void setMaxConnections(int max) {
    maxConnections = max;
  }

  /**
   * This method displays status information about the server on the specified
   * stream. It can be used for debugging, and is used by the Control service
   * later in this example.
   */
  public synchronized void displayStatus(PrintWriter out) {
    // Display a list of all Services that are being provided
    Iterator keys = services.keySet().iterator();
    while (keys.hasNext()) {
      Integer port = (Integer) keys.next();
      Listener listener = (Listener) services.get(port);
      out.print("SERVICE " + listener.service.getClass().getName()
          + " ON PORT " + port + "\n");
    }

    // Display the current connection limit
    out.print("MAX CONNECTIONS: " + maxConnections + "\n");

    // Display a list of all current connections
    Iterator conns = connections.iterator();
    while (conns.hasNext()) {
      Connection c = (Connection) conns.next();
      out.print("CONNECTED TO "
          + c.client.getInetAddress().getHostAddress() + ":"
          + c.client.getPort() + " ON PORT "
          + c.client.getLocalPort() + " FOR SERVICE "
          + c.service.getClass().getName() + "\n");
    }
  }

  /**
   * This class is a subclass of Thread that handles an individual connection
   * between a client and a Service provided by this server. Because each such
   * connection has a thread of its own, each Service can have multiple
   * connections pending at once. Despite all the other threads in use, this
   * is the key feature that makes this a multi-threaded server
   * implementation.
   */
  public class Connection extends Thread {
    Socket client; // The socket to talk to the client through

    Service service; // The service being provided to that client

    /**
     * This constructor just saves some state and calls the superclass
     * constructor to create a thread to handle the connection. Connection
     * objects are created by Listener threads. These threads are part of
     * the server's ThreadGroup, so all Connection threads are part of that
     * group, too.
     */
    public Connection(Socket client, Service service) {
      super("Server.Connection:"
          + client.getInetAddress().getHostAddress() + ":"
          + client.getPort());
      this.client = client;
      this.service = service;
    }

    /**
     * This is the body of each and every Connection thread. All it does is
     * pass the client input and output streams to the serve() method of the
     * specified Service object. That method is responsible for reading from
     * and writing to those streams to provide the actual service. Recall
     * that the Service object has been passed from the Server.addService()
     * method to a Listener object to the addConnection() method to this
     * Connection object, and is now finally being used to provide the
     * service. Note that just before this thread exits it always calls the
     * endConnection() method to remove itself from the set of connections
     */
    public void run() {
      try {
        InputStream in = client.getInputStream();
        OutputStream out = client.getOutputStream();
        service.serve(in, out);
      } catch (IOException e) {
        log(e);
      } finally {
        endConnection(this);
      }
    }
  }

  /**
   * Here is the Service interface that we have seen so much of. It defines
   * only a single method which is invoked to provide the service. serve()
   * will be passed an input stream and an output stream to the client. It
   * should do whatever it wants with them, and should close them before
   * returning.
   * 
   * All connections through the same port to this service share a single
   * Service object. Thus, any state local to an individual connection must be
   * stored in local variables within the serve() method. State that should be
   * global to all connections on the same port should be stored in instance
   * variables of the Service class. If the same Service is running on more
   * than one port, there will typically be different Service instances for
   * each port. Data that should be global to all connections on any port
   * should be stored in static variables.
   * 
   * Note that implementations of this interface must have a no-argument
   * constructor if they are to be dynamically instantiated by the main()
   * method of the Server class.
   */
  public interface Service {
    public void serve(InputStream in, OutputStream out) throws IOException;
  }

  /**
   * A very simple service. It displays the current time on the server to the
   * client, and closes the connection.
   */
  public static class Time implements Service {
    public void serve(InputStream i, OutputStream o) throws IOException {
      PrintWriter out = new PrintWriter(o);
      out.print(new Date() + "\n");
      out.close();
      i.close();
    }
  }

  /**
   * This is another example service. It reads lines of input from the client,
   * and sends them back, reversed. It also displays a welcome message and
   * instructions, and closes the connection when the user enters a '.' on a
   * line by itself.
   */
  public static class Reverse implements Service {
    public void serve(InputStream i, OutputStream o) throws IOException {
      BufferedReader in = new BufferedReader(new InputStreamReader(i));
      PrintWriter out = new PrintWriter(new BufferedWriter(
          new OutputStreamWriter(o)));
      out.print("Welcome to the line reversal server.\n");
      out.print("Enter lines.  End with a '.' on a line by itself.\n");
      for (;;) {
        out.print("> ");
        out.flush();
        String line = in.readLine();
        if ((line == null) || line.equals("."))
          break;
        for (int j = line.length() - 1; j >= 0; j--)
          out.print(line.charAt(j));
        out.print("\n");
      }
      out.close();
      in.close();
    }
  }

  /**
   * This service is an HTTP mirror, just like the HttpMirror class
   * implemented earlier in this chapter. It echos back the client's HTTP
   * request
   */
  public static class HTTPMirror implements Service {
    public void serve(InputStream i, OutputStream o) throws IOException {
      BufferedReader in = new BufferedReader(new InputStreamReader(i));
      PrintWriter out = new PrintWriter(o);
      out.print("HTTP/1.0 200 \n");
      out.print("Content-Type: text/plain\n\n");
      String line;
      while ((line = in.readLine()) != null) {
        if (line.length() == 0)
          break;
        out.print(line + "\n");
      }
      out.close();
      in.close();
    }
  }

  /**
   * This service demonstrates how to maintain state across connections by
   * saving it in instance variables and using synchronized access to those
   * variables. It maintains a count of how many clients have connected and
   * tells each client what number it is
   */
  public static class UniqueID implements Service {
    public int id = 0;

    public synchronized int nextId() {
      return id++;
    }

    public void serve(InputStream i, OutputStream o) throws IOException {
      PrintWriter out = new PrintWriter(o);
      out.print("You are client #: " + nextId() + "\n");
      out.close();
      i.close();
    }
  }

  /**
   * This is a non-trivial service. It implements a command-based protocol
   * that gives password-protected runtime control over the operation of the
   * server. See the main() method of the Server class to see how this service
   * is started.
   * 
   * The recognized commands are: password: give password; authorization is
   * required for most commands add: dynamically add a named service on a
   * specified port remove: dynamically remove the service running on a
   * specified port max: change the current maximum connection limit. status:
   * display current services, connections, and connection limit help: display
   * a help message quit: disconnect
   * 
   * This service displays a prompt, and sends all of its output to the user
   * in capital letters. Only one client is allowed to connect to this service
   * at a time.
   */
  public static class Control implements Service {
    Server server; // The server we control

    String password; // The password we require

    boolean connected = false; // Whether a client is already connected

    /**
     * Create a new Control service. It will control the specified Server
     * object, and will require the specified password for authorization
     * Note that this Service does not have a no argument constructor, which
     * means that it cannot be dynamically instantiated and added as the
     * other, generic services above can be.
     */
    public Control(Server server, String password) {
      this.server = server;
      this.password = password;
    }

    /**
     * This is the serve method that provides the service. It reads a line
     * the client, and uses java.util.StringTokenizer to parse it into
     * commands and arguments. It does various things depending on the
     * command.
     */
    public void serve(InputStream i, OutputStream o) throws IOException {
      // Setup the streams
      BufferedReader in = new BufferedReader(new InputStreamReader(i));
      PrintWriter out = new PrintWriter(o);
      String line; // For reading client input lines
      // Has the user has given the password yet?
      boolean authorized = false;

      // If there is already a client connected to this service, display
      // a message to this client and close the connection. We use a
      // synchronized block to prevent a race condition.
      synchronized (this) {
        if (connected) {
          out.print("ONLY ONE CONTROL CONNECTION ALLOWED.\n");
          out.close();
          return;
        } else
          connected = true;
      }

      // This is the main loop: read a command, parse it, and handle it
      for (;;) { // infinite loop
        out.print("> "); // Display a prompt
        out.flush(); // Make it appear right away
        line = in.readLine(); // Get the user's input
        if (line == null)
          break; // Quit if we get EOF.
        try {
          // Use a StringTokenizer to parse the user's command
          StringTokenizer t = new StringTokenizer(line);
          if (!t.hasMoreTokens())
            continue; // if input was empty
          // Get first word of the input and convert to lower case
          String command = t.nextToken().toLowerCase();
          // Now compare to each of the possible commands, doing the
          // appropriate thing for each command
          if (command.equals("password")) { // Password command
            String p = t.nextToken(); // Get the next word
            if (p.equals(this.password)) { // Is it the password?
              out.print("OK\n"); // Say so
              authorized = true; // Grant authorization
            } else
              out.print("INVALID PASSWORD\n"); // Otherwise fail
          } else if (command.equals("add")) { // Add Service command
            // Check whether password has been given
            if (!authorized)
              out.print("PASSWORD REQUIRED\n");
            else {
              // Get the name of the service and try to
              // dynamically load and instantiate it.
              // Exceptions will be handled below
              String serviceName = t.nextToken();
              Class serviceClass = Class.forName(serviceName);
              Service service;
              try {
                service = (Service) serviceClass.newInstance();
              } catch (NoSuchMethodError e) {
                throw new IllegalArgumentException(
                    "Service must have a "
                        + "no-argument constructor");
              }
              int port = Integer.parseInt(t.nextToken());
              // If no exceptions occurred, add the service
              server.addService(service, port);
              out.print("SERVICE ADDED\n"); // acknowledge
            }
          } else if (command.equals("remove")) { // Remove service
            if (!authorized)
              out.print("PASSWORD REQUIRED\n");
            else {
              int port = Integer.parseInt(t.nextToken());
              server.removeService(port); // remove the service
              out.print("SERVICE REMOVED\n"); // acknowledge
            }
          } else if (command.equals("max")) { // Set connection limit
            if (!authorized)
              out.print("PASSWORD REQUIRED\n");
            else {
              int max = Integer.parseInt(t.nextToken());
              server.setMaxConnections(max);
              out.print("MAX CONNECTIONS CHANGED\n");
            }
          } else if (command.equals("status")) { // Status Display
            if (!authorized)
              out.print("PASSWORD REQUIRED\n");
            else
              server.displayStatus(out);
          } else if (command.equals("help")) { // Help command
            // Display command syntax. Password not required
            out.print("COMMANDS:\n" + "\tpassword <password>\n"
                + "\tadd <service> <port>\n"
                + "\tremove <port>\n"
                + "\tmax <max-connections>\n" + "\tstatus\n"
                + "\thelp\n" + "\tquit\n");
          } else if (command.equals("quit"))
            break; // Quit command.
          else
            out.print("UNRECOGNIZED COMMAND\n"); // Error
        } catch (Exception e) {
          out.print("ERROR WHILE PARSING OR EXECUTING COMMAND:\n" + e
              + "\n");
        }
      }
      connected = false;
      out.close();
      in.close();
    }
  }
}

           
         
    
  








Related examples in the same category

1.Server allows connections on socket 6123Server allows connections on socket 6123
2.This server displays messages to a single clientThis server displays messages to a single client
3.The client can specify information to control the output of a serverThe client can specify information to control the output of a server
4.A server can use specialized streams to deliver typed dataA server can use specialized streams to deliver typed data
5.Serve entire objects using ObjectOutputStreamServe entire objects using ObjectOutputStream
6.A multithreaded serverA multithreaded server
7.Base class to build multithreaded servers easily
8.Manage a pool of threads for clients
9.Client estimates the speed of the network connection to the server
10.This server retrieves the time using the RFC867 protocol.This server retrieves the time using the RFC867 protocol.
11.Quote Server
12.Logging Server based on SocketServer
13.Client and Server Demo
14.Reflector
15.Simple Http Server