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
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