-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Native WebSocket support #90
Comments
This is indeed planned. |
@SergioBenitez got any suggestions for how one could manually add web socket support? |
@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 fn main() {
thread::spawn(|| {
listen("127.0.0.1:3012", |out| {
move |msg| {
out.send(msg)
}
})
})
rocket.ignite()..more()..launch()
} |
@SergioBenitez thanks :) I ended up discovering this sort of approach myself. |
@SergioBenitez is this waiting on a future version of Rocket that migrates to Actix or some other async framework? |
Is there no way to add web socket support on the same port as HTTP? If not, this is quite problematic for me. |
@sparky8251 Not currently, at least easily. |
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 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 #90 (comment)). I've made an example (completely untested so far, sorry) based on the nginx example for WebSocket:
I presume something similar can be done with Apache as well. |
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. |
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: #17 (comment) |
With #1008 merged, would it make sense to start implementing websocket support on the async branch? |
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. |
WebSockets will certainly be one of the larger issues. I'm actually looking into the two main websocket crates ( 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. |
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, based on your 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. |
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 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. |
Okay, that looks much nicer, but I have one question. If I understand your example correctly, wouldn't this require that Rocket run the
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)]);
} |
For those watching: design discussions are taking place on Matrix. Here is my proposed API for websockets, by example: //! A multiroom chat application with a special `echo` room which echoes its
//! messages to `lobby`.
// `&Channel`, `&Broker` are channel guards, provided by Rocket.
// `&Broker` is additionally a request guard for sending messages from anywhere.
//
// A `Channel` is:
// 1) A bidrectional channel between two endpoints: client and server.
// 2) Linked to a topic URI, for broadcasting to that topic.
// 3) A cache storer, like `&Request`. The cache is dropped on `leave`.
//
// A `Broker` knows about all channels.
//
// &Username is an application channel guard that asserts that the connection
// sending a message has been granted the reflected username.
type UserList = State<ConcurrentHashSet<String>>;
enum Event<'r> {
/// A join with a requested `username` in `.0`.
Join(&'r str),
/// Message `.1` sent by user with username `.0`.
Message(&'r Username, &'r str),
/// User with username `.0` has left.
Leave(&'r Username),
}
/// A new client has connected to a room. Inform the room of this, say hello to
/// the client with a "join" message.
#[join("/<_>", data = "<username>")]
fn join(channel: &Channel, username: &str, users: &UserList) {
if users.contains_or_add(username) {
channel.send(Event::Leave("username taken")).await;
return channel.close();
}
channel.local_cache(|| username.to_string());
channel.send(Event::Join("hello!")).await;
channel.broadcast(Event::Join(username)).await;
}
/// A message sent to a room that's not `echo`.
#[message("/<_>", data = "<msg>")]
fn room(channel: &Channel, user: &Username, msg: &str) {
channel.broadcast(Event::Message(user, msg)).await;
}
/// A message sent to `/echo`: send it to `/echo` and `/lobby` concurrently.
#[message("/echo", data = "<msg>")]
fn echo(broker: &Broker, channel: &Channel, user: &Username, msg: &str) {
let lobby_msg = channel!(broker, room("/lobby")).broadcast(Event::Message(user, msg));
let room_msg = channel.broadcast(Event::Message(user, msg));
join!(lobby_msg, room_msg).await;
}
/// A user has left a room. Let the room know.
#[leave("/<_>")]
fn leave(channel: &Channel, user: &Username, users: &UserList) {
users.remove(user);
channel.broadcast(Event::Leave(user)).await;
} |
So I guess there isn't any working prototype for this and I should just use the preexisting websocket thing mentioned above? |
Also maybe add |
Can SocketIO.js on the client side be used as a fallback for Rocket in longpoll mode? |
No; in fact this would probably require even more implementation efforts: Socket.IO uses its own protocol on top of long-polling and/or WebSockets, and the few rust implementations of socket.io servers I found appear to be ready or suitable for "plugging in" to Rocket at the moment. |
If you are interested, there is a pull request I've been working on to integrate websocket support directly into Rocket itself. However, I would caution against using it in production yet, since it's not quite ready yet. There are several option questions related to websocket support, and some serious testing need to happen. Since it sounds like you are implementing a service that uses websockets, how do you handle user authentication? I've implemented a solution that takes advantage of temporary URLs, but I'm curious to hear what your solution is. |
Well for emulating WS via EventStream, I just send out a unique token and use that as the data field and use a custom eventstream impl on the client that supports payloads. |
Looks like WebSockets are currently planned for the 0.8.0 milestone. Sounds it'll be a few years still, then? |
This proposed API is the best IMO, since it offers the greatest flexibility. A channel-based API may be too limiting for certain use cases. |
Did the design discussions get anywhere? I have a feeling this is a highly anticipated feature, and I myself would appreciate it for my app, too, but it looks like it's not feasible to implement for the time being? Another question: Does rocket somehow expose internal state? Because if I run a separate server for WS handling (eg. with tokio-tungstenite), I'd need to somehow get the internal state and possibly DB pool of the rocket app, I think. |
@TimBoettcher I recommend reading up the discussion on the PR to get an idea about alternatives and why rocket cannot be used with tokio-tungstenite. |
What I really like about this proposal is that is the one that is most consistent with Rockets current API. Macros and guards generally deal with request from the user, while the response is dealt by the return type . Here |
I've seen this crate which might provide a somewhat easy way to provide an implementation. |
This one is definitely better than most proposals. The WebSocket request API isn't too complex I could easily write it. It's also very flexible than what most other proposal offers. |
Worked the last few weeks on a solution similar to the one presented in this comment, by adding an API to expose the hyper upgrade to rocket and implementing websockets with it. Have submitted an combined PR (#2466) for you guys to try it out. There are some things missing for a complete websocket implementation such as subprotocols and extensions; but for now simple sending/recieving should suffice. Example for the upgrade API: struct TestSocket {}
#[crate::async_trait]
impl Upgrade<'static> for TestSocket {
async fn start(&self, upgraded: crate::http::hyper::upgrade::Upgraded) {
// can fully use the hyper::upgrade::Upgraded struct
}
}
impl<'r, F> Responder<'r, 'r> for TestSocket {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'r> {
Response::build()
.status(Status::SwitchingProtocols)
.raw_header("Connection", "upgrade")
.raw_header("Upgrade", "testsocket")
.upgrade(Some(Box::new(self)))
.ok()
}
} and one for the websockets: use rocket::response::websocket::{WebsocketMessage, WebsocketChannel, CreateWebsocket, Websocket};
#[get("/ws")]
fn websock() -> Websocket![] {
CreateWebsocket! {
while let Some(msg) = ch.next().await {
println!("handler {msg:?}");
ch.send(msg);
}
}
} |
Connection upgrading support just landed via #2488 and d97c83d! Thanks to @Mai-Lapyst for the push and idea. This means we can implement support for WebSockets outside of Rocket. The new upgrade example does exactly this! Here's the #[get("/echo")]
fn echo(ws: ws::WebSocket) -> ws::Channel {
ws.channel(|mut stream| Box::pin(async move {
while let Some(message) = stream.next().await {
let _ = stream.send(message?).await;
}
Ok(())
}))
} I still believe we want an API like the one in #90 (comment), but this brings us to parity with the #[get("/echo")]
fn echo(ws: ws::WebSocket) -> ws::Channel {
ws::channel! { ws =>
for await message in ws {
yield message;
}
}
} |
The stream API is now implemented in the example! |
WebSocket support is now in the contrib As WebSocket support is now available from an officially supported crate, I believe we can close this issue. I would still like Rocket to eventually support a channels/group-based API as in #90 (comment), but we can leave that for another issue. Thank you all, for your patience. ❤️ |
Congratulations! This feature is really important and I'm really excited to see WebSocket merged into Rocket! |
@SergioBenitez The docs seem to be wrong
but the crate is not yet in crates.io so you need to do
|
We always write docs on master as if they're part of the release master last targeted. Otherwise we'd need to update the docs with a commit rev every time we push them, which is simply untenable. The docs are there as a reference for those using master, so the assumption is they understand how to depend on Rocket and its dependencies from master properly. |
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!
The text was updated successfully, but these errors were encountered: