Skip to Content
Course content

103: TcpListener and TcpStream

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of TcpListener and TcpStream like a hotel receptionist. The receptionist doesn't actually go on vacation with the guests or carry their luggage; their entire job is to sit at a desk, wait for someone to walk through the front door, and then hand that person off to a bellhop who can actually help them. The receptionist is the "Listener," and the conversation between the bellhop and the guest is the "Stream."

In Rust, it works exactly like that. You don't use a TcpListener to send data. You use it to wait for a connection. Once a connection is established, the listener gives you a TcpStream, which is the actual bidirectional pipe you use to read and write bytes.

Setting up the front desk

Before you can accept connections, you have to "bind" your listener to a specific address and port. This is basically telling the operating system, "Any traffic coming in on port 7878 belongs to me."

use std::net::TcpListener;

fn main() {
    // We bind to localhost on port 7878. 
    // If another app is using this port, this will panic.
    let listener = TcpListener::bind("127.0.0.1:7878").expect("Could not bind to port");
    println!("Listening for connections...");
}

I usually use expect here because if you can't bind to your port, your server is dead in the water anyway. There's no point in trying to recover gracefully if you can't even open the front door.

Handing off the guest

Now that the listener is active, you need to actually handle people arriving. The incoming() method returns an iterator that blocks the current thread until a client connects. When it does, it yields a TcpStream.

for stream in listener.incoming() {
    match stream {
        Ok(stream) => {
            println!("New connection established!");
            handle_connection(stream);
        }
        Err(e) => {
            eprintln!("Connection failed: {}", e);
        }
    }
}

Notice how I'm calling a separate function, handle_connection. If you put all your logic inside that for loop, your server can only handle one person at a time. While you're talking to one guest, everyone else is stuck standing in the lobby. In a real production app, you'd likely spawn a thread or use async for each stream, but for now, let's focus on the communication itself.

Talking through the pipe

The TcpStream implements the Read and Write traits. This means you treat it similarly to how you'd treat a file. You read bytes into a buffer and write bytes back out. One thing to keep in mind: TCP is a stream of bytes, not a stream of "messages." There are no inherent boundaries. If a client sends "Hello", you might receive "Hel" and then "lo" in two separate reads.

Here is a complete example of a server that reads a request and sends back the number of bytes it received—a simple "Echo-Length" server.

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

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024]; // A 1KB buffer to hold incoming data

    // Read data from the stream into our buffer
    match stream.read(&mut buffer) {
        Ok(size) => {
            let received_data = &buffer[..size];
            let response = format!("I received {} bytes of data from you!", size);
            
            // Write the response back to the client
            if let Err(e) = stream.write_all(response.as_bytes()) {
                eprintln!("Failed to send response: {}", e);
            }
        }
        Err(e) => eprintln!("Failed to read from stream: {}", e),
    }
}

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    for stream in listener.incoming() {
        if let Ok(s) = stream {
            handle_connection(s);
        }
    }
}

I used write_all instead of write here. I highly recommend this. write doesn't guarantee that the entire buffer is sent in one go; write_all keeps pushing until everything is gone or an error occurs. It saves you from writing a manual loop to ensure the full message was sent.




📋 Practical Task

Build a Remote System-Status Ping Server

Your task is to create a server that acts as a status monitor. Instead of echoing the length of the message, the server should respond based on the content of the message received.

  • Requirement 1: The server must bind to 127.0.0.1:8080.
  • Requirement 2: If the client sends the exact string "STATUS", the server should respond with "System Operational".
  • Requirement 3: If the client sends "TIME", the server should respond with "Current time: [Insert any dummy time string here]".
  • Requirement 4: For any other input, the server should respond with "Unknown Command".
  • Requirement 5: Ensure you are using std::io::Read and std::io::Write to handle the stream.

Hint: Remember that the buffer may contain trailing null bytes or whitespace. You'll want to convert the slice of the buffer you actually read into a string and trim it before comparing.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.