Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

rs-netty is a Tokio-native, Netty-inspired networking framework for Rust. It keeps the familiar Channel / Pipeline / Handler model, but rebuilds the main path around Rust’s type system, async/await, Tokio tasks, bounded channel queues, and ordinary owned messages.

The crate root enables #![deny(unsafe_code)]. The primary public entry points are:

  • pipeline(): builds a TCP stream pipeline.
  • datagram_pipeline(): builds a UDP datagram pipeline.
  • TcpServer / TcpClient: run TCP servers and clients.
  • UdpServer / UdpClient: run UDP servers and clients.
  • Handler / DatagramHandler / Inbound / Business / Outbound: implement pipeline stages.
  • Channel / DatagramChannel: write, flush, or close from outside the current handler.
  • Life: optional lifecycle hooks.
  • #[handler]: generates TCP and UDP final handler impls for simple handlers.

Minimal TCP Server

This is close to examples/tcp_echo_server.rs:

use rs_netty::{codec::LineCodec, handler, pipeline, Result, TcpServer};

#[tokio::main]
async fn main() -> Result<()> {
    TcpServer::bind("127.0.0.1:9000")
        .pipeline(|| {
            pipeline()
                .codec(LineCodec::new())
                .handler(Echo)
        })
        .run()
        .await
}

struct Echo;

#[handler(Echo)]
async fn echo(msg: String) -> Result<String> {
    Ok(msg)
}

LineCodec decodes the TCP byte stream into String values. Echo receives a String, and the macro-generated handler writes and flushes the returned String through the outbound side.

Minimal UDP Server

This is close to examples/udp_echo_server.rs:

use rs_netty::{codec::Utf8DatagramCodec, datagram_pipeline, handler, Result, UdpServer};

#[tokio::main]
async fn main() -> Result<()> {
    UdpServer::bind("127.0.0.1:9002")
        .pipeline(|| {
            datagram_pipeline()
                .codec(Utf8DatagramCodec)
                .handler(UdpEcho)
        })
        .run()
        .await
}

struct UdpEcho;

#[handler(UdpEcho)]
async fn udp_echo(msg: String) -> Result<String> {
    Ok(format!("echo: {msg}"))
}

UDP pipelines process complete datagrams. DatagramContext::write replies to the sender of the current datagram by default. Use write_to or write_to_and_flush to send to an explicit peer.

How To Read This Guide

If you want to use the framework, start with Typed Pipeline, TCP, UDP, Handlers, and Codecs. If you want to extend it, focus on Architecture, Lifecycle, Channel Write And Flush, Extension Guide, and API Map.

Design Goals

rs-netty is not a direct port of Java Netty. Its goal is to take the useful pipeline/handler/channel model and express it in a Rust-native way.

Keep The Useful Shape

The framework keeps these concepts:

  • codecs sit at pipeline boundaries and convert between bytes/datagrams and typed messages.
  • inbound stages process decoded inbound messages.
  • final handlers implement application semantics and write application-level responses.
  • outbound stages convert handler writes into values the codec can encode.
  • channel handles can write, flush, or close from outside the current handler.
  • lifecycle hooks can observe server, connection, and UDP socket startup/shutdown.

Use Rust Types Instead Of Dynamic Pipeline Mutation

Java Netty’s dynamic pipeline is flexible, but many mistakes show up at runtime: handlers are in the wrong order, upstream message types do not match downstream handlers, or a TCP pipeline is used with UDP. rs-netty encodes these constraints in builder types:

codec -> inbound* -> business* -> handler -> outbound*

Only methods that are valid in the current state exist. For example, .handler(...) is unavailable before .codec(...); .inbound(...) is unavailable after .handler(...); and the final outbound type must be encodable by the selected codec.

The repository’s trybuild tests cover these failure modes, including:

  • fail_stream_handler_before_codec.rs
  • fail_stream_outbound_before_handler.rs
  • fail_stream_type_mismatch_inbound_to_handler.rs
  • fail_stream_final_encoder_mismatch.rs
  • fail_udp_with_stream_pipeline.rs
  • fail_tcp_with_datagram_pipeline.rs

Separate TCP And UDP Builders

TCP uses pipeline() and requires codecs to implement Decoder and Encoder<T>. UDP uses datagram_pipeline() and requires codecs to implement DatagramDecoder and DatagramEncoder<T>.

This is a type boundary, not just a naming convention. TcpServer::pipeline accepts only IntoStreamPipeline; UdpServer::pipeline accepts only IntoDatagramPipeline. Mixing TCP and UDP pipeline builders fails at compile time.

Prefer Owned Messages

The public API does not expose Java Netty-style reference-counted ByteBuf. Codecs and handlers use owned types such as String, bytes::Bytes, and user-defined structs. This fits Rust ownership and avoids ref-count lifetime mistakes.

Bounded Queues And Explicit Flush

Channel and DatagramChannel use bounded Tokio mpsc queues internally. outbound_queue_size controls the external command queue size. When the queue is full, write calls wait for capacity instead of growing without bound.

Write and flush are explicit:

  • write only queues or stages data.
  • flush pushes already staged data to the socket.
  • write_and_flush writes and creates a flush boundary.

This lets throughput-oriented code batch writes while latency-oriented code can flush explicitly.

Zero Unsafe

The crate root uses #![deny(unsafe_code)], and rs-netty-macros does the same. The current library and macro crate do not use unsafe on the main implementation path.

Architecture

The main path has four layers: transport, pipeline runtime, stage traits, and channel/context.

Crate Layout

  • src/lib.rs: public modules and common re-exports.
  • src/traits.rs: Inbound, Business, Handler, DatagramHandler, Outbound, and Flow.
  • src/pipeline/stream: TCP typed builder and runtime pipeline.
  • src/pipeline/datagram: UDP typed builder and runtime pipeline.
  • src/pipeline/core: shared Identity, Then, stage pipe traits, and builder state markers.
  • src/codec: stream/datagram codec traits and built-in codecs.
  • src/context: stage contexts, handler contexts, stats, and identity types.
  • src/channel: external write handles and internal command enums.
  • src/tls.rs: optional TLS context builders and server/client contexts behind the tls feature.
  • src/transport/tcp: TCP server, client, connection runtime, and config.
  • src/transport/udp: UDP server, client, socket runtime, and config.
  • src/life.rs: lifecycle hook trait and close reasons.
  • rs-netty-macros: the #[handler] attribute macro.

TCP Runtime Flow

A TCP server calls the pipeline factory once for each accepted connection. A client can use either a reusable factory or pipeline_instance, which consumes one single-use pipeline.

The runtime flow is roughly:

TcpListener / TcpStream
  -> optional TLS accept/connect
  -> read_buf
  -> Decoder::decode
  -> InboundPipe
  -> BusinessPipe
  -> Handler::read(Context<W>, msg)
  -> Context outbox or Channel command queue
  -> OutboundPipe
  -> Encoder::encode
  -> write_buf
  -> flush/write_all

