Loading page…
Rust walkthroughs
Loading page…
tokio::net::TcpListener differ from std::net::TcpListener when used with async runtimes?tokio::net::TcpListener is designed for asynchronous I/O and integrates with tokio's event loop, while std::net::TcpListener performs blocking I/O that will halt the entire async runtime when its methods are called. The critical difference is that calling accept() on a std::net::TcpListener blocks the OS thread, preventing the async runtime from making progress on any other task, whereas tokio::net::TcpListener::accept() returns a future that yields control back to the runtime while waiting for a connection. Using std::net::TcpListener in async code requires explicit handling via tokio::task::spawn_blocking or converting to non-blocking mode, while tokio::net::TcpListener works naturally with .await and doesn't block the runtime.
use std::net::TcpListener;
fn main() -> std::io::Result<()> {
// Bind to a port
let listener = TcpListener::bind("127.0.0.1:8080")?;
// Accept connections in a blocking manner
loop {
let (stream, addr) = listener.accept()?;
println!("Connection from {}", addr);
// Handle stream...
}
}std::net::TcpListener blocks the thread on every accept call.
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Bind to a port
let listener = TcpListener::bind("127.0.0.1:8080").await?;
// Accept connections asynchronously
loop {
let (stream, addr) = listener.accept().await?;
println!("Connection from {}", addr);
// Handle stream without blocking...
}
}tokio::net::TcpListener uses async/await and doesn't block the runtime.
use std::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// Spawn other async tasks
tokio::spawn(async {
loop {
println!("Background task working...");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
});
// This BLOCKS the entire runtime
// The background task cannot make progress while accept() waits
let (stream, addr) = listener.accept().unwrap(); // BLOCKS!
}Using std::net::TcpListener::accept() in async code blocks the entire executor.
use std::net::TcpListener;
#[tokio::main]
async fn main() {
// Tokio runtime uses a thread pool (typically)
// Each thread runs many tasks concurrently via cooperative scheduling
// When a task calls blocking code:
// 1. The thread cannot switch to other tasks
// 2. Other tasks on the same thread are stuck
// 3. The runtime may have fewer threads available
// 4. Performance degrades severely
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// If no connection arrives for 10 seconds:
// - The thread is blocked for 10 seconds
// - All other tasks on this thread wait
// - The runtime cannot use this thread for anything
let (stream, addr) = listener.accept().unwrap();
}Blocking I/O defeats the entire purpose of async concurrency.
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// Spawn background task
tokio::spawn(async {
loop {
println!("Background task running");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
});
// Accept connections asynchronously
loop {
// This yields to the runtime while waiting
// Background task continues running!
let (stream, addr) = listener.accept().await.unwrap();
println!("Connection from {}", addr);
}
}Async accept yields control, allowing other tasks to run.
use std::net::TcpListener as StdTcpListener;
use tokio::net::TcpListener as TokioTcpListener;
// std::net::TcpListener
// fn accept(&self) -> Result<(TcpStream, SocketAddr)>
// tokio::net::TcpListener
// async fn accept(&self) -> Result<(TcpStream, SocketAddr)>
// Key difference: tokio's accept is async (returns a Future)
// std's accept blocks the thread until a connection arrivesTokio's accept returns a Future that can be awaited.
use std::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// Move blocking code to a blocking thread pool
loop {
// spawn_blocking moves work to a dedicated thread pool
// The async runtime continues on other threads
let (stream, addr) = tokio::task::spawn_blocking(|| {
listener.accept().unwrap()
}).await.unwrap();
println!("Connection from {}", addr);
// Handle stream - but stream is std::net::TcpStream
// You'd need to convert it or handle it in spawn_blocking too
}
}spawn_blocking isolates blocking code but has overhead and complexity.
use std::net::TcpListener;
use tokio::net::TcpListener as TokioTcpListener;
#[tokio::main]
async fn main() {
// Create with std
let std_listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// Convert to tokio
// This sets the socket to non-blocking mode
let listener = TokioTcpListener::from_std(std_listener).unwrap();
// Now use async accept
loop {
let (stream, addr) = listener.accept().await.unwrap();
println!("Connection from {}", addr);
}
}from_std converts a blocking socket to a non-blocking async socket.
// Synchronous approach (std::net)
use std::net::TcpListener;
use std::thread;
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
// Spawn a thread per connection
thread::spawn(move || {
// Handle connection
});
}
}
// Thread per connection: doesn't scale wellSynchronous code needs one thread per connection.
// Asynchronous approach (tokio::net)
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
loop {
let (stream, addr) = listener.accept().await.unwrap();
// Spawn a task per connection
tokio::spawn(async move {
// Handle connection
});
}
}
// Task per connection: scales to thousandsAsync code uses lightweight tasks that don't require OS threads.
use std::net::TcpListener;
// std::net::TcpListener is blocking by default
// When you call accept(), the system call blocks
use tokio::net::TcpListener;
// tokio::net::TcpListener sets the socket to non-blocking mode
// Internally, it does something like:
// use std::os::unix::io::AsRawFd;
// fcntl(fd, F_SETFL, O_NONBLOCK);
// Non-blocking accept returns immediately:
// - With a connection if one is pending
// - With WouldBlock error if no connection is ready
// Tokio registers the socket with the reactor and waits for readinessTokio sets the underlying socket to non-blocking mode.
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// When you call listener.accept().await:
// 1. Tokio checks if a connection is ready (non-blocking check)
// 2. If ready: returns the connection immediately
// 3. If not ready: registers interest with the OS (epoll/kqueue/IOCP)
// 4. The Future yields, allowing other tasks to run
// 5. When the OS signals readiness, tokio wakes this task
// 6. The task resumes and accepts the connection
loop {
let (stream, addr) = listener.accept().await.unwrap();
println!("Connection from {}", addr);
}
}Tokio integrates with the OS event notification system.
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// Spawn multiple accept loops on the same listener
// Tokio handles this correctly with internal locking
for _ in 0..4 {
let listener = listener.clone(); // TcpListener can be cloned
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok((stream, addr)) => {
println!("Accepted from {}", addr);
}
Err(e) => eprintln!("Accept error: {}", e),
}
}
});
}
tokio::signal::ctrl_c().await.unwrap();
}Multiple tasks can share a TcpListener for parallel accept.
use tokio::net::TcpListener;
use tokio::time::{timeout, Duration};
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// Async timeout on accept
match timeout(Duration::from_secs(5), listener.accept()).await {
Ok(Ok((stream, addr))) => {
println!("Connection from {}", addr);
}
Ok(Err(e)) => {
eprintln!("Accept error: {}", e);
}
Err(_) => {
println!("No connection within 5 seconds");
}
}
}Timeouts work naturally with async accept.
use tokio::net::TcpListener;
use tokio::signal;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
tokio::spawn(async move {
// Accept loop can be cancelled by dropping the listener
loop {
match listener.accept().await {
Ok((stream, addr)) => {
println!("Connection from {}", addr);
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}
});
// Wait for Ctrl-C
signal::ctrl_c().await.unwrap();
println!("Shutting down...");
// When main exits, listener is dropped
// Accept operations will return errors
}Async code integrates cleanly with cancellation and shutdown.
use tokio::net::TcpListener;
use socket2::{Domain, Protocol, Socket, Type};
#[tokio::main]
async fn main() {
// Create socket with socket2 for advanced options
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
// Set socket options
socket.set_reuse_address(true).unwrap();
socket.set_nonblocking(true).unwrap();
// Bind and listen
socket.bind(&"127.0.0.1:8080".parse().unwrap()).unwrap();
socket.listen(128).unwrap();
// Convert to tokio TcpListener
let std_listener: std::net::TcpListener = socket.into();
let listener = TcpListener::from_std(std_listener).unwrap();
// Now use async accept with all the socket options
loop {
let (stream, addr) = listener.accept().await.unwrap();
println!("Connection from {}", addr);
}
}Use socket2 for advanced socket options, then convert to tokio.
use std::net::TcpListener;
// Synchronous iterator
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// incoming() returns an Iterator
// Each next() call blocks until a connection arrives
for stream in listener.incoming() {
let stream = stream.unwrap();
// Handle stream
}
}
// Asynchronous approach
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// No incoming() iterator - use accept() loop
loop {
let (stream, addr) = listener.accept().await.unwrap();
// Handle stream asynchronously
}
}Tokio's API doesn't provide an incoming() iterator.
// std::net::TcpListener with blocking:
// - One thread per concurrent accept
// - Each accept blocks an OS thread
// - Thread context switching overhead
// - Limited by number of threads
// tokio::net::TcpListener:
// - Single thread can handle thousands of connections
// - Non-blocking: no thread is idle while waiting
// - Event-driven: OS notifies when connections are ready
// - Scales to 10K+ concurrent connectionsAsync scales far better than thread-per-connection.
use tokio::net::TcpListener;
// tokio abstracts platform differences:
// - Linux: uses epoll
// - macOS/BSD: uses kqueue
// - Windows: uses IOCP
// All platforms get non-blocking behavior:
// - accept() yields until connection available
// - read/write on streams yield until data available
// - Runtime handles waking tasks when ready
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
// Same code works on all platforms
loop {
let (stream, addr) = listener.accept().await.unwrap();
}
}Tokio provides consistent async behavior across platforms.
| Aspect | std::net::TcpListener | tokio::net::TcpListener |
|--------|-------------------------|---------------------------|
| accept() behavior | Blocks thread | Returns Future |
| Usage in async code | Blocks entire runtime | Yields to runtime |
| Scalability | Limited by threads | Thousands of connections |
| Integration | Standalone | Requires tokio runtime |
| Socket mode | Blocking | Non-blocking |
| Thread requirement | One per accept | Shared across accepts |
tokio::net::TcpListener and std::net::TcpListener both bind to TCP ports and accept connections, but they interact with the system fundamentally differently:
std::net::TcpListener performs blocking I/O. When you call accept(), the OS thread sleeps until a connection arrives. In an async runtime, this is catastrophic—the thread can't run any other tasks while blocked. The entire executor on that thread grinds to a halt. Even with spawn_blocking, you're using dedicated threads for blocking operations, which defeats the purpose of lightweight async tasks.
tokio::net::TcpListener performs non-blocking I/O. When you call accept().await, tokio checks if a connection is immediately available. If not, it registers interest with the OS (via epoll/kqueue/IOCP) and yields. The runtime can run other tasks while waiting. When a connection arrives, the OS notifies tokio, which wakes the waiting task. This cooperative multitasking is what allows async runtimes to handle thousands of concurrent operations with a small number of threads.
Key insight: The difference isn't just about API style—it's about resource usage. Blocking I/O requires one OS thread per concurrent operation. Non-blocking I/O requires one small task per concurrent operation, with tasks sharing threads cooperatively. In async code, always use async I/O primitives; using blocking I/O will silently destroy your runtime's ability to make progress.