WebSockets in Actix Web Full Tutorial — WebSockets & Actors
By: Anthony Oleinik
Tags: Actix Web, Actors, Rust

Step Three: Defining Messages for our Mailboxes

create a new file called messages.rs . This file will hold all the "messages" that get put into our actor's mailboxes.

Messages are structs with the two traits: the first is simple, and is just #[derive(Message)] that tells us that it's an actor message. The second is rtype . That return type must be the same T we talked about in the end of the last section — the type that gets returned after the message is handled. So if we wanted to define a message that returned a string, it'd look like this:

#[derive(Message)]

#[rtype(result = "String")] // result = your type T

pub struct MyMessage; // they usually carry info, but not for this example



impl Handler<MyMessage> for MyActor {

type Result = String; // This type is T



fn handle(&mut self, msg: MyMessage, ctx: &mut Self::Context) -> String { // Returns your type T

...

}

}

And that’s it! defining messages is actually pretty easy. Here’s the code drop for the message file, with respective comments:

actix::prelude::{Message, Recipient};

use uuid::Uuid;



//WsConn responds to this to pipe it through to the actual client

#[derive(Message)]

#[rtype(result = "()")]

pub struct WsMessage(pub String);



//WsConn sends this to the lobby to say "put me in please"

#[derive(Message)]

#[rtype(result = "()")]

pub struct Connect {

pub addr: Recipient<WsMessage>,

pub lobby_id: Uuid,

pub self_id: Uuid,

}



//WsConn sends this to a lobby to say "take me out please"

#[derive(Message)]

#[rtype(result = "()")]

pub struct Disconnect {

pub room_id: Uuid,

pub id: Uuid,

}



//client sends this to the lobby for the lobby to echo out.

#[derive(Message)]

#[rtype(result = "()")]

pub struct ClientActorMessage {

pub id: Uuid,

pub msg: String,

pub room_id: Uuid

}

Defining messages for actors is super easy. One of my favorite parts about the actor framework is if you need to get data from one Actor to another, creating a message or adding data to an existing message is super easy.

Step Four: Defining the Lobby

First, the imports for lobby.rs :

crate::messages::{ClientActorMessage, Connect, Disconnect, WsMessage};

use actix::prelude::{Actor, Context, Handler, Recipient};

use std::collections::{HashMap, HashSet};

use uuid::Uuid;

We’re almost done! Now, we have the actual lobby to write. Like we’ve said, the lobby is an Actor, but the actor is a plain old struct. Here’s that struct (to be placed in lobby.rs ):

Socket = Recipient<WsMessage>;



pub struct Lobby {

sessions: HashMap<Uuid, Socket>, //self id to self

rooms: HashMap<Uuid, HashSet<Uuid>>, //room id to list of users id

}

We store the Socket as a simple recepient of a WsMessage. With this setup, we can easily navigate through all, some, or find a specific client.

As a helper, we’ll implement a default for the lobby:

Default for Lobby {

fn default() -> Lobby {

Lobby {

sessions: HashMap::new(),

rooms: HashMap::new(),

}

}

}

and now let’s write a helper that sends a message to a client.

Lobby {

fn send_message(&self, message: &str, id_to: &Uuid) {

if let Some(socket_recipient) = self.sessions.get(id_to) {

let _ = socket_recipient

.do_send(WsMessage(message.to_owned()));

} else {

println!("attempting to send message but couldn't find user id.");

}

}

}

This method takes a string and a id, and sends that string to a client with that Id (if it exists; if it doesn’t, it just prints something to console. You probably want to handle this accordingly by returning a result or similar.)

Making the lobby an actor

Ready for the shortest section in this whole tutorial? To make the lobby an actor, use this following code:

Actor for Lobby {

type Context = Context<Self>;

}

That’s it! We don’t care about any lifecycle of the Lobby. We only have one, and we only mount it when the app starts and remove it when the app closes. The rooms are just HashSets, so we don’t need to make them actors for this simple example.

Handling Messages

The lobby will get 3 types of messages: Connects, Disconnects, and WsMessage from the clients. Both come from the WsConn lifecycle methods from the actor trait. This part isn’t actix specific, but I’ll explain the meat of the code.

/// Handler for Disconnect message.