StreamConnectionRuntime selects over socket reads, external channel commands, and shutdown signals. Without an idle timeout it uses a no-timeout loop. With idle_timeout, it adds a read-idle timer. The timer is reset only by socket reads; outbound writes do not reset it.

UDP Runtime Flow

UDP servers and clients run around one socket task. The pipeline is socket-level:

UdpSocket::recv_from
  -> DatagramDecoder::decode_datagram
  -> InboundPipe
  -> BusinessPipe
  -> DatagramHandler::read(DatagramContext<W>, msg)
  -> DatagramContext outbox or DatagramChannel command queue
  -> OutboundPipe
  -> DatagramEncoder::encode_datagram
  -> pending_datagrams
  -> flush/send_to

A UDP server does not currently create per-peer child pipelines. If you need per-peer state, store it in the handler explicitly, for example with HashMap<SocketAddr, State>.

Static Stage Composition

The builder composes stages at the type level as Then<A, B>. Identity means that a direction has no user stages. Runtime InboundPipe, BusinessPipe, and OutboundPipe process Then recursively:

  • Flow::Next(value) forwards the value to the next stage.
  • Flow::Stop stops processing the current message direction without treating it as an error.
  • Err is mapped by the connection/socket runtime into decode, encode, handler, or runtime errors.

The main path does not use dynamic Box<dyn Handler> pipeline dispatch. Pipeline types are built from generic static stage composition.

Channel And Context

Context<W> and DatagramContext<W> are the write entry points inside final handlers. They hold a handler-local outbox, which is useful for multiple writes and explicit flush boundaries during one read.

Channel<W> and DatagramChannel<W> are cloneable external handles. They send commands to the connection/socket task through a bounded Tokio mpsc queue. TCP channels expose stats(), capacity(), max_capacity(), and is_closed(); UDP channels expose socket identity and queue state.

Typed Pipeline

The typed pipeline is rs-netty’s central constraint mechanism. It puts stage order and message transitions into builder type parameters so many mistakes become compile-time errors.

Shape

TCP:

#![allow(unused)]
fn main() {
pipeline()
    .codec(...)
    .inbound(...)*
    .business(...)*
    .handler(...)
    .outbound(...)*
}

UDP:

#![allow(unused)]
fn main() {
datagram_pipeline()
    .codec(...)
    .inbound(...)*
    .business(...)*
    .handler(...)
    .outbound(...)*
}

The two builders have the same stage shape, but different codec traits and final handler traits.

Builder States

Shared state markers live in pipeline/core/state.rs:

  • Start: initial state; only a codec can be added.
  • InboundPhase: a codec exists; inbound, business, or final handler can be added.
  • BusinessPhase: business processing has started; more business stages or the final handler can be added.
  • Ready: the final handler exists; outbound stages can be added and the builder can become a runtime pipeline.

These states appear as the first type parameter of PipelineBuilder<State, C, InP, BizP, H, OutP, CurrentIn, Write, CurrentOut> and DatagramPipelineBuilder<...>. If a stage is illegal in the current state, the method is simply not implemented for that builder type.

Message Type Chain

For TCP, the constraints are:

C: Decoder<Item = A>
InboundPipe<A, Out = B>
BusinessPipe<B, Out = CIn>
H: Handler<CIn, Write = W>
OutboundPipe<W, Out = COut>
codec: Encoder<COut>

This means:

  • the first inbound stage must accept the type decoded by the codec.
  • each following inbound/business stage must accept the previous stage’s Out.
  • the final handler input must match the inbound/business chain output.
  • the first outbound stage must accept Handler::Write.
  • the final outbound output must be encodable by the stream codec.

UDP uses the same type chain, but swaps in DatagramDecoder / DatagramEncoder and DatagramHandler.

Compile Failures Are Intentional API

The trybuild compile-fail tests document these constraints. This pipeline does not compile because Parse converts String into Request, but the final handler expects String:

#![allow(unused)]
fn main() {
let _ = pipeline()
    .codec(LineCodec::new())
    .inbound(Parse)
    .handler(EchoString);
}

This one also fails because LineCodec can encode String, while the handler writes Response and no outbound stage converts it:

#![allow(unused)]
fn main() {
let _ = TcpServer::bind("127.0.0.1:0")
    .pipeline(|| pipeline().codec(LineCodec::new()).handler(Router));
}

The fix is to add an outbound stage:

#![allow(unused)]
fn main() {
let _ = pipeline()
    .codec(LineCodec::new())
    .inbound(Parse)
    .handler(Router)
    .outbound(RenderResponse);
}

Flow

Inbound, Business, and Outbound return Result<Flow<T>>:

#![allow(unused)]
fn main() {
pub enum Flow<T> {
    Next(T),
    Stop,
}
}

Flow::Next continues the pipeline. Flow::Stop consumes the current message and stops processing in that direction without error. Final Handler / DatagramHandler implementations do not return Flow; they return Result<()> because they are the end of the inbound side.

TCP

TCP supports both server and client modes through stream pipelines.

Server

TcpServer::bind(addr) creates a builder. You must call .pipeline(...) to set a per-connection pipeline factory, then use either .run().await or .start().await.

#![allow(unused)]
fn main() {
use rs_netty::{codec::LineCodec, handler, pipeline, Result, TcpServer};

struct Echo;

#[handler(Echo)]
async fn echo(msg: String) -> Result<String> {
    Ok(msg)
}

let server = TcpServer::bind("127.0.0.1:0")
    .pipeline(|| pipeline().codec(LineCodec::new()).handler(Echo))
    .start()
    .await?;

server.shutdown();
server.wait().await?;
Ok::<(), rs_netty::Error>(())
}

start returns TcpServerHandle, which exposes local_addr(), shutdown(), and wait().await.

Client

TcpClient::connect(addr) offers two pipeline setup methods:

  • .pipeline(|| ...): a reusable factory for stateless or cheaply cloneable state.
  • .pipeline_instance(...): consumes one already-built pipeline, useful when a handler owns one-shot state such as oneshot::Sender.

This client snippet is close to examples/tcp_json_line_echo.rs:

#![allow(unused)]
fn main() {
let (tx, rx) = tokio::sync::oneshot::channel();

let client = TcpClient::connect("127.0.0.1:9003")
    .pipeline_instance(
        pipeline()
            .codec(LineCodec::new())
            .inbound(JsonDecode::<Response>::new())
            .handler(PrintResponse { response_tx: Some(tx) })
            .outbound(JsonEncode::<Request>::new()),
    )
    .run()
    .await?;

client.write_and_flush(Request { message: "hello json".to_string() }).await?;
let _ = rx.await;
client.close().await?;
client.wait().await?;
Ok::<(), rs_netty::Error>(())
}

