Compare commits
5 Commits
Author | SHA1 | Date |
---|---|---|
|
7ff80c8a9f | |
|
a0d1274bbe | |
|
01d48e9537 | |
|
5e79fba1b4 | |
|
febce5eb8d |
6
LICENSE
6
LICENSE
|
@ -1 +1,5 @@
|
||||||
Handy-Http by Andrew Lalis is marked with CC0 1.0 Universal. To view a copy of this license, visit https://creativecommons.org/publicdomain/zero/1.0/
|
Copyright 2025 Andrew Lalis
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -26,7 +26,7 @@ class MyHandler : HttpRequestHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
HttpTransport tp = new TaskPoolHttp1Transport(new MyHandler(), 8080);
|
HttpTransport tp = new TaskPoolHttp1Transport(new MyHandler());
|
||||||
tp.start();
|
tp.start();
|
||||||
}
|
}
|
||||||
```
|
```
|
|
@ -7,32 +7,79 @@ import handy_http_transport.http1.transport;
|
||||||
import handy_http_primitives;
|
import handy_http_primitives;
|
||||||
import slf4d;
|
import slf4d;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options to provide when creating a new Http1Transport
|
||||||
|
* instance.
|
||||||
|
*/
|
||||||
|
struct Http1TransportConfig {
|
||||||
|
/// The host address to bind to.
|
||||||
|
string host;
|
||||||
|
/// The port to bind to.
|
||||||
|
ushort port;
|
||||||
|
/// The number of workers to use in the task pool.
|
||||||
|
size_t workerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the default configuration options if none are provided. They are:
|
||||||
|
* * Host address 127.0.0.1
|
||||||
|
* * Port 8080
|
||||||
|
* * Worker count of 5.
|
||||||
|
* Returns: The default configuration.
|
||||||
|
*/
|
||||||
|
Http1TransportConfig defaultConfig() {
|
||||||
|
return Http1TransportConfig(
|
||||||
|
"127.0.0.1",
|
||||||
|
8080,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of Http1Transport which uses D's standard library
|
* An implementation of Http1Transport which uses D's standard library
|
||||||
* parallelization, where each incoming client request is turned into a task
|
* parallelization, where each incoming client request is turned into a task
|
||||||
* and submitted to the standard task pool.
|
* and submitted to the standard task pool.
|
||||||
*/
|
*/
|
||||||
class TaskPoolHttp1Transport : Http1Transport {
|
class TaskPoolHttp1Transport : Http1Transport {
|
||||||
this(HttpRequestHandler requestHandler, ushort port = 8080) {
|
private TaskPool httpTaskPool;
|
||||||
super(requestHandler, port);
|
private immutable Http1TransportConfig config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new transport instance using a std.parallelism TaskPool for
|
||||||
|
* handling requests.
|
||||||
|
* Params:
|
||||||
|
* requestHandler = The handler to call for each incoming request.
|
||||||
|
* workerCount = The number of workers to use in the task pool.
|
||||||
|
* port = The port.
|
||||||
|
*/
|
||||||
|
this(HttpRequestHandler requestHandler, in Http1TransportConfig config = defaultConfig()) {
|
||||||
|
super(requestHandler, config.port);
|
||||||
|
this.config = config;
|
||||||
|
this.httpTaskPool = new TaskPool(config.workerCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
override void runServer() {
|
override void runServer() {
|
||||||
Socket serverSocket = new TcpSocket();
|
Socket serverSocket = new TcpSocket();
|
||||||
serverSocket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, 1);
|
serverSocket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, 1);
|
||||||
serverSocket.bind(new InternetAddress("127.0.0.1", port));
|
serverSocket.bind(parseAddress(config.host, config.port));
|
||||||
|
debugF!"Bound the server socket to %s"(serverSocket.localAddress);
|
||||||
serverSocket.listen(1024);
|
serverSocket.listen(1024);
|
||||||
|
debug_("Server is now listening.");
|
||||||
|
|
||||||
while (super.isRunning) {
|
while (super.isRunning) {
|
||||||
try {
|
try {
|
||||||
|
trace("Waiting to accept a new socket.");
|
||||||
Socket clientSocket = serverSocket.accept();
|
Socket clientSocket = serverSocket.accept();
|
||||||
|
trace("Accepted a new socket.");
|
||||||
auto t = task!handleClient(clientSocket, requestHandler);
|
auto t = task!handleClient(clientSocket, requestHandler);
|
||||||
taskPool().put(t);
|
this.httpTaskPool.put(t);
|
||||||
|
trace("Added handleClient() task to the task pool.");
|
||||||
} catch (SocketAcceptException e) {
|
} catch (SocketAcceptException e) {
|
||||||
warn("Failed to accept socket connection.", e);
|
warn("Failed to accept socket connection.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
serverSocket.close();
|
serverSocket.close();
|
||||||
|
this.httpTaskPool.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
override void stop() {
|
override void stop() {
|
||||||
|
|
|
@ -93,7 +93,10 @@ version(unittest) {
|
||||||
string statusLine = headerLines[0];
|
string statusLine = headerLines[0];
|
||||||
string[] statusLineParts = statusLine.split(" ");
|
string[] statusLineParts = statusLine.split(" ");
|
||||||
assert(statusLineParts[0] == "HTTP/1.1");
|
assert(statusLineParts[0] == "HTTP/1.1");
|
||||||
assert(statusLineParts[1] == "200");
|
assert(
|
||||||
|
statusLineParts[1] == "200",
|
||||||
|
format!"Expected status line's HTTP code to be 200, but it was \"%s\"."(statusLineParts[1])
|
||||||
|
);
|
||||||
assert(statusLineParts[2] == "OK");
|
assert(statusLineParts[2] == "OK");
|
||||||
|
|
||||||
info("Testing is complete. Stopping the server.");
|
info("Testing is complete. Stopping the server.");
|
||||||
|
@ -142,6 +145,7 @@ void handleClient(Socket clientSocket, HttpRequestHandler requestHandler) {
|
||||||
debugF!"%s %s -> %d %s"(request.method, request.url, response.status.code, response.status.text);
|
debugF!"%s %s -> %d %s"(request.method, request.url, response.status.code, response.status.text);
|
||||||
// If the response's headers aren't flushed yet, write them now.
|
// If the response's headers aren't flushed yet, write them now.
|
||||||
if (!responseOutputStream.areHeadersFlushed()) {
|
if (!responseOutputStream.areHeadersFlushed()) {
|
||||||
|
trace("Flushing response headers because they weren't flushed by the request handler.");
|
||||||
auto writeResult = responseOutputStream.writeHeaders();
|
auto writeResult = responseOutputStream.writeHeaders();
|
||||||
if (writeResult.hasError) {
|
if (writeResult.hasError) {
|
||||||
errorF!"Failed to write response headers: %s"(writeResult.error.message);
|
errorF!"Failed to write response headers: %s"(writeResult.error.message);
|
||||||
|
|
|
@ -3,6 +3,7 @@ module handy_http_transport.response_output_stream;
|
||||||
import handy_http_transport.helpers : writeUIntToBuffer;
|
import handy_http_transport.helpers : writeUIntToBuffer;
|
||||||
import handy_http_primitives : ServerHttpResponse;
|
import handy_http_primitives : ServerHttpResponse;
|
||||||
import streams;
|
import streams;
|
||||||
|
import slf4d;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around a byte output stream that's used for writing HTTP response
|
* A wrapper around a byte output stream that's used for writing HTTP response
|
||||||
|
@ -61,6 +62,7 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) {
|
||||||
if (headersFlushed) {
|
if (headersFlushed) {
|
||||||
return StreamResult(0); // No need to write again.
|
return StreamResult(0); // No need to write again.
|
||||||
}
|
}
|
||||||
|
debug_("Flushing HTTP status and headers to the output stream.");
|
||||||
headersFlushed = true;
|
headersFlushed = true;
|
||||||
size_t idx = 0;
|
size_t idx = 0;
|
||||||
char[6] statusCodeBuffer; // Normal HTTP codes are 3 digits, but this leaves room for extensions.
|
char[6] statusCodeBuffer; // Normal HTTP codes are 3 digits, but this leaves room for extensions.
|
||||||
|
@ -69,9 +71,11 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) {
|
||||||
StreamResult r = outputStream.writeToStream(cast(ubyte[]) "HTTP/1.1 ");
|
StreamResult r = outputStream.writeToStream(cast(ubyte[]) "HTTP/1.1 ");
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
size_t writeCount = r.count;
|
size_t writeCount = r.count;
|
||||||
|
traceF!"Wrote HTTP version. Bytes written: %d."(writeCount);
|
||||||
r = outputStream.writeToStream(cast(ubyte[]) statusCodeBuffer[0..idx]);
|
r = outputStream.writeToStream(cast(ubyte[]) statusCodeBuffer[0..idx]);
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
writeCount += r.count;
|
writeCount += r.count;
|
||||||
|
traceF!"Wrote status code. Bytes written: %d."(writeCount);
|
||||||
r = outputStream.writeToStream([' ']);
|
r = outputStream.writeToStream([' ']);
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
writeCount += r.count;
|
writeCount += r.count;
|
||||||
|
@ -81,9 +85,11 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) {
|
||||||
r = outputStream.writeToStream(['\r', '\n']);
|
r = outputStream.writeToStream(['\r', '\n']);
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
writeCount += r.count;
|
writeCount += r.count;
|
||||||
|
traceF!"Wrote HTTP status line. Bytes written: %d."(writeCount);
|
||||||
|
|
||||||
foreach (headerName; response.headers.keys) {
|
foreach (headerName; response.headers.keys) {
|
||||||
// Write the header name.
|
// Write the header name.
|
||||||
|
traceF!"Writing header name: %s"(headerName);
|
||||||
r = outputStream.writeToStream(cast(ubyte[]) headerName);
|
r = outputStream.writeToStream(cast(ubyte[]) headerName);
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
writeCount += r.count;
|
writeCount += r.count;
|
||||||
|
@ -105,6 +111,7 @@ struct HttpResponseOutputStream(S) if (isByteOutputStream!S) {
|
||||||
r = outputStream.writeToStream(['\r', '\n']);
|
r = outputStream.writeToStream(['\r', '\n']);
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
writeCount += r.count;
|
writeCount += r.count;
|
||||||
|
traceF!"Wrote header %s: %s"(headerName, headerValues);
|
||||||
}
|
}
|
||||||
r = outputStream.writeToStream(['\r', '\n']); // Trailing CLRF before the body.
|
r = outputStream.writeToStream(['\r', '\n']); // Trailing CLRF before the body.
|
||||||
if (r.hasError) return r;
|
if (r.hasError) return r;
|
||||||
|
|
Loading…
Reference in New Issue