Jetty7 WebSockets

68 views
Skip to first unread message

Hugo Duncan

unread,
Jul 7, 2010, 11:13:39 AM7/7/10
to ring-c...@googlegroups.com
Hi,

I have two ring libs, ring-jetty7-adapter and a ring-jetty7-servlet, that
enable the use of websockets with ring. They are in my ring fork at
http://github.com/hugoduncan/ring.

WebSocket connection requests are passed to the handler with a
:request-method of :websocket-connect. The handler should return a
WebSocket instance in the :websocket key of the response map. The
WebSocket instance may be built using the websocket-proxy function.

Feedback appreciated.

--
Hugo Duncan

James Reeves

unread,
Jul 7, 2010, 1:53:30 PM7/7/10
to ring-c...@googlegroups.com
Websocket support in Ring needs to be implemented through a standard
Clojure interface, such as a protocol:

(defprotocol Channel
(send [message])
(listen [listener]))

Your ring-jetty7-adapter and ring-jetty7-servlet are okay for a
standalone libraries, but they are tied to specific classes in the
Jetty libraries, and as such cannot (in my opinion) be included in the
core Ring library.

Any websocket/comet implementation for RIng needs to:

1. Require only Clojure types - no Java classes
2. Be as simple and elegant as possible

- James

Hugo Duncan

unread,
Jul 7, 2010, 2:13:15 PM7/7/10
to ring-c...@googlegroups.com
On Wed, 07 Jul 2010 13:53:30 -0400, James Reeves <jre...@weavejester.com>
wrote:

> Websocket support in Ring needs to be implemented through a standard
> Clojure interface, such as a protocol:
>
> (defprotocol Channel
> (send [message])
> (listen [listener]))
>
> Your ring-jetty7-adapter and ring-jetty7-servlet are okay for a
> standalone libraries, but they are tied to specific classes in the
> Jetty libraries, and as such cannot (in my opinion) be included in the
> core Ring library.

Sure. I was under the impression that ring was still 1.1 compatible?

> Any websocket/comet implementation for RIng needs to:
>
> 1. Require only Clojure types - no Java classes
> 2. Be as simple and elegant as possible

Other than protocols, do you see ways of making this simpler or more
elegant?

> - James
>
> On 7 July 2010 16:13, Hugo Duncan <dunca...@gmail.com> wrote:
>> Hi,
>>
>> I have two ring libs, ring-jetty7-adapter and a ring-jetty7-servlet,
>> that
>> enable the use of websockets with ring. They are in my ring fork at
>> http://github.com/hugoduncan/ring.
>>
>> WebSocket connection requests are passed to the handler with a
>> :request-method of :websocket-connect. The handler should return a
>> WebSocket instance in the :websocket key of the response map. The
>> WebSocket
>> instance may be built using the websocket-proxy function.
>>
>> Feedback appreciated.
>>
>> --
>> Hugo Duncan
>>


--
Hugo Duncan

James Reeves

unread,
Jul 7, 2010, 2:53:23 PM7/7/10
to ring-c...@googlegroups.com
On 7 July 2010 19:13, Hugo Duncan <dunca...@gmail.com> wrote:
> Sure.  I was under the impression that ring was still 1.1 compatible?

Multimethods and function maps can achieve a similar effect, but
rather less elegantly than the new 1.2 features. I think we should
design with 1.2 in mind, and then "backport" the functionality into
1.1 structures.

The session stores were designed this way. Once 1.2 comes out, they'll
be changed from function maps into a SessionStore protocol and several
types.

> Other than protocols, do you see ways of making this simpler or more
> elegant?

What we need is a structure that does two things:

1. Can be sent messages.
2. Can send messages to listeners.

The internal details are unimportant for now - they're easily implemented.

...Athough they must be implemented in a way that allows for a form of
streaming middleware. I don't think we can easily or elegantly do this
with one function, because send and receive are asynchronous. I think
the simplest implementation is a protocol with two functions.

