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