Ring Websocket SPEC Proposal

50 views
Skip to first unread message

James Reeves

unread,
Jul 10, 2010, 8:23:50 AM7/10/10
to Ring
I propose an extension to the Ring SPEC to support websockets and
comet.

Channels
========

Handler functions would be allowed to return *channels*, as well as
the usual response map.

A channel looks like this:

(fn [send]
(fn [message]
(send message)))

The channel function accepts a side-effectful sender function as its
argument. This function can be used to send messages to the client.

The return value of the channel is a listener function. This function
is called whenever the client sends a message to the server.

The above example is a channel that echos any messages sent to it back
to the client.

Messages
========

The messages themselves contain at least the following keys:

:type - This is one of [:connect :data :disconnect]
:body - This is the actual message content (optional)

:connect messages are only sent from the client to the server, and
will be ignored if the server tries to send them to the client.
The :data and :disconnect messages can be sent from either the client
or the server. A :disconnect message signals the end of the channel,
and any messages sent after a :disconnect are ignored.

Additional keys can be added at the developer's discretion, usually
for use by middleware.

Advantages
==========

1. Simple. This approach relies only on functions and no special
types. It can be implemented in both Clojure 1.1 and 1.2.

2. An accurate metaphor. The way channels work is essentially the
Clojure equivalent to how Websockets and Comet work: Start with a HTTP
request, and then open up a two-way message stream.

3. Backward compatible. All previous Ring applications will still
work.

4. Protocol neutral. This can be used with any Comet protocol, because
all Comet protocols support at least a bidirectional message stream.
Likewise, this is a perfect fit for Websockets.

5. Flexible. The sender can be stored and called from any thread. For
example, the following handler broadcasts any message sent to all
connected clients:

