Skip to content

Commit

Permalink
Merge pull request #61 from bgreni/header-refactor
Browse files Browse the repository at this point in the history
Refactor header parsing and data structure
  • Loading branch information
saviorand authored Sep 20, 2024
2 parents 313595b + 1483253 commit 805372e
Show file tree
Hide file tree
Showing 34 changed files with 1,139 additions and 2,326 deletions.
77 changes: 41 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,15 @@ Once you have a Mojo project set up locally,
@value
struct Printer(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri()
print("Request URI: ", to_string(uri.request_uri()))
var header = req.header
print("Request protocol: ", header.protocol_str())
print("Request method: ", to_string(header.method()))
print("Request Content-Type: ", to_string(header.content_type()))
var uri = req.uri
print("Request URI: ", to_string(uri.request_uri))
var header = req.headers
print("Request protocol: ", req.protocol)
print("Request method: ", req.method)
print(
"Request Content-Type: ", to_string(header[HeaderKey.CONTENT_TYPE])
)
var body = req.body_raw
print("Request Body: ", to_string(body))
Expand All @@ -103,9 +105,11 @@ Once you have a Mojo project set up locally,
```
6. Start a server listening on a port with your service like so.
```mojo
from lightbug_http import Welcome, SysServer
fn main() raises:
var server = SysServer()
var handler = Printer()
var handler = Welcome()
server.listen_and_serve("0.0.0.0:8080", handler)
```
Feel free to change the settings in `listen_and_serve()` to serve on a particular host and port.
Expand All @@ -123,15 +127,15 @@ from lightbug_http import *
struct ExampleRouter(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
var body = req.body_raw
var uri = req.uri()
var uri = req.uri
if uri.path() == "/":
if uri.path == "/":
print("I'm on the index path!")
if uri.path() == "/first":
if uri.path == "/first":
print("I'm on /first!")
elif uri.path() == "/second":
elif uri.path == "/second":
print("I'm on /second!")
elif uri.path() == "/echo":
elif uri.path == "/echo":
print(to_string(body))
return OK(body)
Expand All @@ -152,21 +156,21 @@ from lightbug_http import *
@value
struct Welcome(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri()
var uri = req.uri
if uri.path() == "/":
if uri.path == "/":
var html: Bytes
with open("static/lightbug_welcome.html", "r") as f:
html = f.read_bytes()
return OK(html, "text/html; charset=utf-8")
if uri.path() == "/logo.png":
if uri.path == "/logo.png":
var image: Bytes
with open("static/logo.png", "r") as f:
image = f.read_bytes()
return OK(image, "image/png")
return NotFound(uri.path())
return NotFound(uri.path)
```

### Using the client
Expand All @@ -178,33 +182,34 @@ from lightbug_http import *
from lightbug_http.sys.client import MojoClient
fn test_request(inout client: MojoClient) raises -> None:
var uri = URI("http://httpbin.org/status/404")
try:
uri.parse()
except e:
print("error parsing uri: " + e.__str__())
var uri = URI.parse_raises("http://httpbin.org/status/404")
var headers = Header("Host", "httpbin.org")
var request = HTTPRequest(uri)
var response = client.do(request)
var request = HTTPRequest(uri, headers)
var response = client.do(request^)
# print status code
print("Response:", response.header.status_code())
print("Response:", response.status_code)
# print parsed headers (only some are parsed for now)
print("Content-Type:", to_string(response.header.content_type()))
print("Content-Length", response.header.content_length())
print("Server:", to_string(response.header.server()))
print("Content-Type:", response.headers["Content-Type"])
print("Content-Length", response.headers["Content-Length"])
print("Server:", to_string(response.headers["Server"]))
print("Is connection set to connection-close? ", response.header.connection_close())
print(
"Is connection set to connection-close? ", response.connection_close()
)
# print body
print(to_string(response.get_body_bytes()))
print(to_string(response.body_raw))
fn main() raises -> None:
var client = MojoClient()
test_request(client)
fn main() -> None:
try:
var client = MojoClient()
test_request(client)
except e:
print(e)
```

Pure Mojo-based client is available by default. This client is also used internally for testing the server.
Expand Down
163 changes: 149 additions & 14 deletions bench.mojo
Original file line number Diff line number Diff line change
@@ -1,24 +1,152 @@
import benchmark
from lightbug_http.sys.server import SysServer
from lightbug_http.python.server import PythonServer
from lightbug_http.service import TechEmpowerRouter
from benchmark import *
from lightbug_http.io.bytes import bytes, Bytes
from lightbug_http.header import Headers, Header
from lightbug_http.utils import ByteReader, ByteWriter
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
from lightbug_http.uri import URI
from tests.utils import (
TestStruct,
FakeResponder,
new_fake_listener,
FakeServer,
getRequest,
)

alias headers = bytes(
"""GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n"""
)
alias body = bytes(String("I am the body of an HTTP request") * 5)
alias Request = bytes(
"""GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n"""
) + body
alias Response = bytes(
"HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:"
" application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:"
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n"
) + body


fn main():
run_benchmark()


fn run_benchmark():
try:
var server = SysServer(tcp_keep_alive=True)
var handler = TechEmpowerRouter()
server.listen_and_serve("0.0.0.0:8080", handler)
except e:
print("Error starting server: " + e.__str__())
return
var config = BenchConfig(warmup_iters=100)
config.verbose_timing = True
config.tabular_view = True
var m = Bench(config)
m.bench_function[lightbug_benchmark_header_encode](
BenchId("HeaderEncode")
)
m.bench_function[lightbug_benchmark_header_parse](
BenchId("HeaderParse")
)
m.bench_function[lightbug_benchmark_request_encode](
BenchId("RequestEncode")
)
m.bench_function[lightbug_benchmark_request_parse](
BenchId("RequestParse")
)
m.bench_function[lightbug_benchmark_response_encode](
BenchId("ResponseEncode")
)
m.bench_function[lightbug_benchmark_response_parse](
BenchId("ResponseParse")
)
m.dump_report()
except:
print("failed to start benchmark")


var headers_struct = Headers(
Header("Content-Type", "application/json"),
Header("Content-Length", "1234"),
Header("Connection", "close"),
Header("Date", "some-datetime"),
Header("SomeHeader", "SomeValue"),
)


@parameter
fn lightbug_benchmark_response_encode(inout b: Bencher):
@always_inline
@parameter
fn response_encode():
var res = HTTPResponse(body, headers=headers_struct)
_ = encode(res^)

b.iter[response_encode]()


@parameter
fn lightbug_benchmark_response_parse(inout b: Bencher):
@always_inline
@parameter
fn response_parse():
var res = Response
try:
_ = HTTPResponse.from_bytes(res^)
except:
pass

b.iter[response_parse]()


@parameter
fn lightbug_benchmark_request_parse(inout b: Bencher):
@always_inline
@parameter
fn request_parse():
var r = Request
try:
_ = HTTPRequest.from_bytes("127.0.0.1/path", 4096, r^)
except:
pass

b.iter[request_parse]()


@parameter
fn lightbug_benchmark_request_encode(inout b: Bencher):
@always_inline
@parameter
fn request_encode():
var req = HTTPRequest(
URI.parse("http://127.0.0.1:8080/some-path")[URI],
headers=headers_struct,
body=body,
)
_ = encode(req^)

b.iter[request_encode]()


@parameter
fn lightbug_benchmark_header_encode(inout b: Bencher):
@always_inline
@parameter
fn header_encode():
var b = ByteWriter()
var h = headers_struct
h.encode_to(b)

b.iter[header_encode]()


@parameter
fn lightbug_benchmark_header_parse(inout b: Bencher):
@always_inline
@parameter
fn header_parse():
try:
var b = headers
var header = Headers()
var reader = ByteReader(b^)
_ = header.parse_raw(reader)
except:
print("failed")

b.iter[header_parse]()


fn lightbug_benchmark_server():
Expand All @@ -28,19 +156,26 @@ fn lightbug_benchmark_server():


fn lightbug_benchmark_misc() -> None:
var direct_set_report = benchmark.run[init_test_and_set_a_direct](max_iters=1)
var direct_set_report = benchmark.run[init_test_and_set_a_direct](
max_iters=1
)

var recreating_set_report = benchmark.run[init_test_and_set_a_copy](max_iters=1)
var recreating_set_report = benchmark.run[init_test_and_set_a_copy](
max_iters=1
)

print("Direct set: ")
direct_set_report.print(benchmark.Unit.ms)
print("Recreating set: ")
recreating_set_report.print(benchmark.Unit.ms)


var GetRequest = HTTPRequest(URI.parse("http://127.0.0.1/path")[URI])


fn run_fake_server():
var handler = FakeResponder()
var listener = new_fake_listener(2, getRequest)
var listener = new_fake_listener(2, encode(GetRequest))
var server = FakeServer(listener, handler)
server.serve()

Expand Down
12 changes: 12 additions & 0 deletions bench_server.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from lightbug_http.sys.server import SysServer
from lightbug_http.service import TechEmpowerRouter


def main():
try:
var server = SysServer(tcp_keep_alive=True)
var handler = TechEmpowerRouter()
server.listen_and_serve("0.0.0.0:8080", handler)
except e:
print("Error starting server: " + e.__str__())
return
36 changes: 19 additions & 17 deletions client.mojo
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
from lightbug_http import *
from lightbug_http.sys.client import MojoClient

fn test_request(inout client: MojoClient) raises -> None:
var uri = URI("http://httpbin.org/status/404")
try:
uri.parse()
except e:
print("error parsing uri: " + e.__str__())

fn test_request(inout client: MojoClient) raises -> None:
var uri = URI.parse_raises("http://httpbin.org/status/404")
var headers = Header("Host", "httpbin.org")

var request = HTTPRequest(uri)
var response = client.do(request)
var request = HTTPRequest(uri, headers)
var response = client.do(request^)

# print status code
print("Response:", response.header.status_code())
print("Response:", response.status_code)

# print parsed headers (only some are parsed for now)
print("Content-Type:", to_string(response.header.content_type()))
print("Content-Length", response.header.content_length())
print("Server:", to_string(response.header.server()))
print("Content-Type:", response.headers["Content-Type"])
print("Content-Length", response.headers["Content-Length"])
print("Server:", to_string(response.headers["Server"]))

print("Is connection set to connection-close? ", response.header.connection_close())
print(
"Is connection set to connection-close? ", response.connection_close()
)

# print body
print(to_string(response.get_body_bytes()))
print(to_string(response.body_raw))


fn main() raises -> None:
var client = MojoClient()
test_request(client)
fn main() -> None:
try:
var client = MojoClient()
test_request(client)
except e:
print(e)
Loading

0 comments on commit 805372e

Please sign in to comment.