2
votes

My question here is in the context of using actix-web with Rust.

Unfortunately I can't explain this without a somewhat hefty code example, so let me start with that.

struct MyWs {
    game: Arc<RwLock<Game>>,
}

impl Actor for MyWs {
    type Context = ws::WebsocketContext<Self>;
}

impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Text(text)) => {
                debug!("Echoing text with {:?}", text);
                self.game.write().unwrap().some_method();
                ctx.text(text)
            },
            _ => (),
        }
    }
}

struct Game {
    websockets: Vec<Arc<RwLock<MyWs>>>,
}

impl Game {
    pub fn new() -> GameWrapper {
        GameWrapper {
            websockets: vec![],
        }
    }

    pub fn add_websocket(&mut self, my_ws: Arc<RwLock<MyWs>>) {
        self.websockets.push(my_ws);
    }

    pub fn some_method(&mut self) {
        // Do something to influence internal state.
        self.push_state();
    }

    pub fn push_state(&self) {
        for w in self.websockets {
            // I'm not sure about this part, idk how to access the
            // WebsocketContext with which I can send stuff back to the client.
            let game_state = get_game_state_or_something();
            w.write().unwrap().ctx.text(self.game_state);
        }
    }
}

struct GameWrapper {
    pub game: Arc<RwLock<Game>>,
}

impl GameWrapper {
    pub fn new(game: Arc<RwLock<Game>>) -> GameWrapper {
        GameWrapper { game }
    }
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    let game = Arc::new(RwLock::new(Game::new()));
    let game_wrapper = RwLock::new(GameWrapper::new(game.clone()));
    let game_wrapper_data = web::Data::new(game_wrapper);
    HttpServer::new(move || {
        App::new()
            .app_data(game_wrapper_data.clone())
            .route("/play_game", web::get().to(play_game))
    })
    .bind(ip_port)?
    .run()
    .await
}

pub async fn play_game(
    req: HttpRequest,
    stream: web::Payload,
    game_wrapper: web::Data<GameWrapper>,
) -> impl Responder {
    let my_ws = MyWs { game: game_wrapper.game.clone() };
    let my_ws = Arc::new(RwLock::new(my_ws));
    let mut game = game_wrapper.game.write().unwrap();
    game.add_websocket(my_ws);
    let resp = ws::start(my_ws, &req, stream);  // This is the problem.
    let resp = match resp {
        Ok(resp) => resp,
        Err(e) => return HttpResponse::from_error(e),
    };
    debug!("Successfully upgraded to websocket");
    resp
}

Let me explain what I'm trying to do first. When I client connects, I establish a websocket with them. I need a list of these websockets, so when something changes in Game, I can push an update to all clients.

I bind the play_game function as the handler for the play_game route. In this function, I upgrade the HTTP get request to a websocket. IBefore that, I make a copy of an Arc+RwLock of a Game and pass it into MyWs, the websocket struct. You can see in the handle function of the MyWs impl of StreamHandler that I modify the Game (with the some_method function). This is fine so far.

Things explode when I try to get multiple references to the websocket. You can see in play_game that I call add_websocket, giving Game a reference to it, so it can push updates back to all clients when something changes. For example, after calling some_method, we would call push_updates. The problem with this, is ws::start doesn't take in an Arc, it must take in an Actor that impls StreamHandler with a WebSocketContext.

So my main two issues are:

  1. I need a way to keep multiple references to the websocket, so I can talk to the client from multiple locations (read: threads).
  2. I need some way to even do this. I'm not sure in actix how to actually send messages back to the client outside of the context of my MyWs actor. The framework passes in the WebSocketContext to handle, but I don't know how to get my hands on this myself.

My ideas for fixing this:

  1. In the handle (or started) function of MyWs, pass out a reference to Context into self.game. This doesn't work because I'm moving out a mutable ref.
  2. Make my own ws::start that can take a reference. I haven't tried this yet because it seems like I'd end up rewriting a lot.
  3. Somehow impl Actor and StreamHandler on an Arc, or my own struct with interior mutability / something that allows me to keep multiple references to it.

This doesn't really help me send messages back because I still don't know how to send messages back via the websocket outside of the context of the handle function.

Sorry for the length of this question. The tl;dr is, how do I get multiple references to a websocket in actix-web and send messages to the client with them?

Here are the relevant docs for each of the components I'm using:

1
I tried to break out ws::start but I run into a similar issue: pastebin.com/WDC99Uup.Daniel Porteous

1 Answers

0
votes

Okay so the solution to my dilemma here was unsurprisingly to change the way I was trying to solve this problem. Instead of holding multiple references to the websockets, what I really need is references to each of the actors that hold the websocket. I figure this is how you're meant to do it, given Actix is an actor framework.

This means the code should look like this:

impl Game {
    ...

    pub fn register_actor(&mut self, actor: Addr<MyWs>) {
        self.actors.push(actor);
    }
}

pub async fn play_game(
    req: HttpRequest,
    stream: web::Payload,
    game_wrapper: web::Data<GameWrapper>,
) -> impl Responder {
    let my_ws = MyWs { game: game_wrapper.game.clone() };
    let my_ws = Arc::new(RwLock::new(my_ws));
    let mut game = game_wrapper.game.write().unwrap();
    let res = ws::start_with_addr(my_ws, &req, stream);
    let (addr, resp) = match res {
        Ok(res) => res,
        Err(e) => return HttpResponse::from_error(e),
    };
    game_manager.register_actor(handle, addr);
    debug!("Successfully upgraded to websocket");
    resp
}

You can then send messages to the actor instead via the Addr<MyWs>.

I'm going to leave the question for a while in case others have ideas for how to do this whole thing better.