Skip to main content

task_echo/
task_echo.rs

1//! The thread-free counterpart of `echo.rs`: a TCP echo server on **one** compio
2//! runtime, where concurrency is compio *tasks*, not OS threads.
3//!
4//! `echo.rs` spawns a pinned OS thread per core and gives each its own runtime,
5//! listener and [`LocalPool`]. This example does none of that: it builds a single
6//! [`Runtime`], binds one ordinary [`TcpListener`], and for each accepted
7//! connection calls [`compio::runtime::spawn`] — a task on this same thread and
8//! this same `io_uring` ring. There is no [`std::thread::spawn`], no core pinning
9//! and no `SO_REUSEPORT`; thousands of connections are served concurrently by
10//! thousands of cheap tasks cooperatively scheduled on one ring.
11//!
12//! The [`Pool`] is used exactly as everywhere else — one [`LocalPool`] for the
13//! thread, a buffer leased per connection — which is the point: the pool's model
14//! is *per runtime*, so a single-runtime server is just the one-thread case of
15//! it, with nothing extra to set up.
16//!
17//! ```text
18//! cargo run --release --example task_echo -- [ADDR] [CAPACITY]
19//!   ADDR       bind address               default 127.0.0.1:7000
20//!   CAPACITY   max connections (buffers)  default 1024
21//!
22//! # talk to it:
23//! echo -n hello | nc -w1 127.0.0.1 7000
24//! ```
25
26use std::{io, net::SocketAddr};
27
28use compio::{
29    BufResult,
30    io::{AsyncRead, AsyncWriteExt},
31    net::{TcpListener, TcpStream},
32    runtime::Runtime,
33};
34use compio_pool::{LocalPool, ManageConnection, Pool};
35
36/// Hands out reusable 16 KiB read buffers, one per in-flight connection.
37struct Buffers;
38
39impl ManageConnection for Buffers {
40    type Connection = Vec<u8>;
41    type Error = io::Error;
42
43    async fn connect(&self) -> io::Result<Vec<u8>> {
44        Ok(Vec::with_capacity(16 * 1024))
45    }
46
47    async fn is_valid(&self, _buf: &mut Vec<u8>) -> io::Result<()> {
48        Ok(())
49    }
50
51    fn has_broken(&self, _buf: &mut Vec<u8>) -> bool {
52        false
53    }
54}
55
56fn main() -> io::Result<()> {
57    let addr: SocketAddr = std::env::args()
58        .nth(1)
59        .unwrap_or_else(|| "127.0.0.1:7000".into())
60        .parse()
61        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, format!("bad address: {e}")))?;
62    let capacity: u32 = std::env::args()
63        .nth(2)
64        .and_then(|s| s.parse().ok())
65        .unwrap_or(1024);
66
67    let pool = Pool::builder().max_size(capacity).build(Buffers);
68
69    // One runtime, one ring. Everything below runs on this single thread.
70    let runtime = Runtime::builder().build().expect("build compio runtime");
71    runtime.block_on(async move {
72        // This runtime's pool. `!Send`, so it and its buffers stay on this thread.
73        let local = pool.local();
74
75        let listener = TcpListener::bind(addr).await?;
76        println!("task_echo on {addr}: 1 runtime, capacity {capacity}");
77
78        // Accept forever; hand each connection to its own task on this same ring.
79        loop {
80            let Ok((stream, _peer)) = listener.accept().await else {
81                continue;
82            };
83            let _ = stream.set_nodelay(true);
84            compio::runtime::spawn(serve(local.clone(), stream)).detach();
85        }
86    })
87}
88
89/// Borrow a buffer from this runtime's pool for the life of the connection and
90/// echo until the peer closes. Dropping the lease returns the buffer.
91async fn serve(local: LocalPool<Buffers>, mut stream: TcpStream) {
92    let mut lease = match local.get().await {
93        Ok(lease) => lease,
94        Err(_) => return,
95    };
96    loop {
97        let mut buf = std::mem::take(&mut *lease);
98        buf.clear();
99        let BufResult(read, buf) = stream.read(buf).await;
100        match read {
101            Ok(0) | Err(_) => {
102                *lease = buf;
103                return;
104            }
105            Ok(_) => {}
106        }
107        let BufResult(written, buf) = stream.write_all(buf).await;
108        *lease = buf;
109        if written.is_err() {
110            return;
111        }
112    }
113}