TCP
Listen on unused port TCP/IP
In this example, the port is displayed on the console, and the program will
listen until a request is made. TcpListener::bind uses a random port
allocated by the OS when requested to bind to port 0.
use std::net::TcpListener;
use std::io::{Read, Error};
fn main() -> Result<(), Error> {
let listener = TcpListener::bind("localhost:0")?;
let port = listener.local_addr()?;
println!("Listening on {}, access this port to end the program", port);
let (mut tcp_stream, addr) = listener.accept()?; //block until requested
println!("Connection received! {:?} is sending data.", addr);
let mut input = String::new();
let _ = tcp_stream.read_to_string(&mut input)?;
println!("{:?} says {}", addr, input);
Ok(())
}
TCP echo server
When you’re building a TCP client you need a server that behaves predictably. An echo server is that server: every byte you send comes back unchanged. Point your client at it to check framing, disconnect handling, or timeouts.
TcpListener::bind starts listening on an address. incoming yields each
new connection. Spawning a thread per connection keeps the accept loop free for
the next client. Inside the handler, read returning 0 means the peer
closed the connection — that’s the cue to drop out of the loop.
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
fn handle_connection(mut stream: TcpStream) -> io::Result<()> {
let mut buf = [0u8; 1024];
loop {
let n = stream.read(&mut buf)?;
if n == 0 {
return Ok(());
}
stream.write_all(&buf[..n])?;
}
}
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:7878")?;
println!("echo server listening on {}", listener.local_addr()?);
for incoming in listener.incoming() {
let stream = incoming?;
thread::spawn(move || {
if let Err(e) = handle_connection(stream) {
eprintln!("connection error: {e}");
}
});
}
Ok(())
}
TCP client
With the echo server from the previous recipe running on 127.0.0.1:7878, a
client can connect, send a message, and read the reply.
TcpStream::connect opens a connection. The returned TcpStream is both a
reader and a writer. write_all sends the request; read fills a buffer
with whatever the server returns. Echo guarantees the bytes round-trip
unchanged, so the client can assert on what came back.
use std::io::{self, Read, Write};
use std::net::TcpStream;
fn main() -> io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:7878")?;
stream.write_all(b"ping")?;
let mut buf = [0u8; 4];
stream.read_exact(&mut buf)?;
println!("echo: {}", std::str::from_utf8(&buf).unwrap_or("<binary>"));
assert_eq!(&buf, b"ping");
Ok(())
}
TCP connect with a timeout
Plain TcpStream::connect waits for the kernel to give up on SYN retries.
When the peer is down or a firewall is silently dropping packets, that can
take a minute or more. TcpStream::connect_timeout caps the wait.
The timeout variant takes a SocketAddr, not a string, so resolve the
hostname first.
use std::io;
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
fn main() -> io::Result<()> {
let addr = "example.com:443"
.to_socket_addrs()?
.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no address"))?;
match TcpStream::connect_timeout(&addr, Duration::from_secs(3)) {
Ok(_stream) => println!("connected to {addr}"),
Err(e) if e.kind() == io::ErrorKind::TimedOut => {
eprintln!("connect timed out");
}
Err(e) => return Err(e),
}
Ok(())
}
Set a read timeout on a TCP stream
A default TcpStream read blocks until data arrives or the peer hangs up.
If the peer goes silent — process stuck, network partition, slow upstream —
the read hangs with it. set_read_timeout caps the wait.
On timeout the read returns an io::Error. The platform decides the kind:
WouldBlock on Unix, TimedOut on Windows. Match both or your code
breaks on the other platform.
This example stands up a listener that accepts but never sends, then connects and reads with a short timeout to prove the timeout fires.
use std::io::{self, Read};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
// Accept the connection but never write to it.
thread::spawn(move || {
let _conn = listener.accept();
thread::sleep(Duration::from_secs(60));
});
let mut stream = TcpStream::connect(addr)?;
stream.set_read_timeout(Some(Duration::from_millis(50)))?;
let mut buf = [0u8; 16];
match stream.read(&mut buf) {
Ok(n) => println!("received {n} bytes"),
Err(e) if e.kind() == io::ErrorKind::WouldBlock
|| e.kind() == io::ErrorKind::TimedOut =>
{
println!("read timed out ({:?})", e.kind());
}
Err(e) => return Err(e),
}
Ok(())
}
Disable Nagle’s algorithm
TCP coalesces small writes by default — Nagle’s algorithm — to avoid
flooding the network with tiny packets. For interactive protocols that’s the
wrong tradeoff: a typed keystroke or a chat message can sit in the kernel
buffer for tens of milliseconds waiting for company. set_nodelay turns
Nagle off so each write goes out as its own segment.
Use it for chat, games, REPL-style protocols, and RPCs where round-trip latency matters more than packet count. Leave it on for bulk transfer.
use std::io::{self, Write};
use std::net::TcpStream;
fn main() -> io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:7878")?;
stream.set_nodelay(true)?;
stream.write_all(b"ping ")?;
stream.write_all(b"pong\n")?;
Ok(())
}
Half-close a TCP connection
Many request-then-response protocols expect the client to finish sending
before the server replies: HTTP/1.0, custom RPCs, anything that reads
until EOF. To say “I’m done sending, give me your reply” without losing the
ability to read, shut down only the write side with Shutdown::Write.
After the half-close the peer sees EOF on its read, can do its work, and
writes a response that you read normally. read_to_end keeps reading until
the peer closes its own write half.
use std::io::{self, Read, Write};
use std::net::{Shutdown, TcpStream};
fn main() -> io::Result<()> {
let mut stream = TcpStream::connect("example.com:80")?;
stream.write_all(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")?;
stream.shutdown(Shutdown::Write)?;
let mut response = Vec::new();
stream.read_to_end(&mut response)?;
println!("received {} bytes", response.len());
Ok(())
}
Non-blocking TCP accept
A normal accept blocks until a client arrives. If a single thread needs
to do other work between connections — poll a timer, watch another file
descriptor — flip the listener into non-blocking mode with
set_nonblocking. accept then returns immediately with an
io::Error of kind WouldBlock when nothing is pending.
This readiness pattern is the primitive every async runtime uses underneath.
For production code, reach for mio or an async runtime — what follows is
the manual version of what they automate.
use std::io::{self, ErrorKind};
use std::net::{TcpListener, TcpStream};
use std::thread;
use std::time::Duration;
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
listener.set_nonblocking(true)?;
// Spawn a client so the listener has something to accept.
thread::spawn(move || {
let _ = TcpStream::connect(addr);
});
let stream = loop {
match listener.accept() {
Ok((s, _)) => break s,
Err(e) if e.kind() == ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(e) => return Err(e),
}
};
println!("accepted from {}", stream.peer_addr()?);
Ok(())
}