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
andWrite
are traits which define methods for reading and writing data respectively.- Both the
main()
method and thehandle_incoming_connections()
method return aResult
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:
- 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. - 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. - 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:
- First we define a buffer, in our case a mutable array of size 1024. This used for reading data from the client.
- Next we define our response as a fixed string.
- In
loop { ... }
we keep processing data coming from the client. Using thematch
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.
- 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.