11个版本
新增 0.0.10 | 2024年8月23日 |
---|---|
0.0.9 | 2023年8月27日 |
0.0.7 | 2023年7月14日 |
0.0.6 | 2023年6月30日 |
0.0.1 | 2022年7月28日 |
#305 in 网络编程
58 每月下载量
用于 2 crates
98KB
2K SLoC
touché
Touché是一个低级但功能齐全的HTTP 1.0/1.1库。
它试图模仿 hyper,但提供同步API。
目前只实现了服务器API。
你好,世界
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")
})
}
特性
- HTTP服务器(每个连接一个线程模型,由线程池支持)
- 非缓冲(流式)请求和响应体
- HTTP/1.1管线化
- TLS
- 升级连接
- 跟踪头
- 100继续期望
- Unix套接字服务器
与Hyper的比较
Touché与Hyper有很多相似之处
- "低级"
- 使用 http crate 表示与HTTP相关的类型
- 允许对流式HTTP体进行细致的实现
但也有一些关键的不同之处
- 它是同步的
- 使用
Vec<u8>
来表示字节,而不是 Bytes - 不支持HTTP 2(可能永远不会支持)
使用非阻塞IO处理持久连接
使用线程-per-连接的Web服务器对于像WebSocket或事件流这样的持久连接来说是非常糟糕的。这主要是因为线程被锁定到连接上,直到它被关闭。
解决这个问题的方法之一是使用非阻塞IO来处理这样的连接。通过这样做,服务器线程可以用于其他连接。
以下示例演示了一个单线程的touché服务器,该服务器处理将WebSocket升级到Tokio运行时的操作。
use std::{error::Error, sync::Arc};
use futures::{stream::StreamExt, SinkExt};
use tokio::{net::TcpStream, runtime};
use tokio_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
use touche::{upgrade::Upgrade, Body, Connection, Request, Server};
fn main() -> std::io::Result<()> {
let runtime = Arc::new(runtime::Builder::new_multi_thread().enable_all().build()?);
Server::builder()
.max_threads(1)
.bind("0.0.0.0:4444")
.serve(move |req: Request<Body>| {
let runtime = runtime.clone();
let res = tungstenite::handshake::server::create_response(&req.map(|_| ()))?;
Ok::<_, Box<dyn Error + Send + Sync>>(res.upgrade(move |stream: Connection| {
let stream = stream.downcast::<std::net::TcpStream>().unwrap();
stream.set_nonblocking(true).unwrap();
runtime.spawn(async move {
let stream = TcpStream::from_std(stream).unwrap();
let mut ws = WebSocketStream::from_raw_socket(stream, Role::Server, None).await;
while let Some(Ok(msg)) = ws.next().await {
if msg.is_text() && ws.send(msg).await.is_err() {
break;
}
}
});
}))
})
}
其他示例
分块响应
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)
})
}
流式传输文件
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(),
)
})
}
使用模式匹配的简单路由
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()),
}
})
}
响应升级
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())
})
}
您可以在示例目录中找到其他示例。
性能
虽然主要关注的是拥有简单易读的实现,但该库显示了相当不错的性能。
对hello_world.rs示例的简单基准测试给出了以下结果
$ cat /proc/cpuinfo | grep name | uniq
model name : AMD Ryzen 5 5600G with Radeon Graphics
$ wrk --latency -t6 -c 200 -d 10s https://127.0.0.1:4444
Running 10s test @ https://127.0.0.1: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
结果是与在同一台机器上运行的Hyper的hello world相当。
免责声明
这个库绝对不是对Hyper或async Rust的批评。我真的很喜欢它们。
我写这个库的主要动机是能够把我同事(他们主要是前端开发者)介绍给Rust。与异步库相比,同步库对初学者来说要友好得多,而且通过提供一个类似于“标准”HTTP Rust库的API,人们可以在冒险尝试Hyper和异步之前以更简单的方式学习Rust概念。
依赖项
~2–12MB
~149K SLoC