TcpClientHandle<W> exposes channel(), write, flush, write_and_flush, close, and wait.

Configuration

TCP servers and clients share TcpConnectionConfig, defined in src/transport/tcp/config.rs. Defaults:

  • read_buffer_capacity: 8 KiB.
  • write_buffer_capacity: 8 KiB.
  • max_frame_size: 1 MiB.
  • outbound_queue_size: 1024.
  • tcp_nodelay: true.
  • idle_timeout: None.
  • track_connection_stats: false.

Builder methods include:

  • read_buffer_capacity(value)
  • write_buffer_capacity(value)
  • max_frame_size(value)
  • outbound_queue_size(value)
  • tcp_nodelay(value)
  • idle_timeout(duration)
  • track_connection_stats()

Clients also provide bind(local_addr) to choose the local address.

Runtime Details

The TCP runtime uses BytesMut for read and write buffers. After each socket read, it repeatedly calls codec.decode until the codec returns Ok(None). If the read buffer exceeds max_frame_size, the connection is closed with FrameTooLarge.

External Channel commands enter the runtime through a bounded queue. write only encodes into the write buffer; flush performs write_all; write_and_flush encodes and immediately flushes.

After track_connection_stats() is enabled, Context::stats() and Channel::stats() expose connection time, bytes read/written, and frames read/written. frames_written counts frames encoded into the write buffer; it does not guarantee those frames have already been flushed to the socket.

UDP

UDP uses datagram pipelines. The model is similar to TCP, but the boundary is a complete datagram rather than a byte stream.

Server

UdpServer::bind(addr) creates a socket server builder. .pipeline(|| ...) sets the socket-level pipeline factory:

#![allow(unused)]
fn main() {
use rs_netty::{codec::Utf8DatagramCodec, datagram_pipeline, handler, Result, UdpServer};

struct UdpEcho;

#[handler(UdpEcho)]
async fn udp_echo(msg: String) -> Result<String> {
    Ok(format!("echo: {msg}"))
}

let server = UdpServer::bind("127.0.0.1:0")
    .pipeline(|| {
        datagram_pipeline()
            .codec(Utf8DatagramCodec)
            .handler(UdpEcho)
    })
    .start()
    .await?;

server.shutdown();
server.wait().await?;
Ok::<(), rs_netty::Error>(())
}

UdpServerHandle exposes local_addr(), shutdown(), and wait().

Client

UdpClient::connect(remote_addr) binds to "0.0.0.0:0" by default. Use .bind(local_addr) to choose a local address. UdpClientHandle<W> provides these methods for the default remote peer:

  • write(msg)
  • flush()
  • write_and_flush(msg)

It also supports explicit peers:

  • write_to(peer_addr, msg)
  • write_to_and_flush(peer_addr, msg)

Socket-Level Pipeline

A UDP server currently creates one socket-level pipeline, not a child pipeline per peer. For every datagram, the runtime creates new DatagramInfo, InboundContext, BusinessContext, DatagramContext, and OutboundContext values. peer_addr() is the sender of the current datagram.

If the application needs per-peer state, store it in the handler yourself:

#![allow(unused)]
fn main() {
use std::{collections::HashMap, net::SocketAddr};

struct PeerState;

struct StatefulUdp {
    peers: HashMap<SocketAddr, PeerState>,
}
}

Configuration

UDP uses UdpSocketConfig:

  • read_buffer_capacity: default 64 KiB; normalized to at least max_datagram_size.
  • write_buffer_capacity: default 8 KiB.
  • max_datagram_size: default 64 KiB.
  • outbound_queue_size: default 1024.

Builder methods include:

  • read_buffer_capacity(value)
  • write_buffer_capacity(value)
  • max_datagram_size(value)
  • outbound_queue_size(value)

UDP currently has no TCP-style tcp_nodelay, connection stats, or idle timeout.

Datagram Write Semantics

DatagramContext::write(msg) writes to the current datagram peer. write_to(peer, msg) writes to an explicit peer. Both only stage data in the handler-local outbox. flush or *_and_flush sends pending datagrams through send_to.

A UDP flush acknowledgement only means the local send_to call completed. It does not mean the peer received the datagram, and UDP still provides no ordering, retransmission, or reliability guarantee.

Handlers

rs-netty has five stage traits. They live in src/traits.rs, and trait-variant generates the public Send variants used on the main path.

Inbound

Inbound<I> is an inbound transformation stage after decoding and before the final handler:

#![allow(unused)]
fn main() {
struct Trim;

impl Inbound<String> for Trim {
    type Out = String;

    async fn read(
        &mut self,
        _ctx: &mut rs_netty::InboundContext,
        msg: String,
    ) -> rs_netty::Result<rs_netty::Flow<Self::Out>> {
        Ok(rs_netty::Flow::Next(msg.trim().to_string()))
    }
}
}

InboundContext exposes id(), peer_addr(), and local_addr(). It does not allow writes.

Business

Business<I> is an application-level transformation stage between inbound stages and the final handler. It has the same type shape as Inbound, but the method is handle and the context is BusinessContext. Once the builder enters the business phase, it can add more business stages or the final handler, but cannot go back to inbound stages.

Handler

Handler<I> is the end of the TCP inbound side:

#![allow(unused)]
fn main() {
impl Handler<Request> for Router {
    type Write = Response;

    async fn read(&mut self, ctx: &mut Context<Self::Write>, req: Request) -> Result<()> {
        ctx.write_and_flush(Response { body: req.body }).await
    }
}
}

type Write is the application type this handler can write to the outbound side. Outbound stages start from this type and eventually produce a value the codec can encode.

Context<W> provides:

  • identity: id, peer_addr, local_addr
  • channel(): a cloneable external channel
  • stats(): ConnectionStats when stats are enabled
  • write, flush, write_and_flush
  • close

DatagramHandler

DatagramHandler<I> is the end of the UDP inbound side. It uses DatagramContext<W>, which supports write, write_to, flush, write_and_flush, write_to_and_flush, and close.

#![allow(unused)]
fn main() {
impl DatagramHandler<String> for UdpEcho {
    type Write = String;

    async fn read(&mut self, ctx: &mut DatagramContext<Self::Write>, msg: String) -> Result<()> {
        ctx.write_and_flush(format!("echo: {msg}")).await
    }
}
}

Outbound

Outbound<I> converts the application type written by a handler into the next outbound type. The final outbound type must be encodable by the codec.

#![allow(unused)]
fn main() {
struct RenderResponse;

impl Outbound<Response> for RenderResponse {
    type Out = String;

    async fn write(
        &mut self,
        _ctx: &mut rs_netty::OutboundContext,
        msg: Response,
    ) -> rs_netty::Result<rs_netty::Flow<Self::Out>> {
        Ok(rs_netty::Flow::Next(msg.body))
    }
}
}