impl Handler<Disconnect> for Lobby {

type Result = ();



fn handle(&mut self, msg: Disconnect, _: &mut Context<Self>) {

if self.sessions.remove(&msg.id).is_some() {

self.rooms

.get(&msg.room_id)

.unwrap()

.iter()

.filter(|conn_id| *conn_id.to_owned() != msg.id)

.for_each(|user_id| self.send_message(&format!("{} disconnected.", &msg.id), user_id));

if let Some(lobby) = self.rooms.get_mut(&msg.room_id) {

if lobby.len() > 1 {

lobby.remove(&msg.id);

} else {

//only one in the lobby, remove it entirely

self.rooms.remove(&msg.room_id);

}

}

}

}

}

All we’re doing is responding to a disconnect message by either:

removing a single client from a room. send everyone else “UUID disconnected! If that client was the last in the room, remove the room entirely (so we don’t clog up our hashmap)

Next, we need to respond to the connection message. Again, nearly no logic that is actor specific:

Handler<Connect> for Lobby {

type Result = ();



fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {

// create a room if necessary, and then add the id to it

self.rooms

.entry(msg.lobby_id)

.or_insert_with(HashSet::new).insert(msg.self_id);



// send to everyone in the room that new uuid just joined

self

.rooms

.get(&msg.lobby_id)

.unwrap()

.iter()

.filter(|conn_id| *conn_id.to_owned() != msg.self_id)

.for_each(|conn_id| self.send_message(&format!("{} just joined!", msg.self_id), conn_id));



// store the address

self.sessions.insert(

msg.self_id,

msg.addr,

);



// send self your new uuid

self.send_message(&format!("your id is {}", msg.self_id), &msg.self_id);

}

}

All that is being done here is adding a socket and sending them messages.

Finally, we open the mailbox for clients to send messages to the lobby for the lobby to forward to clients.

Handler<ClientActorMessage> for Lobby {

type Result = ();



fn handle(&mut self, msg: ClientActorMessage, _: &mut Context<Self>) -> Self::Result {

if msg.msg.starts_with("\\w") {

if let Some(id_to) = msg.msg.split(' ').collect::<Vec<&str>>().get(1) {

self.send_message(&msg.msg, &Uuid::parse_str(id_to).unwrap());

}

} else {



self.rooms.get(&msg.room_id).unwrap().iter().for_each(|client| self.send_message(&msg.msg, client));

}

}

}

This checks if the message starts with a \w. If it does, we know it’s a whisper, and we send it to a specific client. If it’s not, we send it to all users in the room. (this is NOT production ready! It will panic if there is an invalid UUID after the \w, and will just fail silently if it’s just \w without anything after it.)

And that’s the lobby!

Final Step: Setting up the Route / Running the Server

First, we have to open up a route that lets us connect to the server. create a file called “start_connection.rs” and put in this route:

crate::ws::WsConn;

use crate::lobby::Lobby;

use actix::Addr;

use actix_web::{get, web::Data, web::Path, web::Payload, Error, HttpResponse, HttpRequest};

use actix_web_actors::ws;

use uuid::Uuid;





#[get("/{group_id}")]

pub async fn start_connection(

req: HttpRequest,

stream: Payload,

Path(group_id): Path<Uuid>,

srv: Data<Addr<Lobby>>,

) -> Result<HttpResponse, Error> {

let ws = WsConn::new(

group_id,

srv.get_ref().clone(),

);



let resp = ws::start(ws, &req, stream)?;

Ok(resp)

}

We define a route that just has a group id (should be a valid uuid) as a path param. Then, we create a new WsConn with a refrence to the Lobby (we register the lobby with actix web in the next step). Finally, we upgrade the request to a WebSocket request, and bam! we now have an open persistant connection.

Our last step is to register the Lobby as shared data so we can get it like we just did ( srv: Data<Addr<Lobby>> ). Your main.rs should look like the following:

ws;

mod lobby;

use lobby::Lobby;

mod messages;

mod start_connection;

use start_connection::start_connection as start_connection_route;

use actix::Actor;



use actix_web::{App, HttpServer};



#[actix_web::main]

async fn main() -> std::io::Result<()> {

let chat_server = Lobby::default().start(); //create and spin up a lobby



HttpServer::new(move || {

App::new()

.service(start_connection_route) //. rename with "as" import or naming conflict

.data(chat_server.clone()) //register the lobby

})

.bind("127.0.0.1:8080")?

.run()

.await

}

And boom! Our whole app now shares a single lobby. Here’s everything we wrote:

Lobbied chats Send private messages Send broadcast messages easily extensible!

All in actix web, using actors! To test the client, I’d use a simple websocket for either chrome or firefox. Open multiple tabs and send whispers or broadcasts in different lobbies!

Happy WebSocketing!