001: /*
002: * $Id: HTTPServer.java 6825 2006-04-04 03:00:44Z dfs $
003: *
004: * Copyright 2001,2006 Daniel F. Savarese
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.savarese.org/software/ApacheLicense-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: */
018:
019: package org.savarese.barehttp;
020:
021: import java.io.*;
022: import java.net.*;
023: import java.util.concurrent.*;
024:
025: /**
026: * Implements a server that listens for incoming client connections
027: * and services each with {@link HTTPSession} instances. A port
028: * number, bind address, and maximum number of client connections to
029: * service may be specified.
030: *
031: * @author <a href="http://www.savarese.org/">Daniel F. Savarese</a>
032: */
033: public class HTTPServer {
034:
035: /**
036: * The default maximum number of concurrent client connections (10)
037: * that will be accepted if not specified.
038: */
039: public static final int DEFAULT_MAX_CONNECTIONS = 10;
040:
041: /**
042: * The default port number (8080) to bind to if not specified.
043: */
044: public static final int DEFAULT_PORT = 8080;
045:
046: InetAddress bindAddress;
047: int httpPort, maxConnections, backlog, connectionCount;
048: String documentRoot;
049: ExecutorService executor;
050: Server server;
051:
052: synchronized int incrementConnectionCount() {
053: return ++connectionCount;
054: }
055:
056: synchronized int decrementConnectionCount() {
057: return --connectionCount;
058: }
059:
060: /**
061: * Same as HTTPServer(documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS);
062: */
063: public HTTPServer(String documentRoot) {
064: this (documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS);
065: }
066:
067: /**
068: * Creates an HTTPServer instance.
069: *
070: * @param root The fully qualified document root directory pathname.
071: * @param port The port number the server should bind to.
072: * @param maxConnections The maximum number of client connections the
073: * server should accept.
074: */
075: public HTTPServer(String root, int port, int maxConnections) {
076: setPort(port);
077: setMaxConnections(maxConnections);
078: setBindAddress(null);
079: connectionCount = 0;
080: documentRoot = root;
081: executor = null;
082: server = null;
083: }
084:
085: /**
086: * Returns the document root directory pathname.
087: *
088: * @return The document root directory pathname.
089: */
090: public String getDocumentRoot() {
091: return documentRoot;
092: }
093:
094: /**
095: * Sets the port number the server should bind to. By default, the
096: * server binds to {@link #DEFAULT_PORT}. The new port takes effect
097: * the next time {@link #start} is invoked (after a {@link #stop} if
098: * already running).
099: *
100: * @param port The port number the server should bind to.
101: */
102: public synchronized void setPort(int port) {
103: httpPort = port;
104: }
105:
106: /**
107: * The port number the server will bind to.
108: *
109: * @return The port number the server will bind to.
110: */
111: public int getPort() {
112: return httpPort;
113: }
114:
115: /**
116: * If the server is running, returns the port number currently bound
117: * to. Otherwise, returns -1.
118: *
119: * @return The port number currently bound to or -1 if not bound.
120: */
121: public synchronized int getBoundPort() {
122: if (httpPort == 0 && server != null)
123: return server.socket.getLocalPort();
124: return -1;
125: }
126:
127: /**
128: * Sets the maximum number of concurrent client connections the
129: * server should accept.
130: *
131: * @param maxConnections The maximum number of concurrent client
132: * connections the server should accept.
133: */
134: public synchronized void setMaxConnections(int maxConnections) {
135: this .maxConnections = backlog = maxConnections;
136: if (maxConnections > (DEFAULT_MAX_CONNECTIONS << 1))
137: backlog = maxConnections >> 1;
138: }
139:
140: /**
141: * Returns the maximum number of concurrent client connections that
142: * will be accepted.
143: *
144: * @return The maximum number of concurrent client connections that
145: * will be accepted.
146: */
147: public int getMaxConnections() {
148: return maxConnections;
149: }
150:
151: /**
152: * Returns the number of client connections currently established.
153: *
154: * @return The number of client connections currently established.
155: */
156: public synchronized int getConnectionCount() {
157: return connectionCount;
158: }
159:
160: /**
161: * Sets the network interface address the server should bind to. By
162: * default, the server binds to the wildcard address. The new bind
163: * address takes effect the next time {@link #start} is invoked
164: * (after a {@link #stop} if already running).
165: *
166: * @param bindAddr The network interface the server should bind to.
167: * It may be null to reset to the wildcard.
168: */
169: public synchronized void setBindAddress(InetAddress bindAddr) {
170: bindAddress = bindAddr;
171: }
172:
173: /**
174: * Returns the network interface address the server will bind to. A
175: * null return value signifies the wildcard address.
176: *
177: * @return The network interface address the server will bind to.
178: */
179: public InetAddress getBindAddress() {
180: return bindAddress;
181: }
182:
183: /**
184: * If the server is running, returns the address currently bound
185: * to. Otherwise, returns null.
186: *
187: * @return The port number currently bound to or -1 if not bound.
188: */
189: public synchronized InetAddress getBoundAddress() {
190: if (server != null)
191: return server.socket.getInetAddress();
192: return null;
193: }
194:
195: /**
196: * Returns true if the server is in a running state, false if not.
197: *
198: * @return True if the server is in a running state, false if not.
199: */
200: public synchronized boolean isRunning() {
201: return (executor != null && !executor.isTerminated());
202: }
203:
204: final class Session implements Callable<Void> {
205: HTTPSession session;
206:
207: Session(HTTPSession session) {
208: this .session = session;
209: }
210:
211: public Void call() throws Exception {
212: incrementConnectionCount();
213: session.execute();
214: decrementConnectionCount();
215: return null;
216: }
217: }
218:
219: class Server implements Callable<Void> {
220: ServerSocket socket;
221:
222: public Server(int port, int backlog, InetAddress address)
223: throws IOException {
224: if (address != null)
225: socket = new ServerSocket(port, backlog, bindAddress);
226: else
227: socket = new ServerSocket(port, backlog);
228: }
229:
230: public Void call() throws Exception {
231: try {
232: while (true) {
233: Socket client = socket.accept();
234:
235: if (getConnectionCount() >= maxConnections) {
236: // Ungracefully close connection.
237: client.close();
238: continue;
239: }
240:
241: executor.submit(new Session(new HTTPSession(
242: documentRoot, client.getInputStream(),
243: client.getOutputStream())));
244: }
245: } finally {
246: return null;
247: }
248: }
249:
250: public void close() throws IOException {
251: socket.close();
252: }
253:
254: }
255:
256: /**
257: * Starts listening for incoming connectons in an asynchronously
258: * initiated thread. The method returns immediately after the
259: * listening thread is established, making the HTTPServer instance
260: * an active object.
261: *
262: * @throws IOException If the server socket cannot be bound.
263: * @throws IllegalStateException If the server is already running.
264: */
265: public synchronized void start() throws IOException,
266: IllegalStateException {
267: if (isRunning())
268: throw new IllegalStateException();
269:
270: server = new Server(httpPort, backlog, bindAddress);
271: executor = new ThreadPoolExecutor(0, maxConnections + 1, 60L,
272: TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
273: executor.submit(server);
274: }
275:
276: /**
277: * Schedules termination of the server, closes the server socket,
278: * and waits for the specified amount of time or until the server is
279: * terminated before returning.
280: *
281: * @param timeout The maximum amount of time to wait for termination.
282: * @param unit The unit of time for the timeout.
283: * @return True if the server terminated before the method returned,
284: * false if not. If false is returned, the server will not be
285: * completely terminated untl {@link #isRunning} returns false.
286: * Subsequent calls to stop will have no effect while terminating.
287: */
288: public synchronized boolean stop(long timeout, TimeUnit unit)
289: throws IOException {
290: if (server != null) {
291: boolean result = false;
292:
293: try {
294: executor.shutdown();
295: server.close();
296: result = executor.awaitTermination(timeout, unit);
297: } finally {
298: server = null;
299: return result;
300: }
301: }
302:
303: return isRunning();
304: }
305:
306: }
|