OutboundContext also exposes only identity information and does not allow direct writes.

Macro Or Manual Impl

#[handler] is good for simple one-in/one-out final handlers and consume-only handlers. Write a manual impl when you need direct Context / DatagramContext access, explicit flush timing, multiple writes, connection close, or channel().

Codecs

Codecs sit at pipeline boundaries. Stream codecs are for TCP byte streams; datagram codecs are for UDP datagrams.

Stream Codec Traits

#![allow(unused)]
fn main() {
pub trait Decoder: Send + 'static {
    type Item: Send + 'static;
    fn decode(&mut self, src: &mut bytes::BytesMut) -> Result<Option<Self::Item>>;
}

pub trait Encoder<I>: Send + 'static {
    fn encode(&mut self, item: I, dst: &mut bytes::BytesMut) -> Result<()>;
}
}

decode should consume bytes only when a complete frame is available and return Some(item). It returns Ok(None) when more bytes are needed.

Datagram Codec Traits

#![allow(unused)]
fn main() {
pub trait DatagramDecoder: Send + 'static {
    type Item: Send + 'static;
    fn decode_datagram(&mut self, src: &[u8]) -> Result<Self::Item>;
}

pub trait DatagramEncoder<I>: Send + 'static {
    fn encode_datagram(&mut self, item: I, dst: &mut bytes::BytesMut) -> Result<()>;
}
}

A UDP decoder receives one complete datagram payload at a time.

Built-In Stream Codecs

  • LineCodec: UTF-8 newline-delimited stream codec; decoding strips \n and optional \r, encoding appends \n.
  • ByteArrayDecoder: drains the current buffer into Bytes; it can also encode Bytes.
  • ByteArrayEncoder: pass-through Bytes encoder.
  • FixedLengthFrameDecoder: fixed-size binary frame codec; encoding requires the exact configured length.
  • DelimiterBasedFrameDecoder: delimiter-terminated binary frame codec; can keep or strip delimiters.
  • LengthFieldBasedFrameDecoder: Netty-shaped length-field frame decoder with 1/2/3/4/8 byte fields, offset, adjustment, strip count, and byte order. As an encoder, it supports only zero offset and zero adjustment.
  • LengthFieldPrepender: Bytes encoder that prepends a length field.
  • HttpCodec: minimal HTTP/1.1 server codec; decodes requests and encodes responses. Supports Content-Length and optional chunked request bodies.
  • MqttCodec: MQTT 5 packet codec for fixed headers, Remaining Length, properties, and supported control packets. It does not maintain broker/client session state.
  • WebSocketCodec: available with the websocket feature; server-side WebSocket codec that transitions from HTTP Upgrade handshake state to frame state.
  • HttpWsCodec: available with the websocket feature; handles HTTP requests and WebSocket upgrades on one TCP port.

Built-In Datagram Codecs

  • Utf8DatagramCodec: treats every datagram as one UTF-8 String.
  • BytesDatagramCodec: treats every datagram as raw bytes::Bytes.

JSON Is A Pipeline Stage

JsonDecode<T> and JsonEncode<T> are available with the json feature. They are typed pipeline stages, not framing codecs:

#![allow(unused)]
fn main() {
let pipeline = pipeline()
    .codec(LineCodec::new())
    .inbound(JsonDecode::<Request>::new())
    .handler(ApiHandler)
    .outbound(JsonEncode::<Response>::new());
}

This keeps framing separate from JSON parsing/serialization. LineCodec defines message boundaries, JsonDecode<T> converts String/Bytes into T, and JsonEncode<T> converts T into a compact JSON String.

Codec Position

The codec is always the first required builder stage. It decides the initial inbound type and the final outbound type the pipeline must produce.

For example:

#![allow(unused)]
fn main() {
pipeline()
    .codec(LineCodec::new())          // Decoder<Item = String>, Encoder<String>
    .inbound(ParseRequest)            // String -> Request
    .handler(Router)                  // Request -> writes Response
    .outbound(RenderResponse);        // Response -> String
}

Without RenderResponse, LineCodec cannot encode Response, so the pipeline fails to compile.

Lifecycle

Life is the optional lifecycle hook trait. The default implementation is NoLife, where all hooks are no-ops.

#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
struct PrintLife;

impl rs_netty::Life for PrintLife {
    async fn tcp_server_started(&self, local_addr: std::net::SocketAddr) -> rs_netty::Result<()> {
        println!("server started: {local_addr}");
        Ok(())
    }
}
}

Hooks

Current hooks:

  • tcp_server_started(local_addr)
  • tcp_server_stopped(local_addr)
  • tcp_connection_opened(info)
  • tcp_connection_closed(info, reason)
  • udp_socket_started(local_addr)
  • udp_socket_stopped(local_addr)

TCP close reasons use CloseReason, including PeerClosed, LocalClosed, ChannelClosed, HandlerClosed, ServerShutdown, IdleTimeout, IoError, DecodeError, EncodeError, FrameTooLarge, and HandlerError.

Startup And Shutdown Behavior

TCP server start() binds the listener and then calls tcp_server_started. If that hook returns an error, start() returns the error.

Each accepted TCP connection, and each connected TCP client, calls tcp_connection_opened. If that hook fails, the connection task returns an error.

Hook failures during shutdown are logged, and the runtime tries to preserve the original close result.

The UDP socket task calls udp_socket_started when it starts and udp_socket_stopped when it stops.

Graceful Shutdown

TCP and UDP server start() methods return handles:

#![allow(unused)]
fn main() {
let server = TcpServer::bind("127.0.0.1:0")
    .pipeline(|| pipeline().codec(LineCodec::new()).handler(Echo))
    .start()
    .await?;

server.shutdown();
server.wait().await?;
Ok::<(), rs_netty::Error>(())
}

TCP server shutdown uses a watch channel to notify both the accept loop and connection tasks. UDP server shutdown notifies the socket task to exit.

Clients do not have a server-style shutdown handle. Use client.close().await? to request local connection/socket shutdown, then client.wait().await? to join the task.

Idle Timeout

TCP supports idle_timeout(duration). It closes a connection that has had no socket reads for the configured duration, and reports CloseReason::IdleTimeout to lifecycle hooks. In the source, the timer is reset only by successful reads; outbound writes do not reset the read-idle timer.

UDP currently has no idle timeout.

Connection Stats

TCP servers and clients can enable stats with track_connection_stats(). After that:

  • Context::stats() returns the current connection stats.
  • Channel::stats() returns a cloneable stats handle.
  • stats include connected_at, bytes_read, bytes_written, frames_read, and frames_written.

Stats are disabled by default to avoid shared allocations and atomic updates when monitoring is not needed.

Channel Write And Flush

