6 releases

0.0.5 Mar 13, 2023
0.0.4 Jan 28, 2023
0.0.3 Aug 4, 2022
0.0.1 Jul 28, 2022

#716 in Web programming

21 downloads per month
Used in rek

MIT license

94KB
2K SLoC

touché

Touché is a low level but fully featured HTTP 1.0/1.1 library.

It tries to mimic hyper, but with a synchronous API.

For now only the server API is implemented.

Hello world

use touche::{Response, Server, StatusCode};

fn main() -> std::io::Result<()> {
    Server::bind("0.0.0.0:4444").serve(|_req| {
        Response::builder()
            .status(StatusCode::OK)
            .body("Hello World")
    })
}

Features

  • HTTP Server (thread per connection model, backed by a thread pool)
  • Non buffered (streaming) requests and response bodies
  • HTTP/1.1 pipelining
  • TLS
  • Upgrade connections
  • Trailers headers
  • 100 continue expectation
  • Unix sockets servers

Comparison with Hyper

Touché shares a lot of similarities with Hyper:

  • "Low level"
  • Uses the http crate to represent HTTP related types
  • Allows fine-grained implementations of streaming HTTP bodies

But also has some key differences:

  • It is synchronous
  • Uses Vec<u8> to represent bytes instead of Bytes
  • Doesn't support HTTP 2 (and probably never will)

Other examples

Chunked response

use std::{error::Error, thread};

use touche::{Body, Response, Server, StatusCode};

fn main() -> std::io::Result<()> {
    Server::bind("0.0.0.0:4444").serve(|_req| {
        let (channel, body) = Body::channel();

        thread::spawn(move || {
            channel.send("chunk1").unwrap();
            channel.send("chunk2").unwrap();
            channel.send("chunk3").unwrap();
        });

        Response::builder()
            .status(StatusCode::OK)
            .body(body)
    })
}

Streaming files

use std::{fs, io};

use touche::{Body, Response, Server, StatusCode};

fn main() -> std::io::Result<()> {
    Server::bind("0.0.0.0:4444").serve(|_req| {
        let file = fs::File::open("./examples/file.rs")?;
        Ok::<_, io::Error>(
            Response::builder()
                .status(StatusCode::OK)
                .body(Body::try_from(file)?)
                .unwrap(),
        )
    })
}

Naive routing with pattern matching

use touche::{body::HttpBody, Body, Method, Request, Response, Server, StatusCode};

fn main() -> std::io::Result<()> {
    Server::builder()
        .bind("0.0.0.0:4444")
        .serve(|req: Request<Body>| {
            match (req.method(), req.uri().path()) {
                (_, "/") => Response::builder()
                    .status(StatusCode::OK)
                    .body(Body::from("Usage: curl -d hello localhost:4444/echo\n")),

                // Responds with the same payload
                (&Method::POST, "/echo") => Response::builder()
                    .status(StatusCode::OK)
                    .body(req.into_body()),

                // Responds with the reversed payload
                (&Method::POST, "/reverse") => {
                    let body = req.into_body().into_bytes().unwrap_or_default();

                    match std::str::from_utf8(&body) {
                        Ok(message) => Response::builder()
                            .status(StatusCode::OK)
                            .body(message.chars().rev().collect::<String>().into()),

                        Err(err) => Response::builder()
                            .status(StatusCode::BAD_REQUEST)
                            .body(err.to_string().into()),
                    }
                }

                _ => Response::builder()
                    .status(StatusCode::NOT_FOUND)
                    .body(Body::empty()),
            }
        })
}

Response upgrades

use std::io::{BufRead, BufReader, BufWriter, Write};

use touche::{header, upgrade::Upgrade, Body, Connection, Response, Server, StatusCode};

fn main() -> std::io::Result<()> {
    Server::bind("0.0.0.0:4444").serve(|_req| {
        Response::builder()
            .status(StatusCode::SWITCHING_PROTOCOLS)
            .header(header::UPGRADE, "line-protocol")
            .upgrade(|stream: Connection| {
                let reader = BufReader::new(stream.clone());
                let mut writer = BufWriter::new(stream);

                // Just a simple protocol that will echo every line sent
                for line in reader.lines() {
                    match line {
                        Ok(line) if line.as_str() == "quit" => break,
                        Ok(line) => {
                            writer.write_all(format!("{line}\n").as_bytes());
                            writer.flush();
                        }
                        Err(_err) => break,
                    };
                }
            })
            .body(Body::empty())
    })
}

You can find other examples in the examples directory.

Performance

While the primary focus is having a simple and readable implementation, the library shows some decent performance.

A simple benchmark of the hello_world.rs example gives the following result:

$ cat /proc/cpuinfo | grep name | uniq
model name      : AMD Ryzen 5 5600G with Radeon Graphics

$ wrk --latency -t6 -c 200 -d 10s http://localhost:4444
Running 10s test @ http://localhost:4444
  6 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   153.37us  391.20us  19.41ms   99.37%
    Req/Sec    76.11k    13.21k   89.14k    82.67%
  Latency Distribution
     50%  126.00us
     75%  160.00us
     90%  209.00us
     99%  360.00us
  4544074 requests in 10.01s, 225.35MB read
Requests/sec: 454157.11
Transfer/sec:     22.52MB

The result is on par with Hyper's hello world running on the same machine.

Disclaimer

This library is by no means a critique to Hyper or to async Rust. I really love both of them.

The main motivation I had to write this library was to be able to introduce Rust to my co-workers (which are mainly web developers). A synchronous library is way more beginner friendly than an async one, and by having an API that ressembles the "canonical" HTTP Rust library, people can learn Rust concepts in a easier way before adventuring through Hyper and async.

Dependencies

~1.9–4.5MB
~104K SLoC