Rust Reactor: Brewing Up Fun with Asynchronous Magic!

Photo by Pixabay: https://www.pexels.com/photo/white-switch-hub-turned-on-159304/

Introduction

There are many ways to handle incoming events. If you need to be able to handle many potential service requests concurrently, the Reactor pattern could be your pattern of choice.

The key ingredient of this pattern is a so-called event-loop, which delegates request to the designated request handler.

Through the use of this mechanism, rather than some sort of blocking I/O, this pattern can handle many I/O requests simultaneously with optimal performance. Also modifying or expanding the available request handlers is quite easy.

This pattern strikes a balance between simplicity and scalabilty, and therefore it has become a central element in some server and networking software applications.

Implementation in Rust

In this example we will implement a very simple webserver, which uses an event loop to handle requests.

We will start by importing the following:

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

Some notes:

  • Read and Write are traits which define methods for reading and writing data respectively.
  • Both the main() method and the handle_incoming_connections() method return a Result enum. We use this to handle possible errors.
  • The TcpListener is used to create a listener on a specific port, and to listen on that port for incoming TCP connections.
  • We use TcpStream to represent a TCP-connection
  • The std::thread crate is used to import functionality for working with threads.

The main() function

The return type of the main() function is a Result<()> which indicates the function might at some point return an error. Let’s have a look:

fn main() -> Result<()> {
    let listener = TcpListener::bind("127.0.0.1:8080")?;
    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(|| {
                    handle_incoming_connections(stream).unwrap_or_else(|error| eprintln!("{:?}", error));
                });
            }
            Err(e) => {
                eprintln!("Failed to establish a connection: {}", e);
            }
        }


    }
    Ok(())
}

Line by line:

  1. We start by setting up a listener, on our localhost using port 8080. We use the ? instead of proper error handling because the ? operator propagates the error so our main function will return the error. If setting up the listener causes an error, the program needs to exit anyway.
  2. After setting up the listener we iterate over incoming connections. If the connection is succesfully made, we spawn a thread handling this connection, if not we print an error message. Using thread::spawn we define the work to be done in a closure.
  3. When the loop is done, we complete the function by return an Ok(())

The handle_incoming_connections() method

This function takes a mutable tcp stream as an input, and return a Result<()> output, and implements the way the server interacts with each connected client.

This is the code:

fn handle_incoming_connections(mut stream: TcpStream) ->Result<()>{
    let mut buffer=[0;1024];
    let response="HTTP:1.1 200 OK\r\n\r\nHello from Rust\r\n\r\n";
    loop {
        match stream.read(&mut buffer) {
            Ok(0)=>{
                break;
            }
            Ok(n)=>{
                println!("Received {} bytes", n);
                stream.write_all(response.as_ref()).unwrap();
                break;
            }
            Err(e)=>{
                eprintln!("Failed to handle connection. Error: {}", e);
                return Err(e);
            }
        }
    }

    Ok(())
}

Line by line:

  1. First we define a buffer, in our case a mutable array of size 1024. This used for reading data from the client.
  2. Next we define our response as a fixed string.
  3. In loop { ... } we keep processing data coming from the client. Using the match statement each of these three things can happen:
    • Ok(0) : If the program reads zero bytes, the client has apparently closed the connection and the loop breaks.
    • If we get an Ok(n) it means n bytes have been read succesfully, we print the number of bytes, and we return our response.
    • In case of an Err(e) we print an error message and the function returns an error.
  4. If the loop ends without an error, we return an Ok(()).

As you can see the event loop is in our case based in our main function, where we constantly handle incoming streams.

Conclusion

This is a very simplified example, on purpose. Implementing a web server usuallly is much more complicated but to demonstrate the Reactor Pattern it serves its purpose. Enhancements would be to have it handle different request or larger data transfers.

Leave a Reply

Your email address will not be published. Required fields are marked *