1 /**
2 * Contains the core HTTP server components.
3 */4 modulehandy_httpd.server;
5 6 importstd.stdio;
7 importstd.socket;
8 importstd.regex;
9 importstd.container.dlist : DList;
10 importcore.sync.semaphore : Semaphore;
11 importcore.atomic : atomicLoad;
12 importcore.thread.threadgroup : ThreadGroup;
13 14 importhandy_httpd.request;
15 importhandy_httpd.response;
16 importhandy_httpd.handler;
17 importhandy_httpd.parse_utils : parseRequest, Msg;
18 19 importhttparsed : MsgParser, initParser;
20 21 /**
22 * A simple HTTP server that accepts requests on a given port and address, and
23 * lets a configured HttpRequestHandler produce a response, to send back to the
24 * client.
25 */26 classHttpServer {
27 privateAddressaddress;
28 privatesize_treceiveBufferSize;
29 privateintconnectionQueueSize;
30 privatesize_tworkerPoolSize;
31 privateboolverbose;
32 privateHttpRequestHandlerhandler;
33 privatesharedboolready = false;
34 privateSocketserverSocket = null;
35 privateSemaphorerequestSemaphore;
36 privateDList!SocketrequestQueue;
37 38 this(
39 HttpRequestHandlerhandler = noOpHandler(),
40 stringhostname = "127.0.0.1",
41 ushortport = 8080,
42 size_treceiveBufferSize = 8192,
43 intconnectionQueueSize = 100,
44 boolverbose = false,
45 size_tworkerPoolSize = 2546 ) {
47 this.address = parseAddress(hostname, port);
48 this.receiveBufferSize = receiveBufferSize;
49 this.connectionQueueSize = connectionQueueSize;
50 this.workerPoolSize = workerPoolSize;
51 this.verbose = verbose;
52 this.handler = handler;
53 }
54 55 /**
56 * Will be called before the socket is bound to the address. One can set
57 * special socket options in here by overriding it.
58 *
59 * Note: one application would be to add SocketOption.REUSEADDR, in
60 * order to prevent long TIME_WAIT states preventing quick restarts
61 * of the server after termination on some systems. Learn more about it
62 * here: https://stackoverflow.com/a/14388707.
63 */64 protectedvoidconfigurePreBind(Socketsocket) {}
65 66 /**
67 * Starts the server on the calling thread, so that it will begin accepting
68 * HTTP requests. Once the server is able to accept requests, `isReady()`
69 * will return true, and will remain true until the server is stopped by
70 * calling `stop()`.
71 */72 publicvoidstart() {
73 serverSocket = newTcpSocket();
74 configurePreBind(serverSocket);
75 serverSocket.bind(this.address);
76 if (this.verbose) writefln!"Bound to address %s"(this.address);
77 serverSocket.listen(this.connectionQueueSize);
78 this.ready = true;
79 80 // Initialize worker threads.81 this.requestSemaphore = newSemaphore();
82 ThreadGrouptg = newThreadGroup();
83 for (inti = 0; i < this.workerPoolSize; i++) {
84 tg.create(&workerThreadFunction);
85 }
86 87 if (this.verbose) writeln("Now accepting connections.");
88 while (serverSocket.isAlive()) {
89 SocketclientSocket = serverSocket.accept();
90 this.requestQueue.insertBack(clientSocket);
91 this.requestSemaphore.notify();
92 }
93 this.ready = false;
94 95 // Shutdown worker threads. We call notify() one last time to stop them waiting.96 for (inti = 0; i < this.workerPoolSize; i++) {
97 this.requestSemaphore.notify();
98 }
99 tg.joinAll();
100 }
101 102 /**
103 * Shuts down the server by closing the server socket, if possible. This
104 * will block until all pending requests have been fulfilled.
105 */106 publicvoidstop() {
107 if (verbose) writeln("Stopping the server.");
108 if (serverSocket !isnull) {
109 serverSocket.close();
110 }
111 }
112 113 /**
114 * Tells whether the server is ready to receive requests.
115 * Returns: Whether the server is ready to receive requests.
116 */117 publicboolisReady() {
118 returnready;
119 }
120 121 /**
122 * Sets the server's verbosity, which determines whether detailed log
123 * messages are printed during runtime.
124 * Params:
125 * verbose = Whether to enable verbose output.
126 * Returns: The server instance, for method chaining.
127 */128 publicHttpServersetVerbose(boolverbose) {
129 this.verbose = verbose;
130 returnthis;
131 }
132 133 /**
134 * Tells whether the server will give verbose output.
135 * Returns: Whether the server is set to give verbose output.
136 */137 publicboolisVerbose() {
138 returnthis.verbose;
139 }
140 141 /**
142 * Worker function that runs for all worker threads that process incoming
143 * requests. Workers will wait for the requestSemaphore to be notified so
144 * that they can process a request. The worker will stay alive as long as
145 * this server is set as ready.
146 */147 privatevoidworkerThreadFunction() {
148 MsgParser!MsgrequestParser = initParser!Msg();
149 ubyte[] receiveBuffer = newubyte[this.receiveBufferSize];
150 while (atomicLoad(this.ready)) {
151 this.requestSemaphore.wait();
152 if (!this.requestQueue.empty) {
153 SocketclientSocket = this.requestQueue.removeAny();
154 autoreceived = clientSocket.receive(receiveBuffer);
155 if (received == 0 || received == Socket.ERROR) {
156 continue; // Skip if we didn't receive valid data.157 }
158 stringdata = cast(string) receiveBuffer[0..received];
159 requestParser.msg.reset();
160 autorequest = parseRequest(requestParser, data);
161 request.server = this;
162 request.clientSocket = clientSocket;
163 if (verbose) writefln!"<- %s %s"(request.method, request.url);
164 try {
165 HttpResponseresponse;
166 response.status = 200;
167 response.statusText = "OK";
168 response.clientSocket = clientSocket;
169 this.handler.handle(request, response);
170 } catch (Exceptione) {
171 writefln!"An error occurred while handling a request: %s"(e.msg);
172 }
173 clientSocket.close();
174 }
175 }
176 }
177 }