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.rsfail_stream_outbound_before_handler.rsfail_stream_type_mismatch_inbound_to_handler.rsfail_stream_final_encoder_mismatch.rsfail_udp_with_stream_pipeline.rsfail_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:
writeonly queues or stages data.flushpushes already staged data to the socket.write_and_flushwrites 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, andFlow.src/pipeline/stream: TCP typed builder and runtime pipeline.src/pipeline/datagram: UDP typed builder and runtime pipeline.src/pipeline/core: sharedIdentity,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 thetlsfeature.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::Stopstops processing the current message direction without treating it as an error.Erris 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 asoneshot::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 leastmax_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 channelstats():ConnectionStatswhen stats are enabledwrite,flush,write_and_flushclose
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\nand optional\r, encoding appends\n.ByteArrayDecoder: drains the current buffer intoBytes; it can also encodeBytes.ByteArrayEncoder: pass-throughBytesencoder.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:Bytesencoder that prepends a length field.HttpCodec: minimal HTTP/1.1 server codec; decodes requests and encodes responses. SupportsContent-Lengthand 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 thewebsocketfeature; server-side WebSocket codec that transitions from HTTP Upgrade handshake state to frame state.HttpWsCodec: available with thewebsocketfeature; handles HTTP requests and WebSocket upgrades on one TCP port.
Built-In Datagram Codecs
Utf8DatagramCodec: treats every datagram as one UTF-8String.BytesDatagramCodec: treats every datagram as rawbytes::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, andframes_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 returnedWriteHandleis 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_flushudp_context_write_buffers_until_explicit_flushtcp_channel_write_buffers_until_flushudp_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 = Typeis allowed only for functions returningResult<()>.
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.rsexamples/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.rsexamples/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.rsexamples/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.rsexamples/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.rsexamples/websocket_server.rsexamples/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.logthroughput.pngp99_latency.pngserver_memory.pnglatency_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: customBytesLineCodec+Handler<Bytes>.server-rs-line-sync: line echo variant that awaitswrite_and_flush.server-rs-len: custom composite codec usingLengthFieldBasedFrameDecoderandLengthFieldPrepender.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:
- Add a small complete
.rsfile underexamples/. - Add a
[[example]]entry to the rootCargo.toml, withrequired-featuresif needed. - Prefer existing codecs and public traits; avoid relying on
pub(crate)runtime details. - Use
pipeline()for TCP anddatagram_pipeline()for UDP. - Use
#[handler]for simple handlers; write a manual impl when you need manual flushes, multiple writes, or connection close. - Add a trybuild pass case to protect the public API shape.
- Run
cargo testand 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 thetlsfeature, TCP stream contexts can expose negotiated TLS metadata withtls().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 themacrosfeature.TlsContextBuilder/ServerTlsContext/ClientTlsContext/TlsInfo: TLS context and metadata APIs behind thetlsfeature.
traits
Flow<T>:Next(T)continues the pipeline;Stopconsumes the message.Inbound<I>: inbound transform stage returningFlow<Out>.Business<I>: business transform stage between inbound and the final handler.Handler<I>: TCP final inbound handler withtype Write.DatagramHandler<I>: UDP final inbound handler withtype Write.Outbound<I>: outbound transform stage that converts handler writes into codec-encodable values.
codec
Decoder: TCP byte stream decoder returningOption<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 intoBytes, and can encodeBytes.ByteArrayEncoder: pass-throughBytesencoder.FixedLengthFrameDecoder: fixed-size binary frame codec.DelimiterBasedFrameDecoder: delimiter-terminated binary frame codec.LengthFieldBasedFrameDecoder: length-field frame decoder that can also encode zero-offset/zero-adjustmentBytes.LengthFieldPrepender:Bytesencoder 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 thejsonfeature.JsonEncode<T>: outbound JSON stage behind thejsonfeature.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 thewebsocketfeature.HttpWsCodec: HTTP + WebSocket shared-port codec behind thewebsocketfeature.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 byHttpWsRouter.HttpWsRouter<H, W>: combines an HTTP service and WebSocket service into oneHandler<HttpWsInbound>.
context
ConnInfo: TCP connection id, peer address, local address, and negotiated TLS metadata when thetlsfeature is enabled.DatagramInfo: UDP socket id, current peer address, local address, and optional TCP-derived TLS metadata for stream transformation contexts when thetlsfeature is enabled.ConnectionStats: TCP connection counter snapshot handle.Context<W>: TCP handler context withwrite,flush,write_and_flush,close,channel,stats, andtlswhen thetlsfeature is enabled.DatagramContext<W>: UDP handler context with current-peer and explicit-peer write/flush/close operations.InboundContext: identity context for inbound stages, includingtlsfor TLS TCP connections.BusinessContext: identity context for business stages, includingtlsfor TLS TCP connections.OutboundContext: identity context for outbound stages, includingtlsfor 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 inNoTruststate.ServerTlsContextBuilder: accepts certificate chain, private key, required or optional client-auth roots, ALPN protocols, and SNI-specific identities, then buildsServerTlsContext.ClientTlsContextBuilder<NoTrust>: accepts server-name overrides and trust strategy methods, but has nobuild.ClientTlsContextBuilder<HasTrust>: buildsClientTlsContextafter roots or a verifier have been selected, can attach an mTLS client identity withclient_identity_pem/client_identity_der, and can advertise ALPN protocols.TlsInfo: negotiated TLS metadata available from TCPContext::tls, stream stage contexts, andConnInfo::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 withcertificate_chain_*plusprivate_key_*.native_roots,webpki_roots, anddanger_accept_invalid_certsare gated bytls-native-roots,tls-webpki-roots, andtls-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 forStreamPipeline.runtime::StreamRuntimePipeline: public bound required by TCP transport to run typed pipelines.runtime::RuntimePipeline: compatibility alias trait forStreamRuntimePipeline.
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 forTcpConnectionConfig.TcpClientConfig: type alias forTcpConnectionConfig.TcpServer<F, L>: TCP server builder.TcpServer::tls: enables server-side TLS when thetlsfeature is enabled.TcpServerHandle: server shutdown/wait handle.TcpClient<F, L>: TCP client builder.TcpClient::tls: enables client-side TLS when thetlsfeature 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 forUdpSocketConfig.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.