1 /** 2 * Contains the core HTTP server components. 3 */ 4 module handy_httpd.server; 5 6 import std.stdio; 7 import std.socket; 8 import std.regex; 9 import std.container.dlist : DList; 10 import core.sync.semaphore : Semaphore; 11 import core.atomic : atomicLoad; 12 import core.thread.threadgroup : ThreadGroup; 13 14 import handy_httpd.request; 15 import handy_httpd.response; 16 import handy_httpd.handler; 17 import handy_httpd.parse_utils : parseRequest, Msg; 18 19 import httparsed : 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 class HttpServer { 27 private Address address; 28 private size_t receiveBufferSize; 29 private int connectionQueueSize; 30 private size_t workerPoolSize; 31 private bool verbose; 32 private HttpRequestHandler handler; 33 private shared bool ready = false; 34 private Socket serverSocket = null; 35 private Semaphore requestSemaphore; 36 private DList!Socket requestQueue; 37 38 this( 39 HttpRequestHandler handler = noOpHandler(), 40 string hostname = "127.0.0.1", 41 ushort port = 8080, 42 size_t receiveBufferSize = 8192, 43 int connectionQueueSize = 100, 44 bool verbose = false, 45 size_t workerPoolSize = 25 46 ) { 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 protected void configurePreBind(Socket socket) {} 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 public void start() { 73 serverSocket = new TcpSocket(); 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 = new Semaphore(); 82 ThreadGroup tg = new ThreadGroup(); 83 for (int i = 0; i < this.workerPoolSize; i++) { 84 tg.create(&workerThreadFunction); 85 } 86 87 if (this.verbose) writeln("Now accepting connections."); 88 while (serverSocket.isAlive()) { 89 Socket clientSocket = 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 (int i = 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 public void stop() { 107 if (verbose) writeln("Stopping the server."); 108 if (serverSocket !is null) { 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 public bool isReady() { 118 return ready; 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 public HttpServer setVerbose(bool verbose) { 129 this.verbose = verbose; 130 return this; 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 public bool isVerbose() { 138 return this.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 private void workerThreadFunction() { 148 MsgParser!Msg requestParser = initParser!Msg(); 149 ubyte[] receiveBuffer = new ubyte[this.receiveBufferSize]; 150 while (atomicLoad(this.ready)) { 151 this.requestSemaphore.wait(); 152 if (!this.requestQueue.empty) { 153 Socket clientSocket = this.requestQueue.removeAny(); 154 auto received = clientSocket.receive(receiveBuffer); 155 if (received == 0 || received == Socket.ERROR) { 156 continue; // Skip if we didn't receive valid data. 157 } 158 string data = cast(string) receiveBuffer[0..received]; 159 requestParser.msg.reset(); 160 auto request = parseRequest(requestParser, data); 161 request.server = this; 162 request.clientSocket = clientSocket; 163 if (verbose) writefln!"<- %s %s"(request.method, request.url); 164 try { 165 HttpResponse response; 166 response.status = 200; 167 response.statusText = "OK"; 168 response.clientSocket = clientSocket; 169 this.handler.handle(request, response); 170 } catch (Exception e) { 171 writefln!"An error occurred while handling a request: %s"(e.msg); 172 } 173 clientSocket.close(); 174 } 175 } 176 } 177 }