Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TCP

Listen on unused port TCP/IP

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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

std-badge cat-net-badge

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(())
}