Expand description
§SansIO - IO-Free Networking Framework
sansio is a Sans-IO networking framework for Rust that separates protocol logic from I/O operations,
making it easy to build modular, reusable, and testable network protocols.
Inspired by Netty and Wangle,
sansio brings the power of pipeline-based protocol composition to Rust.
§Core Concepts
§Pipeline
The Pipeline is the fundamental abstraction. It’s a chain of Handlers that process
inbound and outbound data. Pipelines implement an advanced form of the Intercepting Filter
pattern, giving you full control over how events flow through your protocol stack.
Key Benefits:
- Modular: Each handler does one thing well (UNIX philosophy)
- Composable: Chain handlers to build complex protocols
- Flexible: Easy to add, remove, or reorder handlers
- Testable: Test handlers in isolation without I/O
§Handler
A Handler processes messages flowing through the pipeline. Each handler has four associated types:
Rin: Input type for inbound messagesRout: Output type for inbound messagesWin: Input type for outbound messagesWout: Output type for outbound messages
Best Practice: Keep handlers focused on a single responsibility. If a handler does multiple things, split it into separate handlers.
§Protocol
The Protocol trait provides a simpler alternative to Handler for building Sans-IO protocols.
It’s fully decoupled from I/O, timers, and other runtime dependencies, making protocols easy to
test and reuse across different runtime environments.
§Event Flow
Messages flow through the pipeline in two directions:
- Inbound (bottom-up): Network → Handler 1 → Handler 2 → … → Handler N
- Outbound (top-down): Handler N → … → Handler 2 → Handler 1 → Network
| write()
+---------------------------------------------------+---------------+
| Pipeline | |
| \|/ |
| +----------+----------+------------+-----------+----------+ |
| | Handler N | |
| +----------+----------+------------+-----------+----------+ |
| /|\ | |
| | | |
| | | |
| | \|/ |
| +----------+----------+------------+-----------+----------+ |
| | Handler N-1 | |
| +----------+----------+------------+-----------+----------+ |
| /|\ | |
| | | |
| | Context.fire_poll_write() |
| | | |
| | | |
| Context.fire_handle_read() | |
| | | |
| | \|/ |
| +----------+----------+------------+-----------+----------+ |
| | Handler 2 | |
| +----------+----------+------------+-----------+----------+ |
| /|\ | |
| | | |
| | | |
| | \|/ |
| +----------+----------+------------+-----------+----------+ |
| | Handler 1 | |
| +----------+----------+------------+-----------+----------+ |
| /|\ | |
+---------------+-----------------------------------+---------------+
| handle_read() | poll_write()
| \|/
+---------------+-----------------------------------+---------------+
| | | |
| Internal I/O Threads (Transport Implementation) |
+-------------------------------------------------------------------+§Example: Echo Server
Here’s a complete example showing how to build a simple echo server using the pipeline pattern.
§Step 1: Define the Handler
The echo handler receives strings, prints them, and sends them back:
struct EchoServerHandler {
transmits: VecDeque<String>,
}
impl Handler for EchoServerHandler {
type Rin = TaggedString;
type Rout = Self::Rin;
type Win = TaggedString;
type Wout = Self::Win;
fn name(&self) -> &str {
"EchoServerHandler"
}
fn handle_read(
&mut self,
_ctx: &Context<Self::Rin, Self::Rout, Self::Win, Self::Wout>,
msg: Self::Rin,
) {
println!("handling {}", msg.message);
self.transmits.push_back(format!("{}\r\n", msg.message));
}
fn poll_write(
&mut self,
ctx: &Context<Self::Rin, Self::Rout, Self::Win, Self::Wout>,
) -> Option<Self::Wout> {
if let Some(msg) = ctx.fire_poll_write() {
self.transmits.push_back(msg);
}
self.transmits.pop_front()
}
}§Step 2: Build the Pipeline
Chain handlers together to form a complete protocol stack:
fn build_pipeline() -> Rc<Pipeline<BytesMut,String>> {
let pipeline: Pipeline<BytesMut,String> = Pipeline::new();
let line_based_frame_decoder_handler = ByteToMessageCodecHandler::new(Box::new(
LineBasedFrameDecoder::new(8192, true, TerminatorType::BOTH),
));
let string_codec_handler = StringCodecHandler::new();
let echo_server_handler = EchoServerHandler::new();
pipeline.add_back(line_based_frame_decoder_handler);
pipeline.add_back(string_codec_handler);
pipeline.add_back(echo_server_handler);
pipeline.finalize()
}Handler Responsibilities:
- LineBasedFrameDecoder: Splits byte stream on
\nor\r\n - StringCodec: Converts bytes ↔ UTF-8 strings
- EchoHandler: Application logic (echo messages back)
Important: Handler order matters! They’re processed in insertion order.
§Step 3: Run the Event Loop
The pipeline is pure protocol logic - you provide the I/O:
fn run(socket: UdpSocket, cancel_rx: crossbeam_channel::Receiver<()>) {
let mut buf = vec![0; 2000];
let pipeline = build_pipeline();
pipeline.transport_active();
loop {
// Check cancellation
if cancel_rx.try_recv().is_ok() {
break;
}
// Poll pipeline to write transmit to socket
while let Some(transmit) = pipeline.poll_write() {
socket.send(transmit)?;
}
// Poll pipeline to get next timeout
let mut eto = Instant::now() + Duration::from_millis(100);
pipeline.poll_timeout(&mut eto);
let delay_from_now = eto.checked_duration_since(Instant::now()).unwrap_or(Duration::from_secs(0));
if delay_from_now.is_zero() {
pipeline.handle_timeout(Instant::now());
continue;
}
socket.set_read_timeout(Some(delay_from_now)).expect("setting socket read timeout");
if let Ok(n) = socket.recv_from(buf) {
pipeline.handle_read(&buf[..n]);
}
// Drive time forward
pipeline.handle_timeout(Instant::now());
}
pipeline.transport_inactive();
}§Design Philosophy
sansio follows the Sans-IO pattern, which separates protocol logic from I/O concerns:
Benefits:
- Testable: Test protocol logic without real network I/O
- Flexible: Use with any I/O model (sync, async, embedded)
- Reusable: Same protocol code across different environments
- Debuggable: Easier to reason about and debug
Trade-offs:
- You manage the I/O loop yourself
- More control means more responsibility
- Steeper learning curve for simple cases
For most applications, the benefits far outweigh the trade-offs, especially as your protocol logic becomes more complex.
Structs§
- Context
- Handler and context types for building protocol pipelines
Enables a
Handlerto interact with the pipeline. - Pipeline
- Pipeline traits for inbound and outbound message processing A pipeline of handlers for processing protocol messages.
- Pipeline
Builder - Type-safe pipeline builder for compile-time type checking Type-safe pipeline builder that tracks the current pipeline state at compile time.
Traits§
- Handler
- Handler and context types for building protocol pipelines A handler processes messages flowing through a pipeline.
- Inbound
Pipeline - Pipeline traits for inbound and outbound message processing Inbound operations for a pipeline.
- Outbound
Pipeline - Pipeline traits for inbound and outbound message processing Outbound operations for a pipeline.
- Protocol
- Protocol trait for Sans-IO protocol implementations A Sans-IO protocol abstraction.