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