| 1 | /* |
| 2 | * $Id$ |
| 3 | * |
| 4 | * Copyright 2001,2006 Daniel F. Savarese |
| 5 | * |
| 6 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | * you may not use this file except in compliance with the License. |
| 8 | * You may obtain a copy of the License at |
| 9 | * |
| 10 | * http://www.savarese.org/software/ApacheLicense-2.0 |
| 11 | * |
| 12 | * Unless required by applicable law or agreed to in writing, software |
| 13 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | * See the License for the specific language governing permissions and |
| 16 | * limitations under the License. |
| 17 | */ |
| 18 | |
| 19 | package org.savarese.barehttp; |
| 20 | |
| 21 | import java.io.*; |
| 22 | import java.net.*; |
| 23 | import java.util.concurrent.*; |
| 24 | |
| 25 | /** |
| 26 | * Implements a server that listens for incoming client connections |
| 27 | * and services each with {@link HTTPSession} instances. A port |
| 28 | * number, bind address, and maximum number of client connections to |
| 29 | * service may be specified. |
| 30 | * |
| 31 | * @author <a href="http://www.savarese.org/">Daniel F. Savarese</a> |
| 32 | */ |
| 33 | public class HTTPServer { |
| 34 | |
| 35 | /** |
| 36 | * The default maximum number of concurrent client connections (10) |
| 37 | * that will be accepted if not specified. |
| 38 | */ |
| 39 | public static final int DEFAULT_MAX_CONNECTIONS = 10; |
| 40 | |
| 41 | /** |
| 42 | * The default port number (8080) to bind to if not specified. |
| 43 | */ |
| 44 | public static final int DEFAULT_PORT = 8080; |
| 45 | |
| 46 | InetAddress bindAddress; |
| 47 | int httpPort, maxConnections, backlog, connectionCount; |
| 48 | String documentRoot; |
| 49 | ExecutorService executor; |
| 50 | Server server; |
| 51 | |
| 52 | synchronized int incrementConnectionCount() { |
| 53 | return ++connectionCount; |
| 54 | } |
| 55 | |
| 56 | synchronized int decrementConnectionCount() { |
| 57 | return --connectionCount; |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Same as HTTPServer(documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS); |
| 62 | */ |
| 63 | public HTTPServer(String documentRoot) { |
| 64 | this(documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Creates an HTTPServer instance. |
| 69 | * |
| 70 | * @param root The fully qualified document root directory pathname. |
| 71 | * @param port The port number the server should bind to. |
| 72 | * @param maxConnections The maximum number of client connections the |
| 73 | * server should accept. |
| 74 | */ |
| 75 | public HTTPServer(String root, int port, int maxConnections) { |
| 76 | setPort(port); |
| 77 | setMaxConnections(maxConnections); |
| 78 | setBindAddress(null); |
| 79 | connectionCount = 0; |
| 80 | documentRoot = root; |
| 81 | executor = null; |
| 82 | server = null; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Returns the document root directory pathname. |
| 87 | * |
| 88 | * @return The document root directory pathname. |
| 89 | */ |
| 90 | public String getDocumentRoot() { |
| 91 | return documentRoot; |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * Sets the port number the server should bind to. By default, the |
| 96 | * server binds to {@link #DEFAULT_PORT}. The new port takes effect |
| 97 | * the next time {@link #start} is invoked (after a {@link #stop} if |
| 98 | * already running). |
| 99 | * |
| 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 | { |
| 225 | if(address != null) |
| 226 | socket = new ServerSocket(port, backlog, bindAddress); |
| 227 | else |
| 228 | socket = new ServerSocket(port, backlog); |
| 229 | } |
| 230 | |
| 231 | public Void call() throws Exception { |
| 232 | try { |
| 233 | while(true) { |
| 234 | Socket client = socket.accept(); |
| 235 | |
| 236 | if(getConnectionCount() >= maxConnections) { |
| 237 | // Ungracefully close connection. |
| 238 | client.close(); |
| 239 | continue; |
| 240 | } |
| 241 | |
| 242 | executor.submit(new Session(new HTTPSession(documentRoot, |
| 243 | client.getInputStream(), |
| 244 | client.getOutputStream()))); |
| 245 | } |
| 246 | } finally { |
| 247 | return null; |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | public void close() throws IOException { |
| 252 | socket.close(); |
| 253 | } |
| 254 | |
| 255 | } |
| 256 | |
| 257 | /** |
| 258 | * Starts listening for incoming connectons in an asynchronously |
| 259 | * initiated thread. The method returns immediately after the |
| 260 | * listening thread is established, making the HTTPServer instance |
| 261 | * an active object. |
| 262 | * |
| 263 | * @throws IOException If the server socket cannot be bound. |
| 264 | * @throws IllegalStateException If the server is already running. |
| 265 | */ |
| 266 | public synchronized void start() throws IOException, IllegalStateException { |
| 267 | if(isRunning()) |
| 268 | throw new IllegalStateException(); |
| 269 | |
| 270 | server = new Server(httpPort, backlog, bindAddress); |
| 271 | executor = |
| 272 | new ThreadPoolExecutor(0, maxConnections + 1, 60L, TimeUnit.SECONDS, |
| 273 | new SynchronousQueue<Runnable>()); |
| 274 | executor.submit(server); |
| 275 | } |
| 276 | |
| 277 | /** |
| 278 | * Schedules termination of the server, closes the server socket, |
| 279 | * and waits for the specified amount of time or until the server is |
| 280 | * terminated before returning. |
| 281 | * |
| 282 | * @param timeout The maximum amount of time to wait for termination. |
| 283 | * @param unit The unit of time for the timeout. |
| 284 | * @return True if the server terminated before the method returned, |
| 285 | * false if not. If false is returned, the server will not be |
| 286 | * completely terminated untl {@link #isRunning} returns false. |
| 287 | * Subsequent calls to stop will have no effect while terminating. |
| 288 | */ |
| 289 | public synchronized boolean stop(long timeout, TimeUnit unit) |
| 290 | throws IOException |
| 291 | { |
| 292 | if(server != null) { |
| 293 | boolean result = false; |
| 294 | |
| 295 | try { |
| 296 | executor.shutdown(); |
| 297 | server.close(); |
| 298 | result = executor.awaitTermination(timeout, unit); |
| 299 | } finally { |
| 300 | server = null; |
| 301 | return result; |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | return isRunning(); |
| 306 | } |
| 307 | |
| 308 | } |