Skip to content

Commit 5728c7e

Browse files
black-binaryclaude
andcommitted
docs: refresh README and rustdoc to match the current Tokio API
The README still showed an async-std/Mux::new flow that hasn't existed since the Tokio rewrite, and lib.rs's quickstart had broken markdown (##-headings inside a fenced code block) and undefined identifiers. Rewrite both around the real API (MuxBuilder → connector/acceptor/ worker triple, with examples that compile against the current crate) and add a configuration section covering keep-alive, idle timeout, and queue knobs. Add a CI badge to the README pointing at the workflow that just landed. Sprinkle one-line rustdoc on the public types and methods that had none — MuxBuilder + every with_* method, MuxConnector::{connect, close, get_num_streams}, MuxAcceptor::accept, MuxStream::{is_closed, get_stream_id}, MuxWorker, StreamIdType, MuxConfig fields, and mux_connection. cargo doc --no-deps is clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4b62420 commit 5728c7e

5 files changed

Lines changed: 250 additions & 92 deletions

File tree

README.md

Lines changed: 112 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,129 @@
11
# async-smux
22

3-
[crates.io](https://crates.io/crates/async_smux)
3+
[![CI](https://github.com/black-binary/async-smux/actions/workflows/ci.yml/badge.svg)](https://github.com/black-binary/async-smux/actions/workflows/ci.yml)
4+
[![crates.io](https://img.shields.io/crates/v/async_smux.svg)](https://crates.io/crates/async_smux)
45

5-
A lightweight asynchronous [smux](https://github.com/xtaci/smux) (Simple MUltipleXing) library for smol/async-std and any async runtime compatible to `futures`.
6+
A lightweight asynchronous [smux](https://github.com/xtaci/smux) (Simple
7+
MUltipleXing) library for Tokio. Wraps any `AsyncRead + AsyncWrite +
8+
Unpin` transport (`TcpStream`, `TlsStream`, …) and lets you spawn many
9+
bi-directional `MuxStream`s over it — each one looks and behaves like a
10+
plain TCP stream.
611

712
![img](https://raw.githubusercontent.com/xtaci/smux/master/mux.jpg)
813

9-
`async-smux` consumes a struct implementing `AsyncRead + AsyncWrite + Unpin + Send`, like `TcpStream` and `TlsStream`, to create a `Mux<T>` struct. And then you may spawn multiple `MuxStream<T>`s (up to 4294967295) over `Mux<T>`, which also implements `AsyncRead + AsyncWrite + Unpin + Send`.
10-
11-
## Benchmark
12-
13-
Here is a simple benchmarking result on my local machine, comparing to the original version smux (written in go).
14-
15-
| Implementation | Throughput (TCP) | Handshake |
16-
| ----------------- | ---------------- | ---------- |
17-
| smux (go) | 0.4854 GiB/s | 17.070 K/s |
18-
| async-smux (rust) | 1.0550 GiB/s | 81.774 K/s |
19-
20-
Run `cargo bench` to test it by yourself. Check out `/benches` directory for more details.
21-
22-
## Laziness
23-
24-
No thread or task will be spawned by this library. It just spawns a few `future`s. So it's totally runtime-independent.
14+
## Quickstart
15+
16+
```rust,ignore
17+
use async_smux::MuxBuilder;
18+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
19+
use tokio::net::TcpStream;
20+
21+
#[tokio::main]
22+
async fn main() {
23+
let tcp = TcpStream::connect("127.0.0.1:12345").await.unwrap();
24+
25+
// build() returns three pieces:
26+
// connector — open new outgoing streams
27+
// acceptor — receive peer-initiated streams
28+
// worker — the future that drives I/O; spawn it
29+
let (connector, mut acceptor, worker) =
30+
MuxBuilder::client().with_connection(tcp).build();
31+
tokio::spawn(worker);
32+
33+
// Outgoing
34+
let mut s = connector.connect().unwrap();
35+
s.write_all(b"hello").await.unwrap();
36+
let mut buf = [0u8; 5];
37+
s.read_exact(&mut buf).await.unwrap();
38+
39+
// Incoming
40+
while let Some(mut peer) = acceptor.accept().await {
41+
// ...
42+
}
43+
}
44+
```
2545

26-
`Mux` and `MuxStream` are completely lazy and will DO NOTHING if you don't `poll()` them.
46+
The server side is identical except for `MuxBuilder::server()`. Client
47+
and server differ only in stream-id parity (odd vs. even) so two peers
48+
never collide on locally-allocated ids.
49+
50+
A complete working example is in [`examples/echo.rs`](examples/echo.rs).
51+
52+
## Lifecycle
53+
54+
Three handles share ownership of the connection state:
55+
56+
- **`MuxConnector`** — opens streams. `Clone`-able.
57+
- **`MuxAcceptor`** — receives peer-initiated streams. Implements
58+
`Stream<Item = MuxStream>`.
59+
- **`MuxWorker`** — the future that drains the underlying transport
60+
and dispatches frames. Spawn it with `tokio::spawn(worker)`.
61+
62+
The worker exits when:
63+
64+
- All public handles (connectors + acceptors + streams) are dropped, or
65+
- `MuxConnector::close().await` is called explicitly, or
66+
- The peer closes the transport, or
67+
- A keep-alive timeout fires (if configured).
68+
69+
`close()` performs an orderly shutdown: any frames already accepted by
70+
`AsyncWrite::poll_write` are drained to the wire before the underlying
71+
sink is closed. It also works without the worker being polled — useful
72+
in test setups or sync teardown paths.
73+
74+
## Configuration
75+
76+
```rust,ignore
77+
use async_smux::MuxBuilder;
78+
use std::num::{NonZeroU64, NonZeroUsize};
79+
80+
let (connector, acceptor, worker) = MuxBuilder::server()
81+
// Send a NOP frame every N seconds to keep the connection alive.
82+
.with_keep_alive_interval(NonZeroU64::new(15).unwrap())
83+
// If we don't see any frame from the peer for this many seconds,
84+
// declare the connection dead and tear everything down.
85+
// Defaults to 3 × keep_alive_interval.
86+
.with_keep_alive_timeout(NonZeroU64::new(45).unwrap())
87+
// Per-stream idle timeout (seconds): close streams with no
88+
// recent traffic.
89+
.with_idle_timeout(NonZeroU64::new(60).unwrap())
90+
// Backpressure thresholds: cap how many frames may sit in the
91+
// tx/rx queues before poll_write / poll_read park.
92+
.with_max_tx_queue(NonZeroUsize::new(1024).unwrap())
93+
.with_max_rx_queue(NonZeroUsize::new(1024).unwrap())
94+
.with_connection(connection)
95+
.build();
96+
```
2797

28-
Any polling operation, including `.read()` ,`.write()`, `accept()` and `connect()`, will push `Mux` and `MuxStream` working.
98+
All of these knobs are optional. Keep-alive and idle timeout are off
99+
unless explicitly enabled.
29100

30-
## Specification
101+
## Frame format (smux v1)
31102

32103
```text
33-
VERSION(1B) | CMD(1B) | LENGTH(2B) | STREAMID(4B) | DATA(LENGTH)
104+
VERSION(1B) | CMD(1B) | LENGTH(2B LE) | STREAMID(4B LE) | DATA(LENGTH)
34105
35106
VERSION: 1
36-
37107
CMD:
38-
SYN(0)
39-
FIN(1)
40-
PSH(2)
41-
NOP(3)
42-
43-
STREAMID: Randomly chosen number
108+
SYN(0) open stream (LENGTH must be 0)
109+
FIN(1) close stream (LENGTH must be 0)
110+
PSH(2) payload
111+
NOP(3) keep-alive (LENGTH must be 0; STREAMID is 0)
44112
```
45113

46-
## Example
47-
48-
```rust
49-
use async_smux::{Mux, MuxConfig};
50-
use async_std::net::{TcpListener, TcpStream};
51-
use async_std::prelude::*;
52-
53-
async fn echo_server() {
54-
let listener = TcpListener::bind("0.0.0.0:12345").await.unwrap();
55-
let (stream, _) = listener.accept().await.unwrap();
56-
let mux = Mux::new(stream, MuxConfig::default());
57-
loop {
58-
let mut mux_stream = mux.accept().await.unwrap();
59-
let mut buf = [0u8; 1024];
60-
let size = mux_stream.read(&mut buf).await.unwrap();
61-
mux_stream.write(&buf[..size]).await.unwrap();
62-
}
63-
}
114+
Stream id 0 is reserved for NOP; the library never hands it out and
115+
rejects any peer SYN that uses it.
64116

65-
fn main() {
66-
async_std::task::spawn(echo_server());
67-
async_std::task::block_on(async {
68-
smol::Timer::after(std::time::Duration::from_secs(1)).await;
69-
let stream = TcpStream::connect("127.0.0.1:12345").await.unwrap();
70-
let mux = Mux::new(stream, MuxConfig::default());
71-
for i in 0..100 {
72-
let mut mux_stream = mux.connect().await.unwrap();
73-
let mut buf = [0u8; 1024];
74-
mux_stream.write(b"hello").await.unwrap();
75-
let size = mux_stream.read(&mut buf).await.unwrap();
76-
let reply = String::from_utf8(buf[..size].to_vec()).unwrap();
77-
println!("{}: {}", i, reply);
78-
}
79-
});
80-
}
81-
```
117+
## Benchmark
118+
119+
| Implementation | Throughput (TCP) | Handshake |
120+
| ----------------- | ---------------- | ---------- |
121+
| smux (go) | 0.4854 GiB/s | 17.070 K/s |
122+
| async-smux (rust) | 1.0550 GiB/s | 81.774 K/s |
123+
124+
`benches/bench.rs` uses the unstable `test` crate so it requires a
125+
nightly toolchain: `cargo +nightly bench`.
126+
127+
## License
128+
129+
MIT — see [LICENSE](LICENSE).

src/builder.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
//! Type-state builder for constructing a smux session.
2+
//!
3+
//! Start with [`MuxBuilder::client`] or [`MuxBuilder::server`], chain
4+
//! optional `with_*` calls to configure timeouts and queue limits, then
5+
//! call `with_connection(...)` followed by `build()` to obtain the
6+
//! `(MuxConnector, MuxAcceptor, MuxWorker)` triple.
7+
18
use std::num::{NonZeroU64, NonZeroUsize};
29

310
use crate::{
@@ -17,11 +24,15 @@ pub struct WithConfig {
1724

1825
pub struct Begin {}
1926

27+
/// Type-state builder. Move through `Begin → WithConfig → WithConnection`
28+
/// by calling `client()` / `server()`, optional `with_*`, then
29+
/// `with_connection(...)`. Finalize with `build()`.
2030
pub struct MuxBuilder<State> {
2131
state: State,
2232
}
2333

2434
impl MuxBuilder<Begin> {
35+
/// Configure as a client: locally-allocated stream ids are odd.
2536
pub fn client() -> MuxBuilder<WithConfig> {
2637
MuxBuilder {
2738
state: WithConfig {
@@ -37,6 +48,7 @@ impl MuxBuilder<Begin> {
3748
}
3849
}
3950

51+
/// Configure as a server: locally-allocated stream ids are even.
4052
pub fn server() -> MuxBuilder<WithConfig> {
4153
MuxBuilder {
4254
state: WithConfig {
@@ -54,6 +66,9 @@ impl MuxBuilder<Begin> {
5466
}
5567

5668
impl MuxBuilder<WithConfig> {
69+
/// Send a NOP keep-alive frame every `interval_secs` seconds. When
70+
/// set, the dead-peer timeout (see [`Self::with_keep_alive_timeout`])
71+
/// also becomes active.
5772
pub fn with_keep_alive_interval(&mut self, interval_secs: NonZeroU64) -> &mut Self {
5873
self.state.config.keep_alive_interval = Some(interval_secs);
5974
self
@@ -67,21 +82,31 @@ impl MuxBuilder<WithConfig> {
6782
self
6883
}
6984

85+
/// Per-stream idle timeout: if a stream sees no traffic for this
86+
/// many seconds, it is closed and its handle is reaped.
7087
pub fn with_idle_timeout(&mut self, timeout_secs: NonZeroU64) -> &mut Self {
7188
self.state.config.idle_timeout = Some(timeout_secs);
7289
self
7390
}
7491

92+
/// Backpressure threshold for outbound frames. `poll_write` parks
93+
/// once a stream's pending tx queue exceeds this value. Defaults
94+
/// to 1024.
7595
pub fn with_max_tx_queue(&mut self, size: NonZeroUsize) -> &mut Self {
7696
self.state.config.max_tx_queue = size;
7797
self
7898
}
7999

100+
/// Backpressure threshold for inbound frames. The dispatcher parks
101+
/// once total pending rx exceeds this value, propagating
102+
/// backpressure to the peer's tx side. Defaults to 1024.
80103
pub fn with_max_rx_queue(&mut self, size: NonZeroUsize) -> &mut Self {
81104
self.state.config.max_rx_queue = size;
82105
self
83106
}
84107

108+
/// Bind the underlying transport. The transport must implement
109+
/// `AsyncRead + AsyncWrite + Unpin`.
85110
pub fn with_connection<T: TokioConn>(
86111
&mut self,
87112
connection: T,
@@ -96,6 +121,10 @@ impl MuxBuilder<WithConfig> {
96121
}
97122

98123
impl<T: TokioConn> MuxBuilder<WithConnection<T>> {
124+
/// Finalize the builder. Returns the three handles that share the
125+
/// session: the connector for outgoing streams, the acceptor for
126+
/// peer-initiated streams, and the worker future that must be
127+
/// polled (e.g. via `tokio::spawn`) to drive I/O.
99128
pub fn build(self) -> (MuxConnector<T>, MuxAcceptor<T>, MuxWorker<T>) {
100129
mux_connection(self.state.connection, self.state.config)
101130
}

src/config.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
use std::num::{NonZeroU64, NonZeroUsize};
22

3+
/// Parity used for locally-allocated stream ids. Server uses `Even`
4+
/// (0, 2, 4, …) and client uses `Odd` (1, 3, 5, …) so the two sides
5+
/// never collide. Stream id 0 is reserved for NOP keep-alive frames
6+
/// and is never handed out as a real stream id.
37
#[derive(Clone, Copy, Debug)]
48
pub enum StreamIdType {
59
Even = 0,
610
Odd = 1,
711
}
812

13+
/// Session configuration. Construct via [`crate::MuxBuilder`] for the
14+
/// fluent API, or build one directly and pass it to
15+
/// [`crate::mux_connection`].
916
#[derive(Clone, Copy, Debug)]
1017
pub struct MuxConfig {
18+
/// Parity for locally-allocated stream ids. See [`StreamIdType`].
1119
pub stream_id_type: StreamIdType,
20+
/// If set, send a NOP frame this often (seconds) and enable
21+
/// dead-peer detection.
1222
pub keep_alive_interval: Option<NonZeroU64>,
13-
/// Maximum gap (seconds) between any two received frames before the
14-
/// peer is declared dead and the connection closed. Only consulted
15-
/// when `keep_alive_interval` is also set. Defaults to 3 *
16-
/// `keep_alive_interval`.
23+
/// Maximum gap (seconds) between any two received frames before
24+
/// the peer is declared dead and the connection closed. Only
25+
/// consulted when `keep_alive_interval` is also set. Defaults to
26+
/// 3 × `keep_alive_interval`.
1727
pub keep_alive_timeout: Option<NonZeroU64>,
28+
/// If set, close any stream that sees no traffic for this many
29+
/// seconds.
1830
pub idle_timeout: Option<NonZeroU64>,
31+
/// Backpressure threshold for outbound frames per stream.
1932
pub max_tx_queue: NonZeroUsize,
33+
/// Backpressure threshold for inbound frames across the session.
2034
pub max_rx_queue: NonZeroUsize,
2135
}

0 commit comments

Comments
 (0)