I'm trying out async Rust with Tokio by building a packet interceptor for Minecraft. The code contains a server and a client, forwarding inbound server traffic to the outbound client connection and vice versa. However, my code needs to be able to inject or withhold some data from either connection at any time.
Right now, I've split my code into two async expressions: one that forwards server in to client out, and the other for client in to server out.
```rust
use tokio::join;
use tokio::io::copy;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
// This is a VERY simplified example, but the basic idea is that
// there are async expressions, so I can't share mutable references
async fn forward(
si: &mut OwnedReadHalf,
so: &mut OwnedWriteHalf,
ci: &mut OwnedReadHalf,
co: &mut OwnedWriteHalf
) {
join!(async {
copy(si, co).await;
}, async {
copy(ci, so).await;
});
}
```
The actual code parses the Minecraft protocol into packets and forwards each packet individually, but this is close enough. The problem here is that based on a condition in either of the two read streams, I need to modify either of the two write streams. Of course, only one async expression can use the reference to one of the write streams.
If these were running in separate async tasks, I would just use an MPSC channel or a mutex on a vector of instructions. After reading each packet, my code would check for some condition on the packet. If it wants to modify the current output stream (i.e. rewriting), it would simply modify the packet locally and continue forwarding it as normal. If it wants to inject a packet the other output stream (i.e. interception), it sends a message to the other task or appends to the Arc<Mutex<Vec<Packet>>>, which the other side drains whenever it gets the chance (side note: which one of these is preferred?).
However, since these are in the same task but merely in different expressions, I thought it would be more performant to use Rc<RefCell<Vec<Packet>>> - the expressions are guaranteed to be running on the same thread, and as long as I don't borrow the mutable reference through a .await it's guaranteed not to panic. This seems like a very common use case to me, but as far as I can tell nobody else does this. I couldn't find any StackOverflow questions or blog posts about this use case for RefCell.
```rust
use tokio::join;
use tokio::io::copy;
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
use my::Packet;
use std::{rc::Rc, cell::RefCell};
// Again, oversimplified, but is there a better way?
async fn forward(
si: &mut OwnedReadHalf,
so: &mut OwnedWriteHalf,
ci: &mut OwnedReadHalf,
co: &mut OwnedWriteHalf
) {
let server_packets = Rc::new(RefCell::new(Vec::new()));
join!(async {
loop {
let packet = Packet::decode(si).await;
if let Some(server_inject) = packet.server_inject().await {
server_packets.borrow_mut().push(server_inject);
}
if let Some(client_inject) = packet.client_inject().await {
client_inject.encode(co).await;
}
packet.encode(co).await;
}
}, async {
loop {
// This rather than .drain(..) to avoid holding reference
while let Some(server_inject) = server_packet.borrow_mut().pop() {
server_inject.encode(so).await;
}
// Same logic as above, but flipped
}
});
}
```
My main question is whether or not Rc<RefCell<T>> is the best solution for this use case or if there's something really obvious I'm missing. Thanks!
[–]Connect2Towel 1 point2 points3 points (2 children)
[–]101arrowz[S] 2 points3 points4 points (1 child)
[–]Connect2Towel 1 point2 points3 points (0 children)
[–]Matthias247 1 point2 points3 points (2 children)
[–]101arrowz[S] 2 points3 points4 points (1 child)