| 1 | /* Copyright 2001,2006,2012 Daniel F. Savarese |
| 2 | * |
| 3 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | * you may not use this file except in compliance with the License. |
| 5 | * You may obtain a copy of the License at |
| 6 | * |
| 7 | * https://www.savarese.org/software/ApacheLicense-2.0 |
| 8 | * |
| 9 | * Unless required by applicable law or agreed to in writing, software |
| 10 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | * See the License for the specific language governing permissions and |
| 13 | * limitations under the License. |
| 14 | */ |
| 15 | |
| 16 | package org.savarese.barehttp; |
| 17 | |
| 18 | import java.io.*; |
| 19 | import java.net.*; |
| 20 | import java.util.concurrent.*; |
| 21 | |
| 22 | /** |
| 23 | * Implements a server that listens for incoming client connections |
| 24 | * and services each with {@link HTTPSession} instances. A port |
| 25 | * number, bind address, and maximum number of client connections to |
| 26 | * service may be specified. |
| 27 | * |
| 28 | * @author <a href="https://www.savarese.org/">Daniel F. Savarese</a> |
| 29 | */ |
| 30 | public class HTTPServer { |
| 31 | |
| 32 | /** |
| 33 | * The default maximum number of concurrent client connections (10) |
| 34 | * that will be accepted if not specified. |
| 35 | */ |
| 36 | public static final int DEFAULT_MAX_CONNECTIONS = 10; |
| 37 | |
| 38 | /** |
| 39 | * The default port number (8080) to bind to if not specified. |
| 40 | */ |
| 41 | public static final int DEFAULT_PORT = 8080; |
| 42 | |
| 43 | InetAddress bindAddress; |
| 44 | int httpPort, maxConnections, backlog, connectionCount; |
| 45 | String documentRoot; |
| 46 | ExecutorService executor; |
| 47 | Server server; |
| 48 | |
| 49 | synchronized int incrementConnectionCount() { |
| 50 | return ++connectionCount; |
| 51 | } |
| 52 | |
| 53 | synchronized int decrementConnectionCount() { |
| 54 | return --connectionCount; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Same as HTTPServer(documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS); |
| 59 | */ |
| 60 | public HTTPServer(String documentRoot) { |
| 61 | this(documentRoot, DEFAULT_PORT, DEFAULT_MAX_CONNECTIONS); |
| 62 | } |
| 63 | |
| 64 | /** |
| 65 | * Creates an HTTPServer instance. |
| 66 | * |
| 67 | * @param root The fully qualified document root directory pathname. |
| 68 | * @param port The port number the server should bind to. |
| 69 | * @param maxConnections The maximum number of client connections the |
| 70 | * server should accept. |
| 71 | */ |
| 72 | public HTTPServer(String root, int port, int maxConnections) { |
| 73 | setPort(port); |
| 74 | setMaxConnections(maxConnections); |
| 75 | setBindAddress(null); |
| 76 | connectionCount = 0; |
| 77 | documentRoot = root; |
| 78 | executor = null; |
| 79 | server = null; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Returns the document root directory pathname. |
| 84 | * |
| 85 | * @return The document root directory pathname. |
| 86 | */ |
| 87 | public String getDocumentRoot() { |
| 88 | return documentRoot; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Sets the port number the server should bind to. By default, the |
| 93 | * server binds to {@link #DEFAULT_PORT}. The new port takes effect |
| 94 | * the next time {@link #start} is invoked (after a {@link #stop} if |
| 95 | * already running). |
| 96 | * |
| 97 | * @param port The port number the server should bind to. |
| 98 | */ |
| 99 | public synchronized void setPort(int port) { |
| 100 | httpPort = port; |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * The port number the server will bind to. |
| 105 | * |
| 106 | * @return The port number the server will bind to. |
| 107 | */ |
| 108 | public int getPort() { |
| 109 | return httpPort; |
| 110 | } |
| 111 | |
| 112 | /** |
| 113 | * If the server is running, returns the port number currently bound |
| 114 | * to. Otherwise, returns -1. |
| 115 | * |
| 116 | * @return The port number currently bound to or -1 if not bound. |
| 117 | */ |
| 118 | public synchronized int getBoundPort() { |
| 119 | if(httpPort == 0 && server != null) { |
| 120 | return server.socket.getLocalPort(); |
| 121 | } |
| 122 | return -1; |
| 123 | } |
| 124 | |
| 125 | /** |
| 126 | * Sets the maximum number of concurrent client connections the |
| 127 | * server should accept. |
| 128 | * |
| 129 | * @param maxConnections The maximum number of concurrent client |
| 130 | * connections the server should accept. |
| 131 | */ |
| 132 | public synchronized void setMaxConnections(int maxConnections) { |
| 133 | this.maxConnections = backlog = maxConnections; |
| 134 | if(maxConnections > (DEFAULT_MAX_CONNECTIONS << 1)) { |
| 135 | backlog = maxConnections >> 1; |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Returns the maximum number of concurrent client connections that |
| 141 | * will be accepted. |
| 142 | * |
| 143 | * @return The maximum number of concurrent client connections that |
| 144 | * will be accepted. |
| 145 | */ |
| 146 | public int getMaxConnections() { |
| 147 | return maxConnections; |
| 148 | } |
| 149 | |
| 150 | /** |
| 151 | * Returns the number of client connections currently established. |
| 152 | * |
| 153 | * @return The number of client connections currently established. |
| 154 | */ |
| 155 | public synchronized int getConnectionCount() { |
| 156 | return connectionCount; |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Sets the network interface address the server should bind to. By |
| 161 | * default, the server binds to the wildcard address. The new bind |
| 162 | * address takes effect the next time {@link #start} is invoked |
| 163 | * (after a {@link #stop} if already running). |
| 164 | * |
| 165 | * @param bindAddr The network interface the server should bind to. |
| 166 | * It may be null to reset to the wildcard. |
| 167 | */ |
| 168 | public synchronized void setBindAddress(InetAddress bindAddr) { |
| 169 | bindAddress = bindAddr; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Returns the network interface address the server will bind to. A |
| 174 | * null return value signifies the wildcard address. |
| 175 | * |
| 176 | * @return The network interface address the server will bind to. |
| 177 | */ |
| 178 | public InetAddress getBindAddress() { |
| 179 | return bindAddress; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * If the server is running, returns the address currently bound |
| 184 | * to. Otherwise, returns null. |
| 185 | * |
| 186 | * @return The port number currently bound to or -1 if not bound. |
| 187 | */ |
| 188 | public synchronized InetAddress getBoundAddress() { |
| 189 | if(server != null) { |
| 190 | return server.socket.getInetAddress(); |
| 191 | } |
| 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 | |
| 232 | public Void call() throws Exception { |
| 233 | try { |
| 234 | while(true) { |
| 235 | Socket client = socket.accept(); |
| 236 | |
| 237 | if(getConnectionCount() >= maxConnections) { |
| 238 | // Ungracefully close connection. |
| 239 | client.close(); |
| 240 | continue; |
| 241 | } |
| 242 | |
| 243 | executor.submit(new Session(new HTTPSession(documentRoot, |
| 244 | client.getInputStream(), |
| 245 | client.getOutputStream()))); |
| 246 | } |
| 247 | } finally { |
| 248 | return null; |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | public void close() throws IOException { |
| 253 | socket.close(); |
| 254 | } |
| 255 | |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * Starts listening for incoming connectons in an asynchronously |
| 260 | * initiated thread. The method returns immediately after the |
| 261 | * listening thread is established, making the HTTPServer instance |
| 262 | * an active object. |
| 263 | * |
| 264 | * @throws IOException If the server socket cannot be bound. |
| 265 | * @throws IllegalStateException If the server is already running. |
| 266 | */ |
| 267 | public synchronized void start() throws IOException, IllegalStateException { |
| 268 | if(isRunning()) { |
| 269 | throw new IllegalStateException(); |
| 270 | } |
| 271 | |
| 272 | server = new Server(httpPort, backlog, bindAddress); |
| 273 | executor = |
| 274 | new ThreadPoolExecutor(0, maxConnections + 1, 60L, TimeUnit.SECONDS, |
| 275 | new SynchronousQueue<Runnable>()); |
| 276 | executor.submit(server); |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * Schedules termination of the server, closes the server socket, |
| 281 | * and waits for the specified amount of time or until the server is |
| 282 | * terminated before returning. |
| 283 | * |
| 284 | * @param timeout The maximum amount of time to wait for termination. |
| 285 | * @param unit The unit of time for the timeout. |
| 286 | * @return True if the server terminated before the method returned, |
| 287 | * false if not. If false is returned, the server will not be |
| 288 | * completely terminated untl {@link #isRunning} returns false. |
| 289 | * Subsequent calls to stop will have no effect while terminating. |
| 290 | */ |
| 291 | public synchronized boolean stop(long timeout, TimeUnit unit) |
| 292 | throws IOException |
| 293 | { |
| 294 | if(server != null) { |
| 295 | boolean result = false; |
| 296 | |
| 297 | try { |
| 298 | executor.shutdown(); |
| 299 | server.close(); |
| 300 | result = executor.awaitTermination(timeout, unit); |
| 301 | } finally { |
| 302 | server = null; |
| 303 | return result; |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | return isRunning(); |
| 308 | } |
| 309 | |
| 310 | } |