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 | } |