Skip to content
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

WebSockets support #67

Open
ibaryshnikov opened this issue Nov 26, 2018 · 16 comments
Open

WebSockets support #67

ibaryshnikov opened this issue Nov 26, 2018 · 16 comments
Labels

Comments

@ibaryshnikov
Copy link
Contributor

@ibaryshnikov ibaryshnikov commented Nov 26, 2018

Is there a way to use websockets in tide?

@aturon

This comment has been minimized.

Copy link
Collaborator

@aturon aturon commented Nov 26, 2018

Not yet! This would require exposing a bit more from hyper, in particular http upgrades. Definitely worth taking on!

@aturon aturon added the design label Nov 26, 2018
@aturon

This comment has been minimized.

Copy link
Collaborator

@aturon aturon commented Nov 26, 2018

I'm going to mark this as a "Design" question right now, because it'd be good to talk through the API for exposing http upgrades within Tide.

@secretfader

This comment has been minimized.

Copy link
Contributor

@secretfader secretfader commented Apr 17, 2019

I recently found tokio-tungstenite, a project which provides tokio bindings for Tungstenite, a WebSocket library. I haven't personally reviewed the code, but it might be worth a look (at least for inspiration).

@najamelan

This comment has been minimized.

Copy link

@najamelan najamelan commented Aug 4, 2019

Tungstenite works on a socket which implements io::Read + io::Write (tungstenite), or AsyncRead + AsyncWrite (tokio-tungstenite).

Here you can have a stream (like a TCP) and tungstenite will handle the websocket handshake for you.

When looking at the examples of hyper, it looks like hyper handles the handshake for you. This makes sense, as it's also listening for other http requests that might not be upgrade requests. On the other hand, it might be cleaner to leave every library their own speciality. I don't know how feasible it would be for hyper to see that it's an upgrade request, leave the data in the stream and expose that, but that's not how it works right now anyway.

In any case this is supported by tungstenite and tokio-tungstenite by the from_raw_socket method, which assumes the handshake has already taken place. It's not possible I think with these libraries to use custom subprotocols and websocket extensions I think (it doesn't look like hyper supports them either), and since these are negotiated in the handshake, a method like from_raw_socket would have to take information about subprotocols, which it doesn't.

Currently tokio-tungstenite still operates on futures 0.1 types.

So in brief, to enable (tokio-)tungstenite, it suffices to expose a socket which implements io::Read + io::Write (tungstenite), or AsyncRead + AsyncWrite (tokio-tungstenite). When using hyper under the hood, it's a matter of exposing the Upgraded object from hyper.

I have not looked into other websocket crates to see what would be needed in order to support them.

@najamelan

This comment has been minimized.

Copy link

@najamelan najamelan commented Aug 4, 2019

ps: warp has all the code for exposing a Stream+Sink of tungstenite::Message on upgrade. This can inspire implementors.

@najamelan

This comment has been minimized.

Copy link

@najamelan najamelan commented Aug 4, 2019

Ok, I've given the whole thing some more thought. Here is what I think:

Currently webservers like hyper handle http(s). Tungstenite also does this, as you can let it listen on a type that provides io::Read + io::Write. Unfortunately hyper only allows listening for http connections on a TCP (AFAICT). So tungstenite allows you to listen for websocket connections on any type of connection, could be UDS, named pipes, mem mapped file etc, where hyper only supports TCP.

The downside is that tungstenite also has to deal with HTTP and TLS (I suppose that when you upgrade an HTTPS connection, the websocket logic/crate no longer needs to worry about the encryption). This is redundant and creates extra work and security risks.

If hyper would be more generic, and support any AsyncRead/AsyncWrite rather than only TCP, crates like tungstenite could rely on it for the HTTP upgrade part, eliminating a bunch of code.

Both sides could focus on implementing support for subprotocols and websocket extensions to be feature complete and on converting to futures 0.3.

Now frameworks like warp and tide can expose an object that implements Sink + Stream of tungstenite::Message and holds information about subprotocols and extensions. Ideally, the type of the object returned by both warp and tide (and possibly other frameworks) would be the same and only implemented once in a separate crate. It would be a type that takes in an upgraded http connection + subprotocol and extension metatdata and that provides the Sink+Stream.

This way, there is no double work/maintenance. Every layer is clearly defined. This new crate could supersede tokio-tungstenite and provide futures 0.3 types. If hyper is to high level for dealing with websocket handshakes over things other than TCP, maybe this can also be separated out into a crate that uses http and http-parse to do so and both hyper and tungstenite can reuse this logic?

