TCP Client and Server
Building a complete TCP client and server combines TcpListener on the server side with TcpStream, BufReader, and BufWriter on both sides to exchange framed messages concurrently across multiple clients.
+--------------------+ TCP Connection +--------------------+
| TCP Client | | TCP Server |
| | ---- "PING hello\n" -----------> | |
| BufWriter<Stream> | | BufReader<Stream> |
| BufReader<Stream> | <--- "PONG HELLO\n" ------------ | BufWriter<Stream> |
+--------------------+ +--------------------+
Thread-Per-Connection Echo Server and Client
In synchronous std::net networking, a server handles multiple concurrent clients by spawning a worker thread (or dispatching to a bounded thread pool) for each TcpStream returned by TcpListener::accept.
Because TcpStream implements Read and Write on &TcpStream, wrapping &stream in a BufReader for line-oriented reading (read_line) while writing responses directly to stream avoids cloning the OS file descriptor.
use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
fn handle_client(mut stream: TcpStream) -> io::Result<()> {
let mut reader = BufReader::new(&stream);
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
let trimmed = line.trim_end();
if trimmed == "QUIT" {
stream.write_all(b"BYE\n")?;
break;
}
let response = format!("ACK: {}\n", trimmed.to_uppercase());
stream.write_all(response.as_bytes())?;
line.clear();
}
Ok(())
}
fn run_client(server_addr: std::net::SocketAddr, name: &str) -> io::Result<Vec<String>> {
let mut stream = TcpStream::connect(server_addr)?;
let mut reader = BufReader::new(&stream);
let mut replies = Vec::new();
for cmd in [format!("hello from {name}\n"), "QUIT\n".to_string()] {
stream.write_all(cmd.as_bytes())?;
let mut reply = String::new();
reader.read_line(&mut reply)?;
replies.push(reply.trim_end().to_string());
}
Ok(replies)
}
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
// Server thread accepting 2 concurrent clients
let server_handle = thread::spawn(move || -> io::Result<()> {
let mut workers = Vec::new();
for stream in listener.incoming().take(2) {
let stream = stream?;
workers.push(thread::spawn(move || handle_client(stream)));
}
for worker in workers {
worker.join().expect("worker panicked")?;
}
Ok(())
});
// Launch two clients in parallel
let c1 = thread::spawn(move || run_client(addr, "alice"));
let c2 = thread::spawn(move || run_client(addr, "bob"));
println!("Alice received: {:?}", c1.join().unwrap()?);
println!("Bob received: {:?}", c2.join().unwrap()?);
server_handle.join().expect("server panicked")?;
Ok(())
}
Alice received: ["ACK: HELLO FROM ALICE", "BYE"]
Bob received: ["ACK: HELLO FROM BOB", "BYE"]
Avoiding Partial Reads and Writes
A single stream.write(buf) call is not guaranteed to transmit all bytes in buf if the kernel send buffer is nearly full, and a single stream.read(&mut buf) call returns whatever bytes have arrived so far rather than waiting for an entire logical message. Always:
- Use
write_all(orwriteln!followed byflush()) when sending bytes. - Frame messages with a delimiter (
BufRead::read_line) or a fixed-length header (Read::read_exact) when receiving bytes.