rs-netty intentionally separates write from flush. This matters for latency, throughput, and tests.

Handler Context Writes

TCP Context<W>:

  • write(msg): puts the message into the current handler’s local outbox. The returned WriteHandle is ready immediately; awaiting it is only async-style compatibility.
  • flush(): requests a flush of messages staged by the current handler. The returned handle may be dropped or awaited.
  • write_and_flush(msg): stages a message and creates a flush boundary.

UDP DatagramContext<W> is similar, but the target may be the current peer or an explicit peer:

  • write(msg)
  • write_to(peer_addr, msg)
  • flush()
  • write_and_flush(msg)
  • write_to_and_flush(peer_addr, msg)

Await Vs Fire And Forget

The handles returned by flush() and write_and_flush() may be dropped:

#![allow(unused)]
fn main() {
ctx.write_and_flush("first".to_string());
}

This requests the runtime to drain the outbox even while the handler future is still pending. tests/server_lifecycle.rs verifies that fire-and-forget flushes can send the first response before the handler returns.

If you await:

#![allow(unused)]
fn main() {
ctx.write_and_flush("first".to_string()).await?;
}

the await waits until the local socket write completes for that flush boundary. It is not a remote acknowledgement and does not mean the peer’s handler processed the data.

Plain Write Buffers

Plain write does not automatically flush. Tests cover this behavior:

  • tcp_context_write_buffers_until_explicit_flush
  • udp_context_write_buffers_until_explicit_flush
  • tcp_channel_write_buffers_until_flush
  • udp_client_write_preserves_datagrams_until_flush

To send data:

#![allow(unused)]
fn main() {
ctx.write("sent".to_string()).await?;
ctx.flush().await?;
}

or:

#![allow(unused)]
fn main() {
ctx.write_and_flush("sent".to_string()).await?;
}

External Channel Writes

Channel<W> is the TCP external handle. It sends commands to the connection task through a bounded mpsc queue:

  • write(msg): waits for queue capacity, queues the message, and encodes it into the write buffer; it does not flush.
  • flush(): waits until previously queued writes have been flushed to the socket.
  • write_and_flush(msg): queues a message and waits until the local socket write completes.
  • close(): requests local connection shutdown.

DatagramChannel<W> is the UDP external handle:

  • write_to(peer, msg): queues a datagram; it does not send.
  • flush(): sends all pending datagrams.
  • write_to_and_flush(peer, msg): queues and sends.
  • close(): requests the socket task to exit.

UdpClientHandle<W> wraps write, flush, and write_and_flush for the default remote peer.

Bounded Queue Boundary

outbound_queue_size controls the channel command queue capacity. The default is 1024. When the queue is full, external write/flush/write_and_flush calls wait for capacity instead of growing without bound.

The handler-local outbox and the external channel queue are separate boundaries. Writes from Context first enter the local outbox, then the runtime drains them into the codec/write buffer or pending datagram queue.

Macros

rs-netty-macros provides #[handler]. The main crate’s default features include macros, so in most cases you can write:

#![allow(unused)]
fn main() {
use rs_netty::handler;
}

What It Generates

#[handler(TypeName)] adapts one async function into both Handler<I> and DatagramHandler<I> impls. The user still declares the handler struct explicitly; the macro only generates repetitive implementation code.

Request-to-response handler:

#![allow(unused)]
fn main() {
struct Echo;

#[handler(Echo)]
async fn echo(msg: String) -> rs_netty::Result<String> {
    Ok(msg)
}
}

This is roughly equivalent to:

#![allow(unused)]
fn main() {
impl rs_netty::Handler<String> for Echo {
    type Write = String;

    async fn read(
        &mut self,
        ctx: &mut rs_netty::Context<Self::Write>,
        msg: String,
    ) -> rs_netty::Result<()> {
        let msg = echo(msg).await?;
        ctx.write_and_flush(msg).await
    }
}
}

The macro also generates DatagramHandler<String> with DatagramContext::write_and_flush.

Consume-Only Handler

If the function returns Result<()>, the macro cannot infer type Write from the return type. You must specify write = Type:

#![allow(unused)]
fn main() {
struct PrintResponse;

#[handler(PrintResponse, write = String)]
async fn print_response(msg: String) -> rs_netty::Result<()> {
    println!("server -> {msg}");
    Ok(())
}
}

Here write = String means the connection can still write String values from an external channel.

Handler State

To access handler fields, put &mut HandlerType as the first argument:

#![allow(unused)]
fn main() {
struct PrintResponse {
    response_tx: Option<tokio::sync::oneshot::Sender<()>>,
}

#[handler(PrintResponse, write = Request)]
async fn print_response(handler: &mut PrintResponse, res: Response) -> rs_netty::Result<()> {
    if let Some(tx) = handler.response_tx.take() {
        let _ = tx.send(());
    }
    println!("server -> {}", res.echoed);
    Ok(())
}
}

This is the pattern used by examples/tcp_json_line_echo.rs.

Limits

The macro requires:

  • the annotated function must be async fn.
  • the function must return Result<T>.
  • arguments must be either (&mut HandlerType, msg) or (msg).
  • write = Type is allowed only for functions returning Result<()>.

Use a manual impl when you need direct Context/DatagramContext access, multiple writes, manual flushes, connection close, channel(), or more complex branching.

Examples

The repository examples cover TCP, UDP, typed chains, JSON, TLS, lifecycle hooks, HTTP, and WebSocket.

TCP Echo

  • examples/tcp_echo_server.rs
  • examples/tcp_echo_client.rs

Run:

cargo run --example tcp_echo_server
cargo run --example tcp_echo_client

The server uses LineCodec and #[handler(Echo)]. The client sends two lines with write_and_flush.

Typed TCP Chain

  • examples/tcp_typed_chain.rs
  • examples/tcp_typed_chain_client.rs

Run:

cargo run --example tcp_typed_chain
cargo run --example tcp_typed_chain_client

Server pipeline:

#![allow(unused)]
fn main() {
pipeline()
    .codec(LineCodec::new())
    .inbound(Trim)
    .inbound(ParseRequest)
    .handler(Router)
    .outbound(RenderResponse)
}

This shows a full String -> Request -> Response -> String type chain.

JSON Over Line

  • examples/tcp_json_line_echo.rs

Run the server:

cargo run --example tcp_json_line_echo --features json

Run the client:

cargo run --example tcp_json_line_echo --features json -- client

This example separates framing from JSON. LineCodec handles line boundaries; JsonDecode<T> / JsonEncode<T> handle typed JSON.

TLS Echo

  • examples/tcp_tls_echo.rs

Run:

cargo run --example tcp_tls_echo --features tls

This example generates a localhost test certificate, attaches TLS with .tls(...), and keeps LineCodec as the application plaintext codec. The same TLS context APIs also support required or optional mTLS, ALPN advertisement, SNI-specific certificate identities, and TlsInfo metadata through TCP handler/stage contexts.

