Rocket icon indicating copy to clipboard operation
Rocket copied to clipboard

Native WebSocket support

Open net opened this issue 7 years ago • 55 comments

I think Rocket would benefit heavily from native WebSocket support. Even better would be something similar to Phoenix's channels.

It's great to see such a comprehensive web framework for Rust coming together!

net avatar Jan 01 '17 05:01 net

@net did you manage to combine rocket with eg. websocket on the same host:port? I'm trying to upgrade connection to ws, but no luck so far :(

jsen- avatar Jan 02 '17 19:01 jsen-

This is indeed planned.

SergioBenitez avatar Jan 03 '17 03:01 SergioBenitez

@SergioBenitez got any suggestions for how one could manually add web socket support?

wagenet avatar May 01 '17 23:05 wagenet

@wagenet Do you mean to Rocket or to your web application? The former is subtle is it requires a lot of design work, but the latter should be straightforward. You should be able to use the websocket or ws-rs crates as they are intended. You won't be able to use the same port for both servers, of course. And, you'll need to spawn one of the two servers in a separate thread so that one server doesn't block the other one from starting. Should you choose to do this, your main function would look something like:

fn main() {
    thread::spawn(|| {
        listen("127.0.0.1:3012", |out| {
            move |msg| {
                out.send(msg)
           }
        })
    })

    rocket.ignite()..more()..launch()
}

SergioBenitez avatar May 29 '17 20:05 SergioBenitez

@SergioBenitez thanks :) I ended up discovering this sort of approach myself.

wagenet avatar May 30 '17 21:05 wagenet

@SergioBenitez is this waiting on a future version of Rocket that migrates to Actix or some other async framework?

jhpratt avatar Feb 20 '19 02:02 jhpratt

Is there no way to add web socket support on the same port as HTTP?

If not, this is quite problematic for me.

sparky8251 avatar Feb 24 '19 03:02 sparky8251

@sparky8251 Not currently, at least easily.

jhpratt avatar Feb 24 '19 04:02 jhpratt

Well, that sucks. Liked the readability of Rocket but without the ability to have websockets on the same port Rocket is useless to me.

Actix just doesnt have the same readability after all...

If you happen to know a way to do websockets on the same port, even if its not clean, I'd love to hear it. If I end up porting to Actix I won't be returning to Rocket but if I can make the websocket stuff work, hopefully I can stick around until native support lands.

sparky8251 avatar Feb 24 '19 04:02 sparky8251

@sparky8251 nginx ("Since version 1.3.13") should be able to forward to a second server on a per-location basis. The second server could even be another listening port connected to the same binary that's serving the rocket HTTP server (e.g. via https://github.com/SergioBenitez/Rocket/issues/90#issuecomment-304725055).

I've made an example (completely untested so far, sorry) based on the nginx example for WebSocket:

