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 网络编程

Download history 1/week @ 2024-05-24 2/week @ 2024-06-28 29/week @ 2024-07-05 52/week @ 2024-07-26 6/week @ 2024-08-02

58 每月下载量
用于 2 crates

MIT 协议

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