I've written a websocket server in Rust using actix. If anyone wants to check the full repo here https://github.com/fabracht/actix-websocket. Now, I want to integrate rabbitmq into the project. For that, I found the lapin crate https://docs.rs/lapin/1.8.0/lapin/. But I'm having problems integrating it with the actix framework. I would like to use my current implementation of the websocket to proxy the messages from rabbitmq back to the client.
This is the beginning of my attempt. Very early stages, so let me know if I'm going the wrong way, because now would be the time to change the approach. First some context: The websocket actor communicates with another actor that holds information about the sockets and the rooms.
pub struct WSConn {
pub id: Uuid,
room_id: Uuid,
hb: Instant,
lobby_address: Addr<Lobby>,
}
impl WSConn {
pub fn new(lobby: Addr<Lobby>, rabbit: Addr<MyRabbit>) -> Self {
Self {
id: Uuid::new_v4(),
room_id: Uuid::nil(),
hb: Instant::now(),
lobby_address: lobby,
}
}
fn hb(&self, context: &mut WebsocketContext<Self>) {
/// HEARTBEAT CODE GOES HERE ///
}
}
So, when the actor starts, I send a connect message to the Lobby, that handles all the logic for adding the connection to the Lobby struct.
impl Actor for WSConn {
type Context = WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
info!("Starting hearbeat");
self.hb(ctx);
let wsserver_address = ctx.address();
info!("A new client has connected with id {}", self.id);
self.lobby_address.send(Connect {
address: wsserver_address.recipient(),
room_id: self.room_id,
id: self.id
}).into_actor(self).then(|res, _, ctx| {
match res {
Ok(_) => (),
_ => ctx.stop()
}
fut::ready(())
}).wait(ctx);
}
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
info!("Stopping actor");
self.lobby_address.do_send(Disconnect {
room_id: self.room_id,
id: self.id
});
Running::Stop
}
}
Here's what the lobby looks like:
type Socket = Recipient<WSServerMessage>;
pub struct Lobby {
pub sessions: DashMap<Uuid, Socket>,//self id to self
pub rooms: DashMap<Uuid, DashSet<Uuid>>,//room id to list of users id
}
The entire Lobby code is quite big, so I won't put it here. Let me know if you need to see that and I'll provide the code. So, once the client connects, it gets assigned to the default room. When a client sends a message to the server, the message gets processed by the StreamHandler.
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSConn {
fn handle(&mut self, item: Result<Message, ProtocolError>, ctx: &mut Self::Context) {
match item.unwrap() {
ws::Message::Binary(bin) => ctx.binary(bin),
ws::Message::Ping(bin) => {
self.hb = Instant::now();
ctx.pong(&bin);
}
ws::Message::Pong(_) => self.hb = Instant::now(),
ws::Message::Close(reason) => {
ctx.close(reason);
ctx.stop();
}
ws::Message::Text(text) => {
let command = serde_json::from_str::<Command>(&text)
.expect(&format!("Can't parse message {}", &text));
info!("{:?}", command);
if command.command.starts_with("/") {
info!("This is a {} request", command.command);
match command.command.as_ref() {
"/join" => {
info!("Join Room {}", command.payload);
let uid = Uuid::from_str(command.payload.as_str().unwrap()).expect("Can't parse message {} to uuid");
self.lobby_address.send(Join {
current_room: self.room_id,
room_id: uid,
id: self.id
}).into_actor(self).then(|res, _, ctx| {
match res {
Ok(_) => (),
_ => ctx.stop()
}
fut::ready(())
}).wait(ctx);
self.room_id = uid;
},
_ => ()
}
} else {
info!("Text is {}", text);
self.lobby_address.do_send(ClientActorMessage {
id: self.id,
msg: command,
room_id: self.room_id,
});
}
}
_ => {
info!("Something weird happened. Closing");
self.lobby_address.do_send(Disconnect {
room_id: self.room_id,
id: self.id
});
ctx.stop();}
}
}
}
So, as you can see, if you send a message with the command /join and a payload with a valid uuidv4, you join the room. I'm only allowing the client to be a part of one room at a time. So, when you join one, you're automatically removed from the last one. Ok, so now let's talk about the rabbitmq connection. The way I thought about this was to use a connection pool to keep the rabbitmq connection and use that connection to create the channels. So I started by defining the struct that will hold my connection pool.
use actix::{Actor, Context, Handler, StreamHandler};
use deadpool_lapin::{Config, Pool, Runtime};
use deadpool_lapin::lapin::Error;
use lapin::message::Delivery;
use crate::lapin_server::messages::CreateChannel;
pub struct MyRabbit {
pub pool: Pool,
}
impl MyRabbit {
pub fn new() -> Self {
let mut cfg = Config::default();
cfg.url = Some("amqps://ghqcmhat:KbhPAA309QRg7TjdgFEV14pQRheoh44P@codfish.rmq.cloudamqp.com/ghqcmhat".into());
let new_pool = cfg.create_pool(Some(Runtime::Tokio1)).expect("Can't create pool");
MyRabbit {
pool: new_pool
}
}
}
impl Actor for MyRabbit {
type Context = Context<Self>;
}
Having this as an actor allows me to start the actor as I start the websocket server. This is done in main.
#[actix_web::main]
async fn main() -> std::io::Result<()>{
std::env::set_var("RUST_LOG", "actix_web=info,info");
env_logger::init();
// Start Lobby actor and get his address
let websocket_lobby = Lobby::default().start();
let rabbit = MyRabbit::new().start();
let application_data = web::Data::new(Appdata::new());
info!("Starting server on 127.0.0.1:8080");
let server = HttpServer::new(move || {
App::new()
.wrap(Logger::default())
.route("/ws/",web::get().to(websocket_handler))
.app_data(Data::new(websocket_lobby.clone()))
.app_data(Data::new(rabbit.clone()))
.app_data(application_data.clone())
});
server.bind("127.0.0.1:8080")?.run().await
}
To accommodate for the new actor, I added a new parameter to my request handler.
pub async fn websocket_handler(request: HttpRequest, stream: web::Payload, srv: Data<Addr<Lobby>>, rab: Data<Addr<MyRabbit>>, data: Data<Appdata>) -> Result<HttpResponse, Error> {
let mut counter = data.counter.lock().unwrap();
counter.add_assign(1);
info!("This is request # {}", counter);
let ws = WSConn::new(srv.get_ref().clone(), rab.get_ref().clone());
let response = ws::start(ws, &request, stream);
debug!("Response: {:?}", &response);
response
}
Now, my WSConn struct looks like this.
pub struct WSConn {
pub id: Uuid,
room_id: Uuid,
hb: Instant,
lobby_address: Addr<Lobby>,
rabbit_address: Addr<MyRabbit>
}
I know that, if I want to consume from a topic in rabbitmq, I need the exchange name, the type, the routing key and the queue name. So, I put these in a struct as well
pub struct Channel {
pub queue_name: String,
pub exchange_name: String,
pub exchange_type: ExchangeKind,
pub routing_key: String
}
impl Default for Channel {
fn default() -> Self {
Self {
queue_name: "".to_string(),
exchange_name: "".to_string(),
exchange_type: Default::default(),
routing_key: "".to_string()
}
}
}
But that's where I'm stuck. First, I'm not sure this deadpool_lapin is the right crate to use for this. I'm also not sure how to translate the example on lapin's page, which uses
async_global_executor::block_on
And spawns new threads using async_global_executor::spawn to consume messages.
So, again, what I want is to be able to proxy messages coming from the websocket to rabbitmq and vice versa. So, if a client connects to the websocket and sends a message like:
{
command: "SUBSCRIBE"
payload: "topic_name"
}
The result should be that messages published on that topic will get sent to him. Sending an UNSUBSCRIBE should undo that. Any help here would be greatly appreciated.
Please let me know if more information is needed.
Thank you
Fabricio