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:
- I need a way to keep multiple references to the websocket, so I can talk to the client from multiple locations (read: threads).
- 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:
- In the
handle
(orstarted
) function of MyWs, pass out a reference to Context intoself.game
. This doesn't work because I'm moving out a mutable ref. - 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. - 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:
ws::start
but I run into a similar issue: pastebin.com/WDC99Uup. – Daniel Porteous