Odin HTTP

repository·main·Indexed 19 days ago

https://github.com/laytan/odin-http

A high-performance HTTP/1.1 implementation written in Odin, serving as a proof of concept for Odin's core collection. It includes a server with Lua-style pattern routing, an HTTP client requiring OpenSSL for HTTPS, and the nbio package, which provides a non-blocking IO abstraction layer using IOCP (Windows), io_uring (Linux), and KQueue (Darwin).

Tokens
2.8K
Snippets
5
Records
9
Agent score
65%

What's inside odin-http

  1. What is package nbio?

    main

    The nbio package provides a non-blocking IO abstraction layer using an event loop pattern. It abstracts away platform-specific high-performance IO APIs to provide a unified interface for asynchronous operations.

    Supported platform backends:

    • Windows: IOCP (IO Completion Ports)
    • Linux: io_uring
    • Darwin: KQueue
  2. Dependencies and Compatibility

    main

    Dependencies

    • HTTPS Support: The client package requires OpenSSL to make HTTPS requests.
    • Linux: Most distributions provide OpenSSL (usually libssl3).
    • Windows: The repository includes copies of these libraries for ease of use.

    Compatibility

    • Status: Beta software / Proof of Concept. Not intended for production use.
    • API Stability: The API is subject to frequent changes; backwards compatibility is not guaranteed.
    • Tested Platforms:
      • Ubuntu Linux
      • MacOS (M1 and Intel)
      • Windows 64-bit
  3. Benchmark Odin-HTTP performance with Empty OK All

    main

    To measure the raw IO rate of Odin-HTTP, you can perform an 'Empty OK All' benchmark where the server responds to all requests on port :8080 with a 200 OK status. This test measures how many requests per second the server can handle under load.

    To achieve optimal performance during development/benchmarking, build the project with optimizations that disable assertions and bounds checking.

    # Build Odin-HTTP with performance optimizations
    odin build . -o:speed -disable-assert -no-bounds-check
  4. Implement an HTTP Client

    main

    The client package provides tools for making HTTP requests.

    Simple GET requests: Use client.get(url) which returns a client.Response and an error. Remember to call client.response_destroy(&res) to prevent leaks and client.body_destroy(body, allocation) after retrieving the response body.

    Complex POST requests (e.g., JSON):

    1. Initialize a request with client.request_init(&req, .Post).
    2. Attach a JSON body using client.with_json(&req, data_struct).
    3. Execute the request with client.request(&req, url).
    4. Clean up using client.request_destroy(&req) and client.response_destroy(&res).
    import "../../client"
    
    // GET example
    res, err := client.get("https://www.google.com/")
    if err == nil {
    	defer client.response_destroy(&res)
    	body, alloc, berr := client.response_body(&res)
    	if berr == nil {
    		defer client.body_destroy(body, alloc)
    		fmt.println(body)
    	}
    }
    
    // POST JSON example
    req: client.Request
    client.request_init(&req, .Post)
    defer client.request_destroy(&req)
    
    client.with_json(&req, my_struct)
    res, err := client.request(&req, "https://example.com")
  5. Implement an HTTP Server

    main

    To create an HTTP server, use the http.Server type and a http.Router for request routing. Routes are matched in the order they are registered using Lua-style patterns (similar to regex but more limited).

    Key steps:

    1. Initialize a http.Server and a http.Router.
    2. Register routes using http.route_get or http.route_post with an http.handler.
    3. Use http.handler(proc(req: ^http.Request, res: ^http.Response)) to define request handlers.
    4. Use http.listen_and_serve to start the server.
    5. Use http.server_shutdown_on_interrupt(&s) to enable graceful shutdown on SIGINT.

    Route parameters captured via patterns (e.g., (%w+)) are accessible via req.url_params.

    import "core:fmt"
    import "core:net"
    import "http"
    
    main :: proc() {
    	s: http.Server
    	http.server_shutdown_on_interrupt(&s)
    
    	router: http.Router
    	http.router_init(&router)
    	defer http.router_destroy(&router)
    
    	// Route with parameters
    	http.route_get(&router, "/users/(%w+)/comments/(%d+)", http.handler(proc(req: ^http.Request, res: ^http.Response) {
    		http.respond_plain(res, fmt.tprintf("user %s, comment: %s", req.url_params[0], req.url_params[1]))
    	}))
    
    	routed := http.router_handler(&router)
    	http.listen_and_serve(&s, routed, net.Endpoint{address = net.IP4_Loopback, port = 6969})
    }
  6. Implement a simple TCP Echo Server with nbio

    main

    To build a non-blocking server, you must initialize an nbio.IO instance, open a listening socket, and run a loop calling nbio.tick. The server operates via callbacks: nbio.accept handles new connections, nbio.recv handles incoming data, and nbio.send_all handles outgoing data.

    Key lifecycle steps:

    1. Initialize: Call nbio.init(&io).
    2. Setup Socket: Use nbio.open_and_listen_tcp to create a listening socket.
    3. Register Accept: Call nbio.accept to register a callback for new connections.
    4. Event Loop: Run a for loop that continuously calls nbio.tick(&io) until an error occurs.
    5. Cleanup: Call nbio.destroy(&io).
    /*
    This example shows a simple TCP server that echos back anything it receives.
    
    Better error handling and closing/freeing connections are left for the reader.
    */
    package main
    
    import "core:fmt"
    import "core:net"
    import "core:os"
    
    import nbio "nbio/poly"
    
    Echo_Server :: struct {
    	nio:          nbio.IO,
    	sock:        net.TCP_Socket,
    	connections: [dynamic]^Echo_Connection,
    }
    
    Echo_Connection :: struct {
    	server:  ^Echo_Server,
    	sock:    net.TCP_Socket,
    	buf:     [50]byte,
    }
    
    main :: proc() {
    	server: Echo_Server
    	defer delete(server.connections)
    
    	nbio.init(&server.io)
    	defer nbio.destroy(&server.io)
    
    	sock, err := nbio.open_and_listen_tcp(&server.io, {net.IP4_Loopback, 8080})
    	fmt.assertf(err == nil, "Error opening and listening on localhost:8080: %v", err)
    	server.sock = sock
    
    	nbio.accept(&server.io, sock, &server, echo_on_accept)
    
    	// Start the event loop.
    	errno: os.Errno
    	for errno == os.ERROR_NONE {
    		errno = nbio.tick(&server.io)
    	}
    
    	fmt.assertf(errno == os.ERROR_NONE, "Server stopped with error code: %v", errno)
    }
    
    echo_on_accept :: proc(server: ^Echo_Server, client: net.TCP_Socket, source: net.Endpoint, err: net.Network_Error) {
    	fmt.assertf(err == nil, "Error accepting a connection: %v", err)
    
    	// Register a new accept for the next client.
    	nbio.accept(&server.io, server.sock, server, echo_on_accept)
    
    	c := new(Echo_Connection)
    	c.server = server
    	c.sock   = client
    	append(&server.connections, c)
    
    	nbio.recv(&server.io, client, c.buf[:], c, echo_on_recv)
    }
    
    echo_on_recv :: proc(c: ^Echo_Connection, received: int, _: Maybe(net.Endpoint), err: net.Network_Error) {
    	fmt.assertf(err == nil, "Error receiving from client: %v", err)
    
    	nbio.send_all(&c.server.io, c.sock, c.buf[:received], c, echo_on_sent)
    }
    
    echo_on_sent :: proc(c: ^Echo_Connection, sent: int, err: net.Network_Error) {
    	// Accept the next message, to then ultimately echo back again.
    	nbio.recv(&c.server.io, c.sock, c.buf[:], c, echo_on_recv)
    }
  7. Server Response Methods

    main

    When writing HTTP handlers, use these methods to send responses back to the client:

    • http.respond_plain(res, text): Sends a plain text response.
    • http.respond_json(res, data): Sends a JSON response (e.g., using the request line as the body).
    • http.respond_file(res, path): Serves a specific file.
    • http.respond_dir(res, prefix, root, param): Serves a directory based on a URL parameter.
    • http.respond(res, status): Sends a specific HTTP status code.
    • http.body(req, len, res, callback): For handling request bodies asynchronously or via a callback.