Skip to main content

file_server/
file_server.rs

1//! Disk **file I/O**: a thread-per-core static-file server. Each request names a
2//! file; the reply is its size then its bytes, read straight off disk through the
3//! ring and written to the socket.
4//!
5//! This is the example where `io_uring` earns its keep: the open, the positional
6//! `read_at`s and the socket writes are all submissions from one pinned thread,
7//! never a blocking-threadpool hop. The crate's [`Pool`] supplies the reusable
8//! 64 KiB transfer buffer each connection streams through.
9//!
10//! Protocol, one request per line on a persistent connection:
11//!   client:  `<filename>\n`
12//!   server:  `<size>\n` then `<size>` raw bytes   — or   `ERR <why>\n`
13//!
14//! Filenames are resolved inside `FILE_DIR` and may not contain a path separator,
15//! so a request cannot escape that directory.
16//!
17//! ```text
18//! FILE_DIR=/tmp/bench-files \
19//! cargo run --release --example file_server -- [ADDR] [CAPACITY]
20//!   ADDR       bind address               default 0.0.0.0:7000
21//!   CAPACITY   buffers (connections)      default 1024
22//! ```
23//!
24//! Drive it with `examples/load_file.rs`, which creates the test files first.
25
26use std::{io, net::SocketAddr, path::PathBuf, sync::LazyLock, thread};
27
28use compio::{
29    BufResult,
30    fs::File,
31    io::{AsyncRead, AsyncReadAt, AsyncWriteExt},
32    net::{TcpListener, TcpStream},
33    runtime::Runtime,
34};
35use compio_pool::{LocalPool, ManageConnection, Pool, bind_reuseport, cpu};
36
37/// The directory requested files are resolved inside.
38static BASE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
39    PathBuf::from(std::env::var("FILE_DIR").unwrap_or_else(|_| "/tmp/bench-files".into()))
40});
41
42/// Hands out reusable 64 KiB transfer buffers, one per connection in flight.
43struct Buffers;
44
45impl ManageConnection for Buffers {
46    type Connection = Vec<u8>;
47    type Error = io::Error;
48
49    async fn connect(&self) -> io::Result<Vec<u8>> {
50        Ok(Vec::with_capacity(64 * 1024))
51    }
52
53    async fn is_valid(&self, _buf: &mut Vec<u8>) -> io::Result<()> {
54        Ok(())
55    }
56
57    fn has_broken(&self, _buf: &mut Vec<u8>) -> bool {
58        false
59    }
60}
61
62fn main() -> io::Result<()> {
63    let addr: SocketAddr = std::env::args()
64        .nth(1)
65        .unwrap_or_else(|| "0.0.0.0:7000".into())
66        .parse()
67        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad address: {e}")))?;
68    let capacity: u32 = std::env::args()
69        .nth(2)
70        .and_then(|s| s.parse().ok())
71        .unwrap_or(1024);
72
73    let pool = Pool::builder().max_size(capacity).build(Buffers);
74
75    let cores = cpu::cores();
76    if cores.is_empty() {
77        return Err(io::Error::new(io::ErrorKind::Unsupported, "no cores to run on"));
78    }
79    println!(
80        "file_server on {addr} serving {}: {} workers, capacity {capacity}/worker",
81        BASE_DIR.display(),
82        cores.len()
83    );
84
85    let mut handles = Vec::new();
86    for (index, core) in cores.into_iter().enumerate() {
87        let pool = pool.clone();
88        let handle = thread::Builder::new()
89            .name(format!("worker/{index}"))
90            .spawn(move || worker(core, addr, pool))?;
91        handles.push(handle);
92    }
93    for handle in handles {
94        let _ = handle.join();
95    }
96    Ok(())
97}
98
99fn worker(core: cpu::CoreId, addr: SocketAddr, pool: Pool<Buffers>) {
100    cpu::pin_current(core);
101
102    let runtime = Runtime::builder().build().expect("build compio runtime");
103    runtime.block_on(async move {
104        let local = pool.local();
105
106        let std_listener = bind_reuseport(addr, 1024, None).expect("bind SO_REUSEPORT");
107        let listener = TcpListener::from_std(std_listener).expect("wrap listener in ring");
108
109        loop {
110            let Ok((stream, _peer)) = listener.accept().await else {
111                continue;
112            };
113            // The reply is a size header followed by the body — two writes. Without
114            // TCP_NODELAY, Nagle holds the body waiting for the header's ACK, which
115            // the peer delays ~40 ms, so disable it on every accepted connection.
116            let _ = stream.set_nodelay(true);
117            compio::runtime::spawn(serve(local.clone(), stream)).detach();
118        }
119    });
120}
121
122/// Serve requests on one connection until it closes. Opening the file is per
123/// request; the large transfer buffer is pooled and reused across every file.
124async fn serve(local: LocalPool<Buffers>, mut stream: TcpStream) {
125    let mut req = Vec::with_capacity(1024);
126    loop {
127        req.clear();
128        let BufResult(read, b) = stream.read(req).await;
129        req = b;
130        match read {
131            Ok(0) | Err(_) => return,
132            Ok(_) => {}
133        }
134
135        // Parse the filename and keep it inside BASE_DIR.
136        let name = match std::str::from_utf8(&req) {
137            Ok(s) => s.trim_end_matches(['\r', '\n']).to_string(),
138            Err(_) => {
139                if reply_err(&mut stream, "utf8").await.is_err() {
140                    return;
141                }
142                continue;
143            }
144        };
145        if name.is_empty() || name.contains('/') || name.contains("..") {
146            if reply_err(&mut stream, "bad name").await.is_err() {
147                return;
148            }
149            continue;
150        }
151
152        let path = BASE_DIR.join(&name);
153        let file = match File::open(&path).await {
154            Ok(f) => f,
155            Err(e) => {
156                if reply_err(&mut stream, &e.to_string()).await.is_err() {
157                    return;
158                }
159                continue;
160            }
161        };
162        let size = match file.metadata().await {
163            Ok(m) => m.len(),
164            Err(e) => {
165                if reply_err(&mut stream, &e.to_string()).await.is_err() {
166                    return;
167                }
168                continue;
169            }
170        };
171
172        // Size header, then the body streamed through the pooled buffer.
173        let BufResult(w, _) = stream.write_all(format!("{size}\n").into_bytes()).await;
174        if w.is_err() {
175            return;
176        }
177
178        let mut lease = match local.get().await {
179            Ok(lease) => lease,
180            Err(_) => return,
181        };
182        let mut data = std::mem::take(&mut *lease);
183        let mut offset = 0u64;
184        while offset < size {
185            data.clear();
186            let BufResult(r, d) = file.read_at(data, offset).await;
187            data = d;
188            let n = match r {
189                Ok(0) => break,
190                Ok(n) => n,
191                Err(_) => {
192                    *lease = data;
193                    return;
194                }
195            };
196            offset += n as u64;
197            let BufResult(w, d) = stream.write_all(data).await;
198            data = d;
199            if w.is_err() {
200                *lease = data;
201                return;
202            }
203        }
204        *lease = data; // return the transfer buffer to the pool
205    }
206}
207
208/// Send a one-line `ERR <why>` response. Returns `Err` if the socket is gone.
209async fn reply_err(stream: &mut TcpStream, why: &str) -> io::Result<()> {
210    let BufResult(w, _) = stream.write_all(format!("ERR {why}\n").into_bytes()).await;
211    w
212}