Lifecycle

  • examples/tcp_lifecycle.rs

Run:

cargo run --example tcp_lifecycle

This example implements Life and prints TCP server started/stopped and connection opened/closed events.

UDP Echo

  • examples/udp_echo_server.rs
  • examples/udp_echo_client.rs

Run:

cargo run --example udp_echo_server
cargo run --example udp_echo_client

It uses Utf8DatagramCodec and datagram_pipeline().

Typed UDP Chain

  • examples/udp_typed_chain.rs
  • examples/udp_typed_chain_client.rs

Run:

cargo run --example udp_typed_chain
cargo run --example udp_typed_chain_client

This demonstrates typed inbound/outbound transformations in a UDP datagram pipeline.

HTTP And WebSocket

  • examples/http_server.rs
  • examples/websocket_server.rs
  • examples/http_websocket_server.rs

Run:

cargo run --example http_server
cargo run --example websocket_server --features websocket
cargo run --example http_websocket_server --features websocket

http_server uses HttpCodec. websocket_server uses WebSocketCodec. http_websocket_server uses HttpWsCodec and HttpWsRouter to handle HTTP requests and WebSocket upgrades on the same port.

Benchmarks

benchmarks/ contains three comparable harnesses:

  • benchmarks/rs-netty: rs-netty echo servers and clients.
  • benchmarks/tokio: bare Tokio echo servers and clients.
  • benchmarks/netty: Java Netty echo servers and clients.

They align the wire protocols:

  • line: TCP line echo, payload + "\n".
  • len: TCP length-field echo, u32be length + payload.
  • udp: UDP datagram echo.

Directional Snapshot

Benchmark results are directional snapshots, not general performance promises. Throughput, latency, and RSS depend on host, NIC, OS, JVM warmup, TCP settings, payload shape, connection count, in-flight count, loopback usage, and other factors.

The table in the README comes from one local non-loopback run of this repository’s benchmark harness. It is useful for understanding approximate scale and relative trends, not as a guarantee in arbitrary production environments.

Runner

Main entry point:

python3 benchmarks/run.py \
  --impls rs-netty tokio netty \
  --protocols line len udp \
  --connections 100 \
  --messages 1000000 \
  --payload 128 \
  --in-flight 16 \
  --output-dir benchmarks/results

The runner:

  • auto-selects a non-loopback local IPv4 address, or uses --host.
  • rejects localhost, 127.0.0.1, and ::1.
  • builds selected implementations.
  • starts the server and samples server RSS.
  • runs the matching client.
  • parses the RESULT ... line.
  • writes CSV, logs, and charts.

Output includes:

  • results.csv
  • *.server.out.log
  • *.server.err.log
  • *.client.out.log
  • *.client.err.log
  • throughput.png
  • p99_latency.png
  • server_memory.png
  • latency_percentiles.png

With --profile cpu, macOS sample(1) also produces server samples and profile summaries.

Smoke Run

Quick smoke run:

python3 benchmarks/run.py \
  --impls rs-netty tokio \
  --protocols len \
  --connections 2 \
  --messages 100 \
  --payload 32 \
  --in-flight 4

Including Netty requires Maven and a JDK. The runner builds Rust harnesses in release mode automatically.

rs-netty Harness Notes

benchmarks/rs-netty/src/main.rs contains:

  • server-rs-line / server-rs-line-string: LineCodec + Handler<String>.
  • server-rs-line-bytes: custom BytesLineCodec + Handler<Bytes>.
  • server-rs-line-sync: line echo variant that awaits write_and_flush.
  • server-rs-len: custom composite codec using LengthFieldBasedFrameDecoder and LengthFieldPrepender.
  • server-rs-udp: Utf8DatagramCodec + DatagramHandler<String>.

Clients use bare Tokio connections, record latency percentiles, and print a uniform RESULT line.

Non Goals

The following are not goals of the current rs-netty main path. They come from the README, public API, and source boundaries.

No EventLoop API

rs-netty directly uses the Tokio runtime, listener/socket tasks, and per-connection tasks. It does not expose a Java Netty-style EventLoop or EventLoopGroup API.

No ByteBuf RefCnt API

The public API uses bytes::Bytes, BytesMut, String, and user-defined owned types. The framework does not expose reference-counted ByteBuf or retain/release/refCnt.

No ChannelFuture / Promise API

Write APIs use Rust async and Result. flush / write_and_flush acknowledgements are expressed by awaiting local socket write completion. There is no Java Netty-style ChannelFuture or Promise main path.

No Dynamic Boxed Handler Main Path

The default pipeline is composed from generic static stages. It does not use Box<dyn Handler> as the main path, which allows stage order and message types to be checked at compile time.

No Runtime Pipeline Mutation API

After the builder creates a pipeline, the runtime does not provide Netty-style pipeline.addLast/remove/replace dynamic mutation on the main path.

No TLS Pipeline Stage

TLS is modeled as an optional TCP transport layer, not as a codec or ordinary pipeline stage. The typed pipeline still processes plaintext application messages after the TLS stream is established. TLS metadata is exposed through TlsInfo, but TLS negotiation itself still happens at the transport boundary rather than as a dynamic pipeline stage.

No Codec Registry

Built-in codecs are ordinary Rust types that are instantiated explicitly in pipelines. There is no global codec registry, protocol-name lookup, or runtime codec negotiation registry.

No Automatic UDP Reliability

UDP supports datagram send/receive, but does not provide reliability, ordering, retransmission, congestion control, or automatic session management.

No Per-Peer UDP Child Pipeline

UDP servers use a socket-level pipeline and do not create independent child pipelines per remote peer. Applications manage per-peer state in their handlers.

No MQTT Broker State

MqttCodec encodes/decodes MQTT 5 packets and performs local format validation. It does not maintain broker/client sessions, subscription trees, QoS state machines, or retained message stores.

Minimal HTTP/WebSocket Scope

HttpCodec and the WebSocket codecs are suitable for simple server-side pipeline examples and lightweight usage. They are not a full HTTP framework and do not provide a routing DSL, middleware stack, HTTP/2, compression, WebSocket extension negotiation, or fragmented data frame reassembly.

Extension Guide

This chapter shows the smallest path for adding codecs, handlers, and examples.

Add A TCP Codec

Implement Decoder and Encoder<T>. If decode output and encode input are different, implement Decoder and Encoder<T> on the same type, then use an outbound stage to convert application responses into T.

