Networking with the Standard Library

TCP Streams

A TcpStream (std::net::TcpStream) represents an active, bidirectional, reliable byte stream connection between a local socket and a remote peer over TCP. It implements both std::io::Read and std::io::Write, allowing applications to read and write network data using the exact same traits and buffered wrappers (BufReader, BufWriter) used for files.

Local Endpoint (local_addr)                      Remote Peer (peer_addr)
+-------------------------+                      +-------------------------+
|       TcpStream         | === Outbound ===>    |       TcpStream         |
|  impl Read + Write      | <=== Inbound ====    |  impl Read + Write      |
+-------------------------+                      +-------------------------+

Establishing and Inspecting a TcpStream

A TcpStream is created either on the client side via TcpStream::connect (or TcpStream::connect_timeout) or on the server side via TcpListener::accept.

Method Signature Description
TcpStream::connect(addr) connect<A: ToSocketAddrs>(addr: A) -> io::Result<TcpStream> Initiates a TCP handshake with addr (trying each resolved address until one succeeds).
TcpStream::connect_timeout(&addr, dur) connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result<TcpStream> Initiates a TCP connection with an upper time bound.
.peer_addr() peer_addr(&self) -> io::Result<SocketAddr> Returns the remote socket address of the connected peer.
.local_addr() local_addr(&self) -> io::Result<SocketAddr> Returns the local socket address to which this stream is bound.
.shutdown(how) shutdown(&self, how: Shutdown) -> io::Result<()> Closes the read half, write half, or both halves of the TCP connection.
.set_nodelay(nodelay) set_nodelay(&self, nodelay: bool) -> io::Result<()> Enables TCP_NODELAY (disabling Nagle's algorithm for low-latency writes).
.peek(&mut buf) peek(&self, buf: &mut [u8]) -> io::Result<usize> Copies incoming bytes from the kernel receive buffer without consuming them.
.try_clone() try_clone(&self) -> io::Result<TcpStream> Duplicates the OS socket descriptor so two handles share the same TCP connection.

Reading, Peeking, and Half-Closing a Stream

Calling stream.shutdown(Shutdown::Write) sends a TCP FIN packet to the remote peer, signaling that the sender has finished transmitting data (so the peer's read returns 0 / EOF), while keeping the receive half open to read the peer's response.

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

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

    let mut client = TcpStream::connect(server_addr)?;
    client.set_nodelay(true)?;
    println!("TCP_NODELAY enabled: {}", client.nodelay()?);

    let (mut server, _) = listener.accept()?;

    // Client sends request and half-closes its write side
    client.write_all(b"GET /status")?;
    client.shutdown(Shutdown::Write)?;

    // Server peeks at the first 3 bytes without removing them from the buffer
    let mut peek_buf = [0u8; 3];
    let peeked = server.peek(&mut peek_buf)?;
    println!("Server peeked ({peeked} bytes): {:?}", std::str::from_utf8(&peek_buf).unwrap());

    // Server reads the full stream (including the peeked bytes) until client EOF
    let mut request = String::new();
    server.read_to_string(&mut request)?;
    println!("Server read full request: {request:?}");

    // Client can still read the server's reply even after shutting down its write half!
    server.write_all(b"200 OK")?;
    drop(server); // Closing server sends EOF to client

    let mut response = String::new();
    client.read_to_string(&mut response)?;
    println!("Client received response: {response:?}");

    Ok(())
}
TCP_NODELAY enabled: true
Server peeked (3 bytes): "GET"
Server read full request: "GET /status"
Client received response: "200 OK"

Splitting a TcpStream for Full-Duplex Threads

Because Read and Write are also implemented for shared references &TcpStream, two scoped threads can read from and write to &stream simultaneously, or you can call stream.try_clone() to obtain an independent owned TcpStream handle pointing to the same underlying kernel socket.