Networking with the Standard Library

Socket Address Resolution

Socket address resolution converts human-readable endpoints—such as "localhost:8080", ("example.com", 443), or already-parsed (IpAddr, u16) pairs—into concrete SocketAddr values ready for network system calls. Rust unifies both zero-cost address conversions and OS DNS lookups through the std::net::ToSocketAddrs trait.

Input implementing ToSocketAddrs
  |-- ("127.0.0.1", 8080)  ---> Parses IP directly (No DNS network call)
  |-- SocketAddr           ---> Yields itself immediately (No DNS network call)
  |-- "localhost:8080"     ---> Queries OS resolver (getaddrinfo)
                                  |
                                  v
                     Iterator<Item = SocketAddr>
                     e.g., [::1]:8080, 127.0.0.1:8080

Types Implementing ToSocketAddrs

Every networking entrypoint in std::net (TcpStream::connect, TcpListener::bind, UdpSocket::bind, UdpSocket::send_to) accepts a generic parameter A: ToSocketAddrs:

Input Type Example Triggers OS DNS Lookup?
SocketAddr / SocketAddrV4 / SocketAddrV6 SocketAddr::from(([127, 0, 0, 1], 80)) No
(IpAddr, u16) / (Ipv4Addr, u16) / (Ipv6Addr, u16) (Ipv4Addr::LOCALHOST, 8080) No
&[SocketAddr] &[addr_v6, addr_v4][..] No
(&str, u16) / (String, u16) ("localhost", 8080) Yes (unless &str is an IP literal)
&str / String "localhost:8080" or "127.0.0.1:8080" Yes (unless &str is an IP:port literal)

Resolving Hostnames with .to_socket_addrs()

A single hostname often resolves to multiple SocketAddr records—for example, both an IPv6 AAAA record ([::1]:80) and an IPv4 A record (127.0.0.1:80), or multiple round-robin load-balanced server IPs.

use std::io;
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};

fn main() -> io::Result<()> {
    // 1. Resolving a tuple with an IP literal (no network query)
    let mut literal_iter = ("127.0.0.1", 9000).to_socket_addrs()?;
    assert_eq!(
        literal_iter.next(),
        Some(SocketAddr::from((Ipv4Addr::LOCALHOST, 9000)))
    );

    // 2. Resolving "localhost:8080" via the OS resolver
    let resolved: Vec<SocketAddr> = "localhost:8080".to_socket_addrs()?.collect();
    println!("Resolved 'localhost:8080' to {} address(es)", resolved.len());
    for addr in &resolved {
        println!("  -> {} (loopback: {})", addr, addr.ip().is_loopback());
    }

    Ok(())
}
Resolved 'localhost:8080' to 2 address(es)
  -> [::1]:8080 (loopback: true)
  -> 127.0.0.1:8080 (loopback: true)

Multi-Address Fallback in TcpStream::connect

When you pass a value yielding multiple SocketAddr items (or a slice &[SocketAddr]) to TcpStream::connect, the standard library iterates through the resolved addresses in order, attempting to connect to each one until a connection succeeds.

Unlike TcpStream::connect, TcpStream::connect_timeout(&addr, dur) takes a single &SocketAddr because DNS lookup via getaddrinfo is a blocking C library call that does not support a portable timeout parameter. To connect to a hostname with a timeout, resolve the addresses via .to_socket_addrs() first and then iterate over them with TcpStream::connect_timeout:

use std::io;
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::time::Duration;

fn connect_host_with_timeout(endpoint: impl ToSocketAddrs, timeout: Duration) -> io::Result<TcpStream> {
    let mut last_err = None;
    for addr in endpoint.to_socket_addrs()? {
        match TcpStream::connect_timeout(&addr, timeout) {
            Ok(stream) => return Ok(stream),
            Err(err) => last_err = Some(err),
        }
    }
    Err(last_err.unwrap_or_else(|| {
        io::Error::new(io::ErrorKind::InvalidInput, "resolved to zero socket addresses")
    }))
}

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

    let stream = connect_host_with_timeout(("127.0.0.1", port), Duration::from_millis(200))?;
    println!("Connected to local port {} with timeout", stream.peer_addr()?.port());
    Ok(())
}
Connected to local port 54321 with timeout