#![allow(unused)]
fn main() {
use bytes::{Buf, BufMut, Bytes, BytesMut};
use rs_netty::{codec::{Decoder, Encoder}, Error, Result};

struct LengthCodec;

impl Decoder for LengthCodec {
    type Item = Bytes;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>> {
        if src.len() < 4 {
            return Ok(None);
        }

        let len = u32::from_be_bytes([src[0], src[1], src[2], src[3]]) as usize;
        if src.len() < 4 + len {
            return Ok(None);
        }

        src.advance(4);
        Ok(Some(src.split_to(len).freeze()))
    }
}

impl Encoder<Bytes> for LengthCodec {
    fn encode(&mut self, item: Bytes, dst: &mut BytesMut) -> Result<()> {
        let len = u32::try_from(item.len()).map_err(|err| Error::Encode(err.to_string()))?;
        dst.put_u32(len);
        dst.extend_from_slice(&item);
        Ok(())
    }
}
}

The benchmark LengthCodec in this repository uses a more reusable approach: it composes LengthFieldBasedFrameDecoder and LengthFieldPrepender internally.

Add A UDP Codec

Implement DatagramDecoder and DatagramEncoder<T>. Each decode input is one datagram payload:

#![allow(unused)]
fn main() {
use bytes::{Bytes, BytesMut};
use rs_netty::{codec::{DatagramDecoder, DatagramEncoder}, Result};

struct RawDatagram;

impl DatagramDecoder for RawDatagram {
    type Item = Bytes;

    fn decode_datagram(&mut self, src: &[u8]) -> Result<Self::Item> {
        Ok(Bytes::copy_from_slice(src))
    }
}

impl DatagramEncoder<Bytes> for RawDatagram {
    fn encode_datagram(&mut self, item: Bytes, dst: &mut BytesMut) -> Result<()> {
        dst.extend_from_slice(&item);
        Ok(())
    }
}
}

This is very close to the built-in BytesDatagramCodec.

Add An Inbound Handler

Implement Inbound<I> and return Flow<Out>:

#![allow(unused)]
fn main() {
struct ParseRequest;

impl rs_netty::Inbound<String> for ParseRequest {
    type Out = Request;

    async fn read(
        &mut self,
        _ctx: &mut rs_netty::InboundContext,
        msg: String,
    ) -> rs_netty::Result<rs_netty::Flow<Self::Out>> {
        Ok(rs_netty::Flow::Next(Request { body: msg }))
    }
}
}

Return Flow::Stop when you want to filter a message instead of forwarding it.

Add An Outbound Handler

Implement Outbound<I> to convert an application response type into the next outbound type:

#![allow(unused)]
fn main() {
struct RenderResponse;

impl rs_netty::Outbound<Response> for RenderResponse {
    type Out = String;

    async fn write(
        &mut self,
        _ctx: &mut rs_netty::OutboundContext,
        msg: Response,
    ) -> rs_netty::Result<rs_netty::Flow<Self::Out>> {
        Ok(rs_netty::Flow::Next(msg.body))
    }
}
}

Place it after the final handler:

#![allow(unused)]
fn main() {
pipeline()
    .codec(LineCodec::new())
    .inbound(ParseRequest)
    .handler(Router)
    .outbound(RenderResponse);
}

Add A Complete Example

Recommended steps:

  1. Add a small complete .rs file under examples/.
  2. Add a [[example]] entry to the root Cargo.toml, with required-features if needed.
  3. Prefer existing codecs and public traits; avoid relying on pub(crate) runtime details.
  4. Use pipeline() for TCP and datagram_pipeline() for UDP.
  5. Use #[handler] for simple handlers; write a manual impl when you need manual flushes, multiple writes, or connection close.
  6. Add a trybuild pass case to protect the public API shape.
  7. Run cargo test and relevant feature tests.

For a complete TCP typed chain, see examples/tcp_typed_chain.rs. For a complete UDP typed chain, see examples/udp_typed_chain.rs.

API Map

This chapter lists important public APIs by module. pub(crate) runtime details are not listed as user APIs.

crate root

  • pipeline(): creates a TCP stream typed pipeline builder.
  • datagram_pipeline(): creates a UDP datagram typed pipeline builder.
  • TcpServer / TcpClient: TCP server/client builders.
  • UdpServer / UdpClient: UDP server/client builders.
  • Channel / DatagramChannel: cloneable external write/flush/close handles.
  • Context / DatagramContext: final handler context for writing and connection/socket operations.
  • InboundContext / BusinessContext / OutboundContext: read-only identity contexts for transform stages. With the tls feature, TCP stream contexts can expose negotiated TLS metadata with tls().
  • Life / NoLife / CloseReason: lifecycle hook API.
  • Error / Result: framework error type and result alias.
  • Flow: continue or stop a stage’s current message.
  • handler: attribute macro re-exported with the macros feature.
  • TlsContextBuilder / ServerTlsContext / ClientTlsContext / TlsInfo: TLS context and metadata APIs behind the tls feature.

traits

  • Flow<T>: Next(T) continues the pipeline; Stop consumes the message.
  • Inbound<I>: inbound transform stage returning Flow<Out>.
  • Business<I>: business transform stage between inbound and the final handler.
  • Handler<I>: TCP final inbound handler with type Write.
  • DatagramHandler<I>: UDP final inbound handler with type Write.
  • Outbound<I>: outbound transform stage that converts handler writes into codec-encodable values.

codec

  • Decoder: TCP byte stream decoder returning Option<Item>.
  • Encoder<I>: TCP byte stream encoder.
  • DatagramDecoder: UDP datagram decoder.
  • DatagramEncoder<I>: UDP datagram encoder.
  • LineCodec: UTF-8 newline-delimited stream codec.
  • ByteArrayDecoder: drains the current stream buffer into Bytes, and can encode Bytes.
  • ByteArrayEncoder: pass-through Bytes encoder.
  • FixedLengthFrameDecoder: fixed-size binary frame codec.
  • DelimiterBasedFrameDecoder: delimiter-terminated binary frame codec.
  • LengthFieldBasedFrameDecoder: length-field frame decoder that can also encode zero-offset/zero-adjustment Bytes.
  • LengthFieldPrepender: Bytes encoder that prepends a length field.
  • ByteOrder: endian setting used by length-field codecs.
  • Utf8DatagramCodec: UTF-8 datagram codec.
  • BytesDatagramCodec: raw bytes datagram codec.
  • JsonDecode<T>: inbound JSON stage behind the json feature.
  • JsonEncode<T>: outbound JSON stage behind the json feature.
  • HttpCodec: minimal HTTP/1.1 request/response server codec.
  • HttpRequest: HTTP request view with method/target/version/header/body/trailer accessors.
  • HttpResponse: HTTP response builder with status/reason/header/body.
  • MqttCodec: MQTT 5 packet stream codec.
  • MqttPacket: MQTT control packet enum.
  • QoS: MQTT QoS enum.
  • ConnectPacket / ConnAckPacket / PublishPacket / AckPacket: common MQTT packet structs.
  • SubscribePacket / SubAckPacket / UnsubscribePacket / UnsubAckPacket: MQTT subscribe/unsubscribe packet structs.
  • DisconnectPacket / AuthPacket / Will / MqttProperty: MQTT helper packet/property types.
  • WebSocketCodec: server-side WebSocket codec behind the websocket feature.
  • HttpWsCodec: HTTP + WebSocket shared-port codec behind the websocket feature.
  • WebSocketInbound / WebSocketOutbound / WebSocketMessage: WebSocket message enums.
  • HttpWsInbound / HttpWsOutbound: message enums for the shared HTTP/WebSocket codec.
  • WebSocketHandshake / WebSocketHandshakeResponse / WebSocketClose: WebSocket handshake and close types.
  • HttpService / WebSocketService: static service traits used by HttpWsRouter.
  • HttpWsRouter<H, W>: combines an HTTP service and WebSocket service into one Handler<HttpWsInbound>.