(defn broadcaster [request]
(let [senders (atom #{})]
(fn [send]
(swap! senders conj send)
(fn [message]
(doseq [s @senders] (s message)))))

5. Middleware. Because we're just using functions, we can write
middleware like this:

(defn wrap-channel [handler middleware]
(fn [request]
(let [response (handler request)]
(if (fn? response)
(middleware response)
response))))

(defn wrap-channel-filter [handler reader writer]
(wrap-channel
(fn [channel]
(fn [send]
(let [sender #(send (update-in % [:body] writer))
listener (channel sender)]
#(listener (update-in % [:body] reader)))))))

(def app
(wrap-channel-filter handler json-read json-str)))

Using wrap-filter, we can easily handle messages in a format like
JSON or XML. More complex behaviour such as user authentication and
message broadcasting could also be added.

Feedback
========

Any criticism or suggestions are welcome. I'd like to get this right
so we're not burdened by a bad implementation further down the road.

Mark, I'd especially like to get some feedback from you, as Ring's
your "baby", so to speak :)

- James

Mark McGranaghan

unread,
Jul 11, 2010, 1:42:42 PM7/11/10
to ring-c...@googlegroups.com
James, thanks for your thoughtful proposal.

I think this work clearly captures the spirit of Ring: abstract
implementation details behind a minimal, clean, and composable
interface. I agree with all the advantages that you described; both
that they are the goals such an interface should try to achieve and
that this interface has the potential to achieve them.

I also agree with your sentiment that it is useful to start by working
backwards from the abstraction. From here I think there are several
things we should do to validate the theory that this is the
appropriate abstraction for duplexed communication in Clojure web
apps.

* Continue to elicit feedback from Ring community members. I am
particularly interested in hearing from people that have production
apps that use duplexed channels extensively, in Clojure or otherwise.
* Sketch implementations of the interface for various backing transports.
* Sketch handlers and middleware that use various aspects of the abstraction.

Ultimately, any change to the Ring SPEC needs to be both carefully
designed and validated by working, non-trivial, end-to-end Clojure web
application code.

It also might be useful to compare this abstraction to interfaces
offered by duplexing libraries in other languages:

* http://github.com/LearnBoost/Socket.IO-node
* http://github.com/igrigorik/em-websocket

I do have a few particular questions about your proposal:

How do you feel about reserving IFn responses exclusively for
channels, and about using clojure.core/fn? as the way to determine if
a response is a channel? Do you think it would ever be useful to be
able to return IFn's as a type of regular synchronous Ring response?

When/how would the channel-enabled handler be invoked and the
corresponding channel returned? For example, you suggested that the
following could implement broadcasting:

(defn broadcaster [request]
(let [senders (atom #{})]
(fn [send]
(swap! senders conj send)
(fn [message]
(doseq [s @senders] (s message)))))


This seems to imply that the broadcaster handler is invoked only once
in the lifetime of the server as `senders` is let'd within its
invocation instead of outside of it.

A related question; is exactly 1 listener closure created per client?
Looking at the echo server example:

(fn [send]
(fn [message]
(send message))

The message callback does not contain any information about what
client sent the message, so presumably it is the single one to which
send corresponds. If this is indeed the case, are :connect messages
redundant?

Do I understand correctly that channel middleware effects both
incoming and outgoing messages by altering the channel response, as
apposed to regular middleware that operations on both sides of the
handler? Is there way to determine as a request moves up from the
adapter through middleware to the handler - before it returns a
channel - if the request is one corresponding to an eventual channel
response?

Do you have any additional comments about representing channels as
functions that presumably dispatch on :type vs. function maps keyed by
type vs implementations of a Channel protocol?. In particular, can you
elaborate on why you chose to represent a channel as single function
instead of 3 - one for each of connect, message, and disconnect?

I'd like to explore the possibility of simplifying the protocol by
removing a layer of function-combination. For example, what do you
think about a protocol that supported the following echo server?

(fn [message]
((:send message) message))

I ask about this because the multiple layers of higher order functions
makes middleware harder to unstand. For example, I've studied this
snippet for a while and do think I fully understand it:

(defn wrap-channel-filter [handler reader writer]
(wrap-channel
(fn [channel]
(fn [send]
(let [sender #(send (update-in % [:body] writer))
listener (channel sender)]
#(listener (update-in % [:body] reader)))))))

Perhaps you provide a few more middleware example snippets, like ones
that just alter incoming message bodies, or that filter incoming
messages, or the inject extra outgoing messages.

Finally, how do you feel about connect/disconnect vs open/close?

Thanks again James for your work on this proposal.

- Mark

Wilson MacGyver

unread,
Jul 11, 2010, 2:46:18 PM7/11/10
to ring-c...@googlegroups.com
The Erlang one maybe useful too.

http://armstrongonsoftware.blogspot.com/2009/12/comet-is-dead-long-live-websockets.html


On Jul 11, 2010, at 1:42 PM, Mark McGranaghan <mmcg...@gmail.com> wrote:
...

James Reeves

unread,
Jul 11, 2010, 7:51:33 PM7/11/10
to ring-c...@googlegroups.com
On 11 July 2010 18:42, Mark McGranaghan <mmcg...@gmail.com> wrote:
> Ultimately, any change to the Ring SPEC needs to be both carefully
> designed and validated by working, non-trivial, end-to-end Clojure web
> application code.

I agree completely. I'll add a new branch to the repository to
experiment with an implementation.

> How do you feel about reserving IFn responses exclusively for
> channels, and about using clojure.core/fn? as the way to determine if
> a response is a channel? Do you think it would ever be useful to be
> able to return IFn's as a type of regular synchronous Ring response?

I think it's reasonable to have a function representing a dynamic
channel, and a map representing a normal, static response.

I need to consider how I'd work this into Compojure, since returning a
function in Compojure is currently equivalent to a nested route.
However, I believe I can work around this.

>  (defn broadcaster [request]
>    (let [senders (atom #{})]
>      (fn [send]
>        (swap! senders conj send)
>        (fn [message]
>          (doseq [s @senders] (s message)))))
>
> This seems to imply that the broadcaster handler is invoked only once
> in the lifetime of the server as `senders` is let'd within its
> invocation instead of outside of it.

Ah, that's an error on my part. It should be:

(let [senders (atom #{})]

  (defn broadcaster [request]


      (fn [send]
       (swap! senders conj send)
       (fn [message]
         (doseq [s @senders] (s message)))))

As you point out, the sender setup would have to be located outside
the handler function, otherwise we'd just create a new set of senders
every request.

> A related question; is exactly 1 listener closure created per client?
> Looking at the echo server example:
>
>  (fn [send]
>     (fn [message]
>       (send message))
>
> The message callback does not contain any information about what
> client sent the message, so presumably it is the single one to which
> send corresponds. If this is indeed the case, are :connect messages
> redundant?

Yes, the send function corresponds to one client.

I'm somewhat undecided on connect messages, but I think they're
useful. In some ways they are redundant, but they perform a subtly
different job than the initial channel function.

The channel function is called immediately after the initial request,
but there might be some data the server is required to send before the
channel is considered completely open. For instance, the client might
open a channel, and then the server sends a unique session ID. The
channel is not considered "connected" until that initial session ID is
sent.

Another advantage of connect messages is that middleware can add
arbitrary keys to them.

There's also a certain neatness in having a connect message to go
along with the disconnect message.

> Do I understand correctly that channel middleware effects both
> incoming and outgoing messages by altering the channel response, as
> apposed to regular middleware that operations on both sides of the
> handler?

Mostly, but not necessarily.

A common Comet technique is to use long polling to simulate a stream
of data. The client sends an XHR to the server, waits for a response,
and then upon receiving a response immediately sends another XHR.

So in effect a duplex channel is constructed by stitching together a
bunch of smaller streams that expire after one message (or a timeout).

We could use Ring middleware to stitch these streams together. So
whilst the middleware receives multiple requests with many short-lived
channels, the wrapped handler only sees one request with one
long-lived channel.

This means that so long as an adapter supports long-polling, we can
use middleware to implement any Comet protocol on the top.

> Is there way to determine as a request moves up from the
> adapter through middleware to the handler - before it returns a
> channel - if the request is one corresponding to an eventual channel
> response?

No. I mean, if the request is a websocket, you can tell by the
headers. But there's no standard way of telling whether a request
expects a Comet response or not, as far as I know.

> Do you have any additional comments about representing channels as
> functions that presumably dispatch on :type vs. function maps keyed by
> type vs implementations of a Channel protocol?. In particular, can you
> elaborate on why you chose to represent a channel as single function
> instead of 3 - one for each of connect, message, and disconnect?

There were a few reasons.

First, it seemed more in line with how requests work currently. To
tell whether a request is a GET, POST, etc., you check the
:request-method key on the request map. Likewise, you'd check the
:type of the channel message.

It would also be more concise to ignore message types you don't want,
for example:

(fn [msg]
(if (= (:type msg) :data)
(do-something msg)))

Another advantage is that you can turn a connect/data/disconnect
protocol or function map into a single listener function fairly
easily, but you can't easily do the reverse.

I was also thinking that perhaps custom types could be added by
middleware (although perhaps that would be best achieved with a
separate key).

> I'd like to explore the possibility of simplifying the protocol by
> removing a layer of function-combination. For example, what do you
> think about a protocol that supported the following echo server?
>
>  (fn [message]
>    ((:send message) message))

Well... it seems a little inelegant to add a side-effectful function
to a message map, and you wouldn't be able to serialize the map unless
you dissoc'd the function first.

I'm also not sure it would make things that much simpler. Consider
some middleware that broadcasts any response:

(defn wrap-broadcast [handler]


(let [senders (atom #{})

send-all #(doseq [s @senders] (s %))]


(fn [request]
(let [response (handler request)]
(if (fn? response)

(fn [send]
(swap! senders conj send)

(response send-all))
response)))))

And now the same middleware using a sender embedded in the message:

(defn wrap-broadcast [handler]


(let [senders (atom #{})

send-all #(doseq [s @senders] (s %))]


(fn [request]
(let [response (handler request)]
(if (fn? response)

(fn [message]
(when (= (:type message) :connect)
(swap! senders conj (:send message))
(response (assoc message :send send-all)))
response)))))

If anything, it's just complicated things :/

It's also a little less safe. By embedding the sender in the message,
middleware can potentially monkey around with the :send key, so you
can't guarantee that the sender is the same. I guess you might *want*
to do this, but it makes globally overriding the sender more
difficult. To be certain, you'd need to check on each message that the
sender wasn't changed by some higher middleware.

> Perhaps you provide a few more middleware example snippets, like ones
> that just alter incoming message bodies, or that filter incoming
> messages, or the inject extra outgoing messages.

I'll see about posting some more snippets tomorrow. I think that
channel middleware lends itself to high-level functions, so I'll begin
by defining a few functions that do various generic tasks, like filter
and map.

But I think this post is probably too long as it is, without half a
dozen examples cluttering it up. Also, it's past midnight here :)

> Finally, how do you feel about connect/disconnect vs open/close?

I'm ambivalent, but connect/disconnect seems to be used in a lot of
other places (Jetty's Websocket class, Comet Sessions spec, etc.).

> Thanks again James for your work on this proposal.

Not a problem.

- James

Reply all
Reply to author
Forward
0 new messages