Networking with the Standard Library

TCP Listeners

A TcpListener (std::net::TcpListener) is a server-side TCP socket bound to a local IP address and port that listens for incoming client connection requests. Each accepted handshake produces a dedicated TcpStream for communicating with that client while the TcpListener continues listening for new connections.

                          +-----------------------+
Client 1 -- SYN/ACK ----> |     TcpListener       | ---> TcpStream 1 (Client 1)
Client 2 -- SYN/ACK ----> |  bound to 0.0.0.0:80  | ---> TcpStream 2 (Client 2)
Client 3 -- SYN/ACK ----> |   .accept() / .incoming()    TcpStream 3 (Client 3)
                          +-----------------------+

Binding a TcpListener

TcpListener::bind<A: ToSocketAddrs>(addr: A) creates a socket, binds it to the specified address, and puts it into the listening state:

  • Binding to "127.0.0.1:8080" accepts connections only from the local machine (loopback).
  • Binding to "0.0.0.0:8080" (or "[::]:8080") accepts connections arriving on any network interface of the host.
  • Binding to port 0 ("127.0.0.1:0") instructs the OS kernel to select an available ephemeral port automatically.

.accept() vs .incoming()

TcpListener provides two ways to receive incoming connections:

Method Signature Returns Best Used When
.accept() accept(&self) -> io::Result<(TcpStream, SocketAddr)> Both the connected TcpStream and the client's remote SocketAddr. You immediately need the client's IP and port for logging, rate-limiting, or access control.
.incoming() incoming(&self) -> Incoming<'_> An infinite iterator yielding io::Result<TcpStream>. You want a concise for stream in listener.incoming() server loop.

Note that iterating over listener.incoming() never returns None; each item is an io::Result<TcpStream> representing a single connection attempt (which may fail if the client resets the handshake or the process runs out of file descriptors).

use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let server_addr = listener.local_addr()?;

    // Spawn 3 client connections on a background thread
    let client_worker = thread::spawn(move || -> io::Result<()> {
        for id in 1..=3 {
            let mut stream = TcpStream::connect(server_addr)?;
            write!(stream, "Hello from client #{id}")?;
        }
        Ok(())
    });

    // Accept exactly 3 connections using `.incoming().take(3)`
    for (idx, stream_result) in listener.incoming().take(3).enumerate() {
        let mut stream = stream_result?;
        let mut msg = String::new();
        stream.read_to_string(&mut msg)?;
        println!("Accepted [{}]: {}", idx + 1, msg);
    }

    client_worker.join().expect("client thread panicked")?;
    Ok(())
}
Accepted [1]: Hello from client #1
Accepted [2]: Hello from client #2
Accepted [3]: Hello from client #3

Setting Time-To-Live (TTL) and Non-Blocking Mode

  • listener.set_ttl(ttl: u32) sets the IP TTL field on packets sent from the socket.
  • listener.set_nonblocking(true) switches the listener into non-blocking mode so listener.accept() returns Err(ErrorKind::WouldBlock) immediately when no pending client connection is waiting in the kernel accept queue.