context

  • ConnInfo: TCP connection id, peer address, local address, and negotiated TLS metadata when the tls feature is enabled.
  • DatagramInfo: UDP socket id, current peer address, local address, and optional TCP-derived TLS metadata for stream transformation contexts when the tls feature is enabled.
  • ConnectionStats: TCP connection counter snapshot handle.
  • Context<W>: TCP handler context with write, flush, write_and_flush, close, channel, stats, and tls when the tls feature is enabled.
  • DatagramContext<W>: UDP handler context with current-peer and explicit-peer write/flush/close operations.
  • InboundContext: identity context for inbound stages, including tls for TLS TCP connections.
  • BusinessContext: identity context for business stages, including tls for TLS TCP connections.
  • OutboundContext: identity context for outbound stages, including tls for TLS TCP connections.

channel

  • Channel<W>: TCP external handle with identity, queue capacity, stats, write/flush/write_and_flush/close.
  • DatagramChannel<W>: UDP external handle with socket identity, queue capacity, write_to/flush/write_to_and_flush/close.

life

  • CloseReason: TCP connection close reason.
  • NoLife: default no-op lifecycle hooks.
  • Life: lifecycle hook trait for servers, connections, and UDP sockets.

tls

  • TlsContextBuilder::for_server(): starts a server TLS context builder.
  • TlsContextBuilder::for_client(): starts a typestate client TLS context builder in NoTrust state.
  • ServerTlsContextBuilder: accepts certificate chain, private key, required or optional client-auth roots, ALPN protocols, and SNI-specific identities, then builds ServerTlsContext.
  • ClientTlsContextBuilder<NoTrust>: accepts server-name overrides and trust strategy methods, but has no build.
  • ClientTlsContextBuilder<HasTrust>: builds ClientTlsContext after roots or a verifier have been selected, can attach an mTLS client identity with client_identity_pem / client_identity_der, and can advertise ALPN protocols.
  • TlsInfo: negotiated TLS metadata available from TCP Context::tls, stream stage contexts, and ConnInfo::tls. It exposes peer certificates, selected ALPN, and the client/server name used by the connection.
  • client_auth_required_pem / client_auth_required_der: require client certificates signed by trusted roots.
  • client_auth_optional_pem / client_auth_optional_der: allow clients without certificates, but verify a certificate when one is presented.
  • alpn_protocols: configures advertised ALPN protocols on client or server contexts. Protocol names must be non-empty and at most 255 bytes.
  • sni_certificate_pem / sni_certificate_der: add SNI-specific server certificate identities. A default certificate can be configured as fallback with certificate_chain_* plus private_key_*.
  • native_roots, webpki_roots, and danger_accept_invalid_certs are gated by tls-native-roots, tls-webpki-roots, and tls-dangerous.

pipeline::stream

  • builder::pipeline(): TCP builder entry point.
  • builder::PipelineBuilder<...>: TCP builder carrying stage state and message types.
  • builder::IntoStreamPipeline: converts a ready builder into a runtime stream pipeline.
  • builder::IntoPipeline: compatibility conversion trait for stream pipelines.
  • runtime::StreamPipeline<...>: TCP runtime pipeline type, usually not named directly.
  • runtime::Pipeline<...>: compatibility type alias for StreamPipeline.
  • runtime::StreamRuntimePipeline: public bound required by TCP transport to run typed pipelines.
  • runtime::RuntimePipeline: compatibility alias trait for StreamRuntimePipeline.

pipeline::datagram

  • builder::datagram_pipeline(): UDP builder entry point.
  • builder::DatagramPipelineBuilder<...>: UDP builder carrying stage state and message types.
  • builder::IntoDatagramPipeline: converts a ready builder into a runtime datagram pipeline.
  • runtime::DatagramPipeline<...>: UDP runtime pipeline type, usually not named directly.
  • runtime::DatagramRuntimePipeline: public bound required by UDP transport to run typed pipelines.

pipeline::core

  • Identity: empty stage pipe that forwards values unchanged.
  • Then<A, B>: statically composes two stage pipes.
  • PipeStep<T, F>: ready/future stage step used by runtime optimizations.
  • InboundPipe<I> / BusinessPipe<I> / OutboundPipe<I>: internal stage-chain processing traits exposed because they appear in builder/runtime bounds.
  • Start / InboundPhase / BusinessPhase / Ready: builder state markers.

transport::tcp

  • TcpConnectionConfig: TCP connection configuration.
  • TcpServerConfig / ServerConfig: type aliases for TcpConnectionConfig.
  • TcpClientConfig: type alias for TcpConnectionConfig.
  • TcpServer<F, L>: TCP server builder.
  • TcpServer::tls: enables server-side TLS when the tls feature is enabled.
  • TcpServerHandle: server shutdown/wait handle.
  • TcpClient<F, L>: TCP client builder.
  • TcpClient::tls: enables client-side TLS when the tls feature is enabled.
  • TcpClientHandle<W>: active TCP client handle.
  • PipelineFactory<F>: wrapper for reusable client pipeline factories.
  • PipelineInstance<B>: wrapper for single-use client pipelines.

transport::udp

  • UdpSocketConfig: UDP socket configuration.
  • UdpServerConfig / UdpClientConfig: type aliases for UdpSocketConfig.
  • UdpServer<F, L>: UDP server socket builder.
  • UdpServerHandle: UDP server shutdown/wait handle.
  • UdpClient<F, L>: UDP client socket builder.
  • UdpClientHandle<W>: active UDP client handle.

client And server

  • client::TcpClient / client::TcpClientHandle / client::TcpClientConfig: TCP re-exports under the client module.
  • client::UdpClient / client::UdpClientHandle / client::UdpClientConfig: UDP re-exports under the client module.
  • server::TcpServer / server::TcpServerHandle / server::TcpServerConfig / server::ServerConfig: TCP re-exports under the server module.
  • server::UdpServer / server::UdpServerHandle / server::UdpServerConfig: UDP re-exports under the server module.