Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Recipe

Serve a line protocol over TCP with threads

Use when: clients send text commands, one per line, and each command gets a reply

Goal: a server that answers several clients at once, and a way to test it in one program

Taught in A TCP client and server

  1. 1

    TcpListener::bind("127.0.0.1:0") in examples and tests, or a configured address in production; read the real address with local_addr().

    Example

    The bed-temperature server starts with let listener = TcpListener::bind("127.0.0.1:0")?; and then let addr = listener.local_addr()?;. The operating system picks a free port, and addr is what each Console connects to.

    Why: port 0 can never collide with another program or test.

  2. 2

    for stream in listener.incoming() yields one TcpStream per client.

    Example

    for stream in listener.incoming() gives one item per client. let Ok(stream) = stream else { continue }; skips an accept that failed, and keeps serving the rest.

    Why: the listener keeps listening; each stream is one conversation.

  3. 3

    thread::spawn(move || handle(stream)), or send the stream to a fixed set of workers over a channel.

    Example

    With handle(stream) called right in the loop, client B's TEMP 2 gets (no reply within 200 ms), because the thread is busy with A. With thread::spawn(move || handle(stream)), the same program prints B: bed 2: 21.5 C.

    • a small, trusted set of clients — one thread per connection.
    • many or untrusted clients — a fixed number of workers, or async (Section 4).

    Why: the accept loop must get back to accept at once, or other clients wait.

  4. 4

    let mut writer = stream.try_clone()?; then BufReader::new(stream).

    Example

    handle starts with let mut writer = stream.try_clone()?; and then loops over BufReader::new(stream).lines(). The reader gets the original stream, and writer is the second handle.

    Why: the BufReader owns one handle for reading lines; the clone writes replies.

  5. 5

    for line in reader.lines(): parse the command, then writeln!(writer, ...).

    Example

    The line TEMP 1 comes in. writeln!(writer, "bed {}: 21.5 C", line.trim_start_matches("TEMP ")) sends back bed 1: 21.5 C, one line for one command.

    Why: lines are the framing, so each iteration is exactly one command however the bytes arrived.

  6. 6

    Stop on your quit command, or when lines() ends because the client closed the connection.

    Example

    On QUIT, handle writes BYE and breaks. handle returns, and both handles are dropped, which closes the connection. If the client closes first without QUIT, lines() simply ends, and the loop stops the same way.

    Why: dropping the stream closes the connection; read returning Ok(0) means the client left.

Common mistakes

  • Calling handle directly in the accept loop, so one client blocks all the others.
  • Treating each read as one whole command.
  • Reading from the raw stream after wrapping it in a BufReader, which skips buffered bytes.
  • A hard-coded port in tests, so tests running at once collide.