http {
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    server {
        listen 80 default server;
        server_name example.com;

        location /websocket/ {
            proxy_pass http://localhost:8080;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }

        location / {
            proxy_pass http://localhost:8000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
        }
    }

I presume something similar can be done with Apache as well.

jebrosen avatar Feb 24 '19 17:02 jebrosen

Unfortunately, using nginx is a non-answer for my use case. I'd like to not need a 3rd party program to make this work.

sparky8251 avatar Apr 07 '19 19:04 sparky8251

Hyper 0.12 supports websockets: https://github.com/hyperium/hyper/blob/master/examples/upgrades.rs

Rocket is still on hyper 0.10 though, so we'll need to upgrade that dependency first

Edit: which seems blocked on 0.5.0: https://github.com/SergioBenitez/Rocket/issues/17#issuecomment-415609486

VictorKoenders avatar May 01 '19 12:05 VictorKoenders

With #1008 merged, would it make sense to start implementing websocket support on the async branch?

fenhl avatar Aug 30 '19 17:08 fenhl

I think we would need to see some progress on #1066 first - which reminds me, I need to write a bit more about that. And WebSocket support is also big enough that I'd like to see a hypothetical API design before going too far into implementation.

jebrosen avatar Aug 30 '19 18:08 jebrosen

WebSockets will certainly be one of the larger issues. I'm actually looking into the two main websocket crates (ws and websocket). ws is, frankly, a bit of a mess, and will be extremely difficult to upgrade to asynchronous. websocket already has support, though it's running on tokio 0.1 and hyper 0.10. I've just asked on an issue there to see what sort of changes will be accepted.

While this wouldn't directly add websocket support to Rocket, it would allow for an asynchronous server that could be spawned on the same runtime.

jhpratt avatar Aug 31 '19 00:08 jhpratt

Actually, I don't think #1066 is necessary for this after all - it would help with SSE, but not websockets. Instead, websockets would likely have to be handled at the hyper-service or connection level with an API such as https://docs.rs/websocket/0.23.0/websocket/server/upgrade/async/trait.IntoWs.html.

jebrosen avatar Aug 31 '19 03:08 jebrosen

@jebrosen, based on your async branch I created a branch with rudimentary websocket support and I would like to contribute this new feature to rocket.

The first draft of the API looks likes this:

let (tx, rx) = channel();

std::thread::spawn(move || {
    let duration = std::time::Duration::from_secs(1);
    loop {
        println!("Sending message");
        tx.unbounded_send(Message{}).unwrap();
        std::thread::sleep(duration);
    }
});

let _ = rocket::ignite().receivers(vec![rx]).mount("/", routes![hello]).launch();

@SergioBenitez, @jebrosen, what do you think about this first implementation.

BTW: here is the branch, implementing the behavior.

schrieveslaach avatar Oct 13 '19 16:10 schrieveslaach

My vague idea for a websocket API was to be able to do something like the following:

#[websocket("/chat/<room>")]
async fn chat(room: String, conn: WebSocket) {
    // let message = conn.recv().await;
    // conn.send(message).await;
}

fn main() {
    rocket::ignite().websocket(websocket![chat]);
}

That is, have it work similarly and in parallel to catchers and routes. It should also have a lower-level API like Handler. I'm not sure if we should support only Websocket or expose more flexibility in terms of other protocols via Upgrade.

As far as I can tell your current implementation takes any number of receivers, and every message that comes on any receiver is sent to all connected clients - and that pattern is too limiting.

jebrosen avatar Oct 19 '19 17:10 jebrosen

Okay, that looks much nicer, but I have one question. If I understand your example correctly, wouldn't this require that Rocket run the chat function every x milliseconds?

As far as I can tell your current implementation takes any number of receivers, and every message that comes on any receiver is sent to all connected clients - and that pattern is too limiting.

This is not intended to be the final result of a future PR. It was just a very basic implementation to get it running.

Currently, I'm wondering if we could combine our API designs:

#[websocket("/chat/<room>")]
async fn chat(room: String, msg: Message) -> Json<Value> {
    // Will be invoced when rx emits a message and here you could 
   // transform the message into a certain paylod.
}

fn main() {
    let (tx, rx) = channel();

    std::thread::spawn(move || {
        let duration = std::time::Duration::from_secs(1);
        loop {
            println!("Sending message");
            // Message could be a trait and here you could create a 
            // text message (similar to websockets crate API)
            tx.unbounded_send(Message{}).unwrap();
            std::thread::sleep(duration);
        }
    });

    rocket::ignite().websocket(vec![websocket!(chat, rx)]);
}

schrieveslaach avatar Oct 21 '19 07:10 schrieveslaach

This API still seems very limited, since it wouldn't be able to send messages on its own, or respond to a message with multiple messages. There's also a lot of boilerplate in main, which seems like it would be unnecessary. A websocket handler that continues to handle the websocket connection for its entire lifetime, as in @jebrosen's example, would be ideal.

fenhl avatar Oct 21 '19 08:10 fenhl

I'm a bit confused how you would receive message from a background process (imaging that you are receiving Kafka messages from another service in the background and want to update the UI through a websocket connection). Could you give me an example API?

schrieveslaach avatar Oct 21 '19 10:10 schrieveslaach

It's hard to give a code example without knowing what the message producer API looks like, but it would probably involve State.

fenhl avatar Oct 21 '19 10:10 fenhl

I previously wrote a chat room example with async rocket and SSE, where the "receiver" endpoint looks like this: https://git.jebrosen.com/jeb/rocket-rooms/src/commit/381f93daaf17db1a430760f536ceb9699c33b2ea/src/main.rs#L34

The websocket equivalent could end up looking something like this:

#[websocket("/room/<room>")]
async fn chat_room(room: String, rooms: State<'_, Rooms<Message>>, socket: WebSocket) {
    // Subscribe to the room. 'subscription' is a Stream of Messages.
    let mut subscription = rooms.subscribe(&room).await;

    // WebSockets are full duplex, and we want to read/write independently
    let (tx, rx) = socket.split();

    loop {
        // TODO: I have no idea if select! actually works like this
        futures::select! {
            // New message from another client: send it to this one
            server_msg = subscription => tx.send(server_msg).await,

            // New message from this client: broadcast it to the others
            client_msg = rx => rooms.send(&room, client_msg).await,

            // TODO: error handling, shutdown, etc.
        }
    }
}

The main thing this showcases is that neither sending nor receiving of messages is directly controlled by rocket, so the handler can receive from any source.

jebrosen avatar Oct 21 '19 14:10 jebrosen

That example is the example I was looking for in order to understand what should be accomplished. At the weekend I'll have a look into it.

schrieveslaach avatar Oct 22 '19 06:10 schrieveslaach

any news about Websocket support with rocket ?

noor-tg avatar Feb 20 '20 20:02 noor-tg

@jebrosen

Can you say anything to compare:

  1. The work you did
  2. The work shown at: https://www.steadylearner.com/blog/read/How-to-start-Rust-Chat-App
  3. What are the cons/pros of native rocket support vs. the two above hybrid approaches?
  4. using Warp instead of Rocket (Warp has websockets)

DrYSG avatar Feb 21 '20 15:02 DrYSG

@DrYSG

  1. I haven't done any actual work on websockets, other than provide an example for nginx that I have not tested myself. The purpose of that example is to map servers on two ports to look like they are on the same port.
  2. That example starts a websocket and a webserver app in the same process listening on two ports. It does not look like the two share data in any way; if needed, the two could communicate through some shared Arc<Mutex<State>> or channel(s).
  3. Native rocket support would mean that both HTTP and websocket would truly be handled by the same server listening on the same port, and it would simplify sharing state: the websocket routes could access the same managed state through the same mechanism as non-websocket routes.
  4. Warp's websocket support is the same kind as number 3 - the same server handles both plain and websocket requests.

jebrosen avatar Feb 22 '20 18:02 jebrosen

see https://github.com/SergioBenitez/Rocket/issues/1238, the autobahn websocket test suite should be included as part of this effort

rustrust avatar Feb 22 '20 19:02 rustrust

I worked on native websocket support for Rocket (see my comments above). However, I did not had time to continue because I think it requires large extensions and maybe refactorings of Rocket (maybe a second kind of handler, as I tried to use here). Maybe, we could discuss this here?

@rustrust, using a test suite would very beneficial. :+1:

schrieveslaach avatar Feb 23 '20 11:02 schrieveslaach

Is there any update on when this is going to release? In a dire need for WebSocket endpoints.

Yuhanun avatar Jul 05 '20 20:07 Yuhanun