๐ŸŽ“ A1Learner
๐Ÿ”

Rust Half-Close with shutdown

shutdown closes one direction of a stream and keeps the other open. Dropping a stream closes both directions at once.

Say "I am done writing"

One call closes only the writing direction:

stream.shutdown(Shutdown::Write).unwrap();

Writes now fail

The writing side is closed for good. A late write meets an error:

let late = client.write_all(b"again");
println!("late write kind = {:?}", late.unwrap_err().kind());

Reads still work

The same client can still read the reply. The half-close closed one direction only:

let mut reply = String::new();
client.read_to_string(&mut reply).unwrap();
println!("reply after half-close = {}", reply);

A Read shutdown answers at once

Close the reading direction and a read returns 0 bytes straight away. No waiting for the peer:

other.shutdown(Shutdown::Read).unwrap();
let mut buf = [0u8; 8];
let n = other.read(&mut buf).unwrap();
println!("read after Read shutdown = {}", n);

Example

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

fn main() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let server = thread::spawn(move || {
        let (mut stream, _) = listener.accept().unwrap();
        let mut received = String::new();
        stream.read_to_string(&mut received).unwrap();
        stream.write_all(received.to_uppercase().as_bytes()).unwrap();
    });

    let mut client = TcpStream::connect(addr).unwrap();
    client.write_all(b"hello").unwrap();
    client.shutdown(Shutdown::Write).unwrap();
    let mut reply = String::new();
    client.read_to_string(&mut reply).unwrap();
    println!("reply = {reply}");
    let late = client.write_all(b"again");
    println!("late write kind = {:?}", late.unwrap_err().kind());
    server.join().unwrap();
}

Key points

  • Shutdown::Write: sending ends on this side.
  • The peer reads your last bytes, then its reads return Ok(0)
  • Shutdown::Read: reading ends on this side.
  • Your own reads return Ok(0) at once
  • Shutdown::Both closes both directions.
  • The stream value itself still exists until you drop it
  • A write after a Write shutdown fails.
  • On Linux its kind is BrokenPipe
  • Use a Write half-close to say "request finished" on a connection where the reply is still coming.
  • TCP Basics used it as an ending rule

In this example, the client sends hello and then half-closes its writing side. The server sits inside read_to_string. The client's half-close is what ends that read, so the server knows the request is complete. It replies in capitals and ends. The client reads the reply with no trouble. Only then does it try one more write. That write fails with BrokenPipe, because the writing direction is closed for good.

Note

A shutdown is a message to the peer, not just local bookkeeping.

Your Write shutdown is the peer's end-of-stream.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

Your client sends a file, then waits for the server's verdict on the same connection. What follows the send?