Help with server design

Hello all

Rust noob trying to find better ways to do things.
To teach myself the language, I am writing an exchange: a socket server that accepts multiple connections, which in turn send orders that match or are put into order books. In order to learn fundamentals, I'm trying to use as few libraries as possible other than std.
After a lot of effort, I came up with a thing that works, but strikes me as ugly.
I've found that in order to conform to my C++ way of doing things, I've been compelled to use the Rc<RefCell<X>> pattern everywhere, which somehow seems un-idiomatic?
I couldn't find an equivalent to C++'s shared_from_this so I've also been forced to do some stuff I'd rather not to "register" my connections with the Exchange object in a circular dependency.

Here's a shorthand outline of my code:

trait ReactorListener {
    fn on_readable(&mut self);
}
trait ClientGateway {
    // ...
}
struct Exchange {
    clients: HashMap<u16, Rc<RefCell<ClientGateway>>,
    // plenty of state ...
}
impl Exchange {
    fn do_something(&mut self, thing_to_do: data) {
        // maybe call a mutable method on one of the clients
    }
}
struct GatewayImpl {
    socket: TcpStream,
    exchange: Rc<RefCell<Exchange>,
}
impl ClientGateway for GatewayImpl {
    // ...
}
impl ReactorListener for GatewayImpl {
    fn on_readable(&mut self) {
        self.socket.read(data);
        self.exchange.borrow_mut().do_something(data);
    }
}
struct Reactor {
    control_blocks: HashMap<u64, Rc<RefCell<ReactorListener>>,
    // ...
}
fn main() {
    let exchange = Exchange{};
    let reactor = Reactor {};
    reactor.listen("0.0.0.0:8086",
        |sock| 
        let gw = Rc::new(RefCell::new(GatewayImpl::new(sock, exchange.clone())));
        exchange.register_new_client(gw);
    );
    loop { reactor.epoll() }
}

Why does this feel so hard and verbose? I feel there is a lot of shared mutability baked into the problem, and so maybe there's no other way than these Rc<RefCell<X>> all over the place.
Is there a rustier way of designing this? a pattern that isn't clear to developers from other less safe languages?

Thanks for any insight or constructive criticism.

Something you've got to keep in mind is that common patterns that were idiomatic in another language may not be idiomatic in Rust (and vice versa).

As you've identified, the pervasive shared mutable state in your design is a point of friction here. Having circular dependencies is also a pretty strong code smell in Rust because it means your ownership story is less well defined.

If it were me, I'd refactor the application to have less nesting and shared references. Maybe instead of taking your normal OO-based approach, you should try something more functional? Alternatively, instead of using shared memory for communication (i.e. the Rc<RefCell<dyn ClientGateway>>) you could communicate between components by using message passing/channels and concurrency.


@alice, as a side note, this implementation is quite close to the "IO resource within a mutex" thing you mentioned a while back, isn't it?

1 Like

Yes, a RefCell is basically a single-threaded Mutex (well a RwLock), so they are doing exactly the thing I did not recommend where they put a TcpStream inside a lock and inside a collection.

With async, you could work around these issues by using actors and multiple async tasks (one per connection, plus one for the exchange). You may be able to do something similar with std threads, but it's much harder because you don't have the ability to cancel stuff, which async gives you. An example of that pattern can be found here, where I implement a chat server.

1 Like

Thanks to both of you.

I specifically wanted to avoid using tokio as it seemed to mask a lot of things I wanted to understand, I also still haven't gotten to what the async/await keywords do. I suppose now is the time to look into this.

If I understand the general suggestion tho: the idea is to have queues between the reactor and the socket, and the socket and the "server" and some kind of scheduling mechanism to pop from/process these queues. That would be how I'd solve this problem with multiple threads.
I guess the part I don't understand here is: doesn't this just move the shared mutable state to the queue? Both the producer and the consumer need to be able to modify it, so I'd still need a Rc<RefCell<MyQueue>>, right?

As a side note, I'm not at all sure how I would refactor this to be more functional. I have an idea how I would do it in C, and I suppose using file descriptors instead of a stateful TcpStream would obviate some of my shared mutability issues.

No, you would not share anything when using channels. Instead, they would send messages to each other and the owner would perform the changes based on those messages.

Regarding TcpStream, this type is not stateful. It is merely a wrapper around the fd.

Thanks for your response.

No, you would not share anything when using channels. Instead, they would send messages to each other and the owner would perform the changes based on those messages.

Ok, I suppose I'm missing something here: isn't the channel shared?

Regarding TcpStream , this type is not stateful. It is merely a wrapper around the fd.

Ah, forgive my ignorance. I had seen that the read add write method implentations for TcpStream required a mutable reference. I viewed this (incorrectly) as some sort of statefulness. It still seems to mean that I can't hold more than one reference to it in order to use it. OTOH, if i used a file descriptor as a "connection reference", i can call libc::write or libc::read from more than one source. This also strikes me as not dealing with my root problem which appears to be design-related.

When you create a channel, you get two “ends” of it: a sender and a receiver. Each is independently owned. You can think of there being an underlying shared channel object, but you don't ever see that directly (just like an OS-level pipe).

The Write trait requires &mut access to whatever the trait is implemented on. But there isn't just a trait implementation for TcpStream; there's also one for &TcpStream. Therefore, you can take an & reference to a TcpStream and write to it. This trick exists throughout the standard library — traits can be implemented for references to a type (and Box and Rc too) as well as the type itself, and this allows flexibility where an operation might need mutability of the underlying data, or might not.

The channel has some internal shared state, but it's not visible to you. The important point is that the senders and receivers are owned by a single task. Note that senders can be cloned to obtain multiple senders to the same channel.


One thing you could attempt is to use the mio crate. It is just a thin wrapper around the underlying epoll API, which you would be using if you wrote it in C. It is also what Tokio uses internally to efficiently implement IO, and the fact that Tokio is using it is what allows you to cancel stuff when using Tokio.

When you create a channel, you get two “ends” of it: a sender and a receiver.

Ah yes, ofc. I forgot about this detail.
I am now curious as to how this is implemented. Will make for some interesting reading.

But there isn't just a trait implementation for TcpStream ; there's also one for &TcpStream .

Whoa, ok. This seems to be an important language feature that I don't remember being covered in the introductory rust book at all (?) and one that I should study.

Thanks!

Ok, I think I can handle examining the code of a "thin" wrapper of a thing I'm familiar with to see how it handles what i'm trying to do. Thanks for the suggestion.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.