Personally I'm working on providing AsyncRead/AsyncWrite on top of such a Sink/Stream of websocket messages so that wasm code and servers can conveniently communicate rust objects on a websocket connection framed with tokio-codec/futures-codec. This logic could potentially go in the same crate as the Sink/Stream.

@seanmonstar @agalakhov What do you think of this?

@agalakhov

This comment has been minimized.

Copy link

@agalakhov agalakhov commented Aug 4, 2019

@najamelan Such a layered system was exactly that I wanted when I coded Tungstenite. I wanted to have a single, well-tested system that can work for both async (probably Tokio-based) and purely synchronous (maybe even microcontroller-based) projects.

@najamelan

This comment has been minimized.

Copy link

@najamelan najamelan commented Aug 4, 2019

So to summarize how this could be:

  • one handshake crate: takes a io::Read + io::Write and/or AsyncRead + AsyncWrite which are expected to receive a http upgrade request and metadata (such as which subprotocols the server accepts). It would returns a Result to a raw socket on which the handshake has been completed + metadata. This should probably also hold client side code for initiating the handshake.
  • one codec crate that frames a raw socket and provides a Sink + Stream of websocket Messages and a synchronous equivalent.
  • low level crates like hyper and (tokio-)tungstenite, as well as frameworks can both use these two crates, possibly deciding to handle Ping, Pong messages for the user. They can expose the Sink+Stream to end users.
  • layers on top of this like proving AsyncRead + AsyncWrite which can work regardless of which web framework the user wants to use, and which can work on connections that aren't TCP thanks to a unified low level API.
@seanmonstar

This comment has been minimized.

Copy link

@seanmonstar seanmonstar commented Aug 4, 2019

Hyper's server works with any AsyncRead + AsyncWrite. It just uses TCP if you use the convenient Server::bind constructor.

The warp framework built on top of hyper uses it's built-in upgrade support, and after a WS handshake, uses tungstenite afterwards.

@najamelan

This comment has been minimized.

Copy link

@najamelan najamelan commented Aug 5, 2019

@seanmonstar Oh, sorry. I had a look a the impl of Server. That solves that than.

@acasajus

This comment has been minimized.

Copy link

@acasajus acasajus commented Nov 8, 2019

Can this be taken into account in the new design?
There are not many web frameworks that allow http+ws in the same codebase and this would greatly help in adopting tide!

@yoshuawuyts

This comment has been minimized.

Copy link
Member

@yoshuawuyts yoshuawuyts commented Nov 8, 2019

@acasajus heh, for sure. I also would like to support #234. @zkat pointed me to the Elixir Phoenix framework, and so far it seems like one of the most solid approaches to websockets, and will probably take inspiration from that.

Additionally @ibaryshnikov has done work on implementing a standalone websocket implementation that we could probably reuse here. However before doing any of that I'd like to present the design, and gather feedback. We're not quite at that stage yet though, as the core of Tide is still under construction.

In short: this is definitely being considered, and I agree we should make this a first-class construct!

@kellytk

This comment has been minimized.

Copy link

@kellytk kellytk commented Dec 4, 2019

@yoshuawuyts

This comment has been minimized.

Copy link
Member

@yoshuawuyts yoshuawuyts commented Jan 27, 2020

Came up with a rough API sketch for what a websocket API could look like. Similarly been drafting one for server sent events over in #234 (comment).

API sketch

use async_std::task;

#[derive(Deserialize, Serialize)]
struct Cat { name: &'static str }

let mut app = tide::new();
app.at("/wss").get(tide::ws()); // endpoint to establish a websocket connection on
app.at("/", async |req| {
    let mut socket = req.ws(); // access the connected socket

    socket.send_json(&Cat { name: "chashu" }).await?; // send some data as json
    let msg: Cat = socket.recv_json().await?; // receive some data from json

    Response::new(200.into())
});
app.listen("127.0.0.1:8080").await?;
@xldenis

This comment has been minimized.

Copy link

@xldenis xldenis commented Jan 31, 2020

@yoshuawuyts it seems like with your api it wouldn't allow websockets to have their own lifecycles. Even writing something as simple as a chat server would be impossible. If it's possible to instead pass in an async closure (just like normal requests) that would be much more useful.

@yoshuawuyts

This comment has been minimized.

Copy link
Member

@yoshuawuyts yoshuawuyts commented Feb 1, 2020

@xldenis you're not wrong; related conversations about this have been happening on the SSE issue that might be worth reading up on.

#234 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.