deprecated-thing icon indicating copy to clipboard operation
deprecated-thing copied to clipboard

Simple Opinionated Networking in Rust

Sonr

Note: this is a work in progress and the api may change

Note: currently not on crates.io, use:

Build Status

[dependencies]
sonr = {git = "https://github.com/hagsteel/sonr" }


Simple Opinionated Networking in Rust


Sonr is built on top of Mio with the aim to make it easier to create networking applications.

The two main components of Sonr are the Reactor and the System.

A Reactor is anything that reacts to the output of another reactor, and has an input and an output. This makes it possible (and intended) to chain two reactors. Such a chain is in it self a Reactor, and can be chained further.

The System runs the reactors and handles the registration and re-registration of reactors (using mio::Poll::register).

System is thread local and has to exist for each thread using Reactors. There can only be one instance of a System per thread, and this instance is created by calling System::init(). Calling System::init() twice in the same thread will panic.

fn main() -> Result<(), sonr::errors::Error> {
    System::init()?

    let reactor_1 = Reactor1::new()?;
    let reactor_2 = Reactor2::new()?;
    let reactor_3 = Reactor3::new()?;

    let run = reactor_1.chain(reactor_2.chain(reactor_3));

    System::start(run)?;
    Ok(())
}

Reactor

The reactor trait consists of two types and a method:

trait Reactor {
    type Input;
    type Output;

    fn react(&mut self, reaction: Reaction<Self::Input>) -> Reaction<Self::Output>;
}

To chain one reactor with another the Output of the first one needs to be of the same type as the Input of the second one.

The following example shows the creation of a chain from two reactors. Since none of the reactors are responding to Events there is no need for the System to be initialised.

use sonr::prelude::*;

struct VecToString;

impl Reactor for VecToString {
    type Input = Vec<u8>;
    type Output = String;

    fn react(&mut self, reaction: Reaction<Self::Input>) -> Reaction<Self::Output> {
        use Reaction::*;
        match reaction {
            Value(bytes) => Value(String::from_utf8(bytes).unwrap()),
            Event(ev) => Event(ev),
            Continue => Continue,
        }
    }
}

struct UppercaseString;

impl Reactor for UppercaseString {
    type Input = String;
    type Output = String;

    fn react(&mut self, reaction: Reaction<Self::Input>) -> Reaction<Self::Output> {
        use Reaction::*;
        match reaction {
            Value(mut s) => {
                s.make_ascii_uppercase();
                Value(s)
            }
            Event(ev) => Event(ev),
            Continue => Continue,
        }
    }
}

fn main() {
    let input = "hello world".to_owned().into_bytes();

    let r1 = VecToString;
    let r2 = UppercaseString;
    let mut chain = r1.chain(r2);
    chain.react(Reaction::Value(input));
}

Since reactors in a chain will always push data forward and never return anything other than Reaction::Continue it is not possible to capture the output from chain.react(Raction::Value(input)); here.

To print the result in the above example it would be possible to create a third reactor that prints the Input, however there is a lot of boilerplate to create yet another reactor so in this case the map function of a reactor could be used.

Map

Updating the previous example to use map to print the output of UppercaseString:

fn main() {
    let input = "hello world".to_owned().into_bytes();

    let r1 = VecToString;
    let r2 = UppercaseString.map(|the_string| println!("{}", the_string));
    let mut chain = r1.chain(r2);
    chain.react(Reaction::Value(input));
}

The map function takes a closure as an argument, where the argument for the closure is the Output of the reactor. Since the output of UppercaseString is a String, the map function is called with a closure FnMut(UppercaseString::Output) -> T.

This is a convenient way to change the output of a reactor.

The ReactiveTcpListener outputs a tuple: (mio::net::TcpStream, SocketAddr). The map function could be useful here if the SocketAddr is not required:

use sonr::prelude::*;
use sonr::net::tcp::ReactiveTcpListener;

fn main() {
    System::init();

    let listener = ReactiveTcpListener::bind("127.0.0.1:8000")
        .unwrap()
        .map(|(stream, _socket_addr)| stream); // ignore the SocketAddr

    System::start(listener);
}

And

The last method on a Reactor is and. This is useful to run more than one (evented) reactor in parallel.

Since the output of a tcp listener is a stream and a socket address, it would not be possible to chain two tcp listeners together. It is also not possible to call System::start twice in the same thread.

To run two tcp listeners at the same time use and:

use sonr::prelude::*;
use sonr::net::tcp::ReactiveTcpListener;

fn main() {
    System::init();

    let listener_1 = ReactiveTcpListener::bind("127.0.0.1:8000").unwrap();
    let listener_2 = ReactiveTcpListener::bind("127.0.0.1:9000").unwrap();

    System::start(listener_1.and(listener_2));
}

This means a Reaction::Event(event) from the System will be sent to both listeners.