I'm also puzzling over the content of the message. So far, I'm
thinking a map of at least three keys:

:event - one of :connect, :data, :disconnect
:request - the original request map that initiated this stream
:body - the actual data content of the message (optional)

So in short, I'm thinking:

A Channel is a protocol with two functions: send and recv.
A message is a map of three keys.

In practise, it would look something like this:

(defn handler [request]
(if (= (request :uri) "/echo")
(channel (fn [msg] (msg :body)))
{:status 404, :headers {}, :body ""}))

I don't think it's possible to get simpler than that for asynchronous
communication, but I'm open to ideas.

- James

Hugo Duncan

unread,
Jul 7, 2010, 3:51:04 PM7/7/10
to ring-c...@googlegroups.com
On Wed, 07 Jul 2010 14:53:23 -0400, James Reeves <jre...@weavejester.com>
wrote:

...

> So in short, I'm thinking:
>
> A Channel is a protocol with two functions: send and recv.
> A message is a map of three keys.
>
> In practise, it would look something like this:
>
> (defn handler [request]
> (if (= (request :uri) "/echo")
> (channel (fn [msg] (msg :body)))
> {:status 404, :headers {}, :body ""}))
>
> I don't think it's possible to get simpler than that for asynchronous
> communication, but I'm open to ideas.

If I understand the above, messages are always sent in reaction to data
received on a channel. Other use cases are for the server to be able to
send on events from other sources, e.g. from a message queue, or for the
client to send a message without the server replying.

Is Channel a user level protocol, i.e. could it be used to customise the
message sent to each connection? or filter which connections actually get
sent the message? Or is Channel intended to be implemented for each
different backend?


--
Hugo Duncan

James Reeves

unread,
Jul 7, 2010, 6:45:38 PM7/7/10
to ring-c...@googlegroups.com
On 7 July 2010 20:51, Hugo Duncan <dunca...@gmail.com> wrote:
> If I understand the above, messages are always sent in reaction to data
> received on a channel.

In this case yes, because it's an echo server. But that doesn't have
to be the case.

I guess I'm not being very clear. Let me flesh out my idea with some code.

First, consider a unidirectional (simplex) channel:

(defprotocol Simplex
(transmit [channel message])
(add-receiver! [channel receiver]))

(deftype AtomicSimplex
[receivers]
(transmit [_ message] (doseq [r @receivers] (r message)))
(add-receiver! [_ receiver] (swap! receivers conj receivers)))

(defn simplex [& receivers]
(AtomicSimplex. (atom (set receivers))))

If we create a simplex channel, and then send a message to it, that
message is relayed to each receiver. Additionally, we can add
receivers whenever we wish.

Simplex channels are nice primitives, but most of the time we want
bidirectional (duplex) communication. Fortunately, we can achieve this
just by adding two simplex channels together:

(defprotocol Duplex
(inbound [channel])
(outbound [channel]))

(deftype AtomicDuplex
[in out]
Duplex
(inbound [_] in)
(outbound [_] out))

(defn channel [& listeners]
(AtomicDuplexChannel.
(apply simplex listeners)
(simplex)))

(defn add-listener! [channel listener]
(-> channel inbound (add-receiver listener)))

(defn send [channel message]
(-> channel outbound (transmit message)

(defn channel? [x]
(isa? Channel x))

However, one of the problems I'm hitting is how to fit middleware into
all this. Ideally, we need a protocol like:

(defprotocol Channel
(send [channel message])
(recv [channel message]))

And then we could write things like:

(defn wrap-json-channel [handler]
(fn [request]
(let [response (handler request)]
(if (channel? response)
(reify Channel
(send [_ mesg] (json-str (send response mesg)))
(recv [_ mesg] (recv response (read-json mesg))))
response))))

Well, that doesn't really work... but hopefully you get the gist of
what I'm trying to do. I can see an outline of a solution, but I'm not
quite there yet.

- James

Hugo Duncan

unread,
Jul 8, 2010, 2:28:22 PM7/8/10
to ring-c...@googlegroups.com
On Wed, 07 Jul 2010 18:45:38 -0400, James Reeves <jre...@weavejestEr.com>
wrote:

> I guess I'm not being very clear. Let me flesh out my idea with some
> code.

OK, I had a go at interpreting this. I have added an example of usage at

http://github.com/hugoduncan/ring/blob/master/ring-jetty7-adapter/src/ring/adapter/example.clj

I'm not sure if it is quite what you had in mind - I just did the simplest
thing that seemed to work for me. Disconnect is not nicely handled at the
moment, and the passing of maps can no doubt be improved.

--
Hugo Duncan

James Reeves

unread,
Jul 8, 2010, 9:27:41 PM7/8/10
to ring-c...@googlegroups.com
On 8 July 2010 19:28, Hugo Duncan <dunca...@gmail.com> wrote:
> OK, I had a go at interpreting this.

Um, I was really just throwing out ideas. Nothing I've written about
thus far is complete enough to warrant an implementation.

If comet/websocket support is added to Ring, it requires an extension
of the SPEC. This isn't something to be taken lightly; specifications
need to be carefully considered, otherwise we'll just end up with a
mess.

And without a specification, there's little point in writing an
implementation, unless it's just to lay general groundwork.

That all said, I've had opportunity to think about this further...

My new idea for Ring comet/websocket support looks like this:

(defn echo-handler [request]
(fn [send]
(fn [message]
(send message))))

If a handler wishes to initiate a duplex stream (i.e. websocket or
comet communication), it returns a "listener-factory" function. Or
perhaps a catchier name would be a "handshake" function.

The handshake function takes a "send" function as an argument. This is
a side-effectful function that will send messages to the client.

The handshake function then returns a "listener" function. This
function is invoked whenever a new message is received from the
client.

This implementation would allow middleware, like so:

(defn json-filter [handler]
(fn [request]
(let [channel (handler request)]
(fn [send]
(let [json-send (fn [msg] (send (json-str msg)))
listener (channel json-send)]
(fn [msg] (listener (read-json msg))))))))

It's quite late here, so maybe I'm too tired to see the flaws in this
approach, but so far it looks rather promising.

Criticism is welcome.

- James

Hugo Duncan

unread,
Jul 8, 2010, 11:36:09 PM7/8/10
to ring-c...@googlegroups.com
On Thu, 08 Jul 2010 21:27:41 -0400, James Reeves <jre...@weavejester.com>
wrote:

> And without a specification, there's little point in writing an


> implementation, unless it's just to lay general groundwork.

I am a firm believer in developing specifications in parallel with working
implementations.

Anyway, the discussion so far has certainly improved what I have, and I
look forward to seeing how ring develops in this area.

--
Hugo Duncan

James Reeves

unread,
Jul 9, 2010, 1:09:59 PM7/9/10
to ring-c...@googlegroups.com
On 9 July 2010 04:36, Hugo Duncan <dunca...@gmail.com> wrote:
> I am a firm believer in developing specifications in parallel with working
> implementations.

I kinda disagree with this. Specifications should begin life as a
malleable collection of ideas and designs. Trying to pin something
vague down to a specific implementation doesn't seem like a good
approach.

That said, once a specification starts to solidify, I agree that a
real implementation needs to be written to see if there's anything
that works in theory but doesn't work in practise.

> Anyway, the discussion so far has certainly improved what I have, and I look
> forward to seeing how ring develops in this area.

This may happen quite soon. I'll write up a post proposing an
extension to the Ring SPEC this weekend, where I'll go into my
reasoning and design in greater detail, in the hope I can get some
good community criticism.

- James

Reply all
Reply to author
Forward
0 new messages