.. | ||
src/net/thegeez/browserchannel | ||
test/net/thegeez/browserchannel | ||
project.clj | ||
README.md |
clj-browserchannel
Cross-browser compatible, real-time, bi-directional communication between ClojureScript and Clojure using Google Closure BrowserChannel.
This is the main library that provides the client/server functionality that your projects will make use of.
See also: clj-browserchannel
Leiningen
[gered/clj-browserchannel "0.3"]
You will also need to include one of the async adapters as an additional dependency. See here for more information..
Basic Concepts
Communication between client and server in a web app using BrowserChannel occurs after a BrowserChannel session is established.
Just before this initial connection/handshake is done, the client will perform up to 3 different test HTTP requests automatically to determine the supported capabilities of the client browser and what the network connection is like. After this testing is complete, another HTTP request will be sent to establish the session.
There are two "channels" used in a BrowserChannel session:
- Forward Channel - used to initiate the session and also used to transmit messages from the client to the server. Behind the scenes this is a quick HTTP POST request each time the client wants to send something. Multiple messages can be batched into a single request.
- Backward Channel - (or just "back channel") a long running HTTP GET request that is used to transmit messages from the server to the client. On IE 9 and earlier, the back channel is implemented using "forever frames" while on every other browser XHR streaming is used.
As mentioned above, the forward channel also is used to initiate the BrowserChannel session, so the first HTTP request after the initial tests is a forward channel request. Upon success, the client will automatically initiate a back channel request.
Because the back channel is a long running HTTP request, over the course of a long session multiple back channel requests will be opened and closed over time. This is normal and to be expected as connections time out, etc. The client and server will automatically manage this.
Regarding terminology, when reviewing BrowserChannel code, you will see client-to-server messages commonly referred to as 'maps' and server-to-client messages as 'arrays.' clj-browserchannel simplifies this somewhat from the perspective of your application's code, as messages sent in either direction can be any arbitrary Clojure value that can be serialized to a string as EDN.
A session ID is used to identify a client's BrowserChannel session. clj-browserchannel uses UUID's as session IDs. These IDs are generated by the server. Unlike HTTP Session IDs that you may be more familiar with, BrowserChannel session IDs are regenerated far more frequently. Each time a reconnection occurs (a full session reconnection, not just a back channel reconnect), a new session ID is generated for the client. For example, each time a user refreshes the page in their browser they will be given a new BrowserChannel session and corresponding ID. If a network issue occurs and forces the client to automatically reconnect (even without a browser page refresh), a new BrowserChannel session is still established with a different ID.
BrowserChannel supports simple message receipt acknowledgements. When the client sends the server a message (via the forward channel), the client immediately receives acknowledgement based on whether the HTTP POST request was successful or not (as the server returns a standard reply on success).
For server-to-client messages, it is a little more complicated.
Each message sent along the back channel is given an "array id"
(which is included in the data written to the back channel).
When the client reads the data from the back channel it makes
note of the corresponding array ids, and on the next request
to the server (either a forward channel or back channel request)
includes an AID
parameter with the most recent array id that
has been read. The server then uses this AID
value to mark
sent items as acknowledged.
I recommend reading this description of the BrowserChannel protocol for far more in-depth details on everything mentioned in this section (moreso detailing the client side of the protocol, but still very helpful information).
As well it may be useful to try out the chat-demo app
while monitoring ongoing XHR requests. In particular, pay
attention to HTTP requests over /channel/test
and
/channel/bind
.
/channel/test
is where all the previously mentioned test
HTTP requests will be sent to during the initial BrowserChannel
session connection process. /channel/bind
is where all
forward and back channel HTTP requests are sent to.
Note that your browser's inspector may not show you the response body of back channel requests (the long running HTTP GET requests) until they finish. By default in clj-browserchannel, these will automatically timeout after 4 minutes.
Usage
Server-Side
An example using Immutant as the web server with the BrowserChannel
async adapter for it. This assumes you have added 3 dependencies
in your project.clj
:
- clj-browserchannel (that's this library)
- clj-browserchannel-immutant-adapter
- Immutant
(ns your-app
(:gen-class)
(:require
; this next line obviously not required.
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
; ...
[net.thegeez.browserchannel.server :as browserchannel :refer [wrap-browserchannel]
[net.thegeez.browserchannel.immutant-async-adapter :refer [wrap-immutant-async-adapter]]
[immutant.web :as immutant]
; ...
))
(def event-handlers
{:on-open
(fn [session-id ring-request]
; called when a new session is established for a client
)
:on-close
(fn [session-id ring-request reason]
; called when an existing client session is closed
; (for any reason)
)
:on-receive
(fn [session-id ring-request data]
; called when a client has sent data to the server
)})
(def your-app-routes
; ...
)
(def ring-handler
(-> your-app-routes
(wrap-browserchannel event-handlers)
(wrap-defaults site-defaults)
(wrap-immutant-async-adapter)))
(defn -main [& args]
(immutant/run
#'ring-handler
{:port 8080}))
All the server-side BrowserChannel magic happens inside of the
wrap-browserchannel
middleware. The event-handlers
map
shown above includes all the different events you can respond to
on the server.
Of important note, in each of these handlers, ring-request
is
a full Ring HTTP request map like you see with any other normal
HTTP requests in a Clojure web app. You can use this to access
the user's HTTP session for example (but not to change it).
The return value of these event handlers is ignored.
Ring Middleware
As mentioned above, wrap-browserchannel
is what you should add
to your web app's Ring handler to make all the server-side
BrowserChannel functionality work. It takes a map of
event handlers and another optional options map as the last
argument. Options you don't pass in will have their default
value used instead.
See here for a description of the various options you can use. Most applications probably won't need to change any of these.
Sending Data
(use 'net.thegeez.browserchannel.server)
; send any clojure data to a client identified by session-id
(send-data! session-id {:msg "Hello, world!"})
(send-data! session-id :foobar)
(send-data! session-id [:a :b :c])
(send-data! session-id "a string of text")
; send something to all clients
(send-data-to-all! {:broadcast "yada yada"})
Sending is done asynchronously. If the client does not currently have a back channel request active, then the data will be queued up in a buffer which will be flushed once a back channel becomes available.
The send functions also take optional callbacks for various events relevant to message sending:
-
on-sent
occurs when the data has been written to the client's back channel. Remember that this may or may not happen immediately! -
on-confirm
occurs when the client acknowledges receipt of the message. This will almost certainly have a long-ish delay before being triggered. As such it is also at risk of not being raised all the time (e.g. if the client disconnects before the next client acknowledgement info can be sent to the server). -
on-error
occurs when there was some kind of error writing the the message to the client's back channel or if the client's session is terminated before the message could be written to a back channel.
(send-data!
session-id
{:important-data 42}
{:on-sent (fn []
(println "message written to back channel"))
:on-confirm (fn []
(println "receipt confirmed by client"))
:on-error (fn []
(println "message could not be sent"))})
The return value of these callback functions is ignored.
Session Management
Various functions are available to query the status of a client's connection or to manipulate it.
(use 'net.thegeez.browserchannel.server)
; does a client have an active session?
; NOTE: may have an active session but no currently active back channel!
(connected? session-id)
=> true
; more detailed info about a client's session
(get-status session-id)
=> {:connected? true
:has-back-channel? true
:last-acknowledged-array-id 42
:outstanding-backchannel-bytes 0}
(get-status a-different-session-id)
=> {:connected? false}
; "politely" tells a client we are going to disconnect them (and to
; not attempt to reconnect), and then disconnects them
(close! session-id)
; forcefully disconnects a client. note that the client may just
; try to reconnect right away if you use this function
(disconnect! session-id)
Client-Side
Client-side use of clj-browserchannel is very simple.
(ns your-app.client
(:require
[net.thegeez.browserchannel.client :as browserchannel]))
(def event-handlers
{:on-open
(fn []
; called when a new session is established
)
:on-opening
(fn []
; called when a new session connection is in progress
)
:on-close
(fn [due-to-error? pending undelivered]
; called when the browserchannel session is closed
; (for any reason)
)
:on-receive
(fn [data]
; called when data has been received from the server
)
:on-sent
(fn [delivered]
; called when data has been sent successfully to the server
)
:on-error
(fn [error-code]
; called when a connection error occurs.
)})
(defn ^:export run []
(browserchannel/connect! event-handlers))
-
on-open
self-explanatory. Also called on reconnects. -
on-opening
called after a connection has been initiated but before it has been established. -
on-close
called when the session is closed (either closed by the client or server).due-to-error?
is true/false depending on if the close was caused by an error (if true, thenon-error
would have been called just before this event).pending
will contain a list of any messages that were queued up to be sent and may or may not have been received by the server.undelivered
will contain a list of messages that were queued up and definitely were not received by the server. This event is also fired if a connection attempt fails (even thoughon-open
won't have been fired in this case). -
on-receive
called when some data is received from the server. -
on-sent
called when data has been sent successfully to the server.delivered
is a list of the messages that were sent. -
on-error
called when an error occurs. See here for a list of error codes and what they mean. In BrowserChannel, when an error occurs, the session is always disconnected (on-close
will always fire immediately after this event). This is just how the BrowserChannel implementation included in Google Closure works. clj-browserchannel will automatically try to reconnect in the event of an error.
The connect!
function also takes an additional and optional map of
options. Any options not specified will have their default values
used instead. See here for a description of the available
options and their defaults.
Sending Data
(use 'net.thegeez.browserchannel.client)
; basically identical to server-side sending
(send-data! {:msg "Hello, world!"})
(send-data! :foobar)
(send-data! [:a :b :c])
(send-data! "a string of text")
Sending is done asynchronously.
Also a nice feature of BrowserChannel is that you can queue up messages to be sent to the server before a connection is established. If you do this, the messages will be delivered to the server in the same HTTP POST request that is used to establish the new BrowserChannel session (thereby saving some extra round-trips).
Like with server-side sending, the send-data!
function also
can take optional callbacks for various message sending events:
-
on-sent
occurs when the data has been successfully sent to the server (via the forward channel). This will usually occur pretty quickly aftersend-data!
is called. -
on-error
called when any kind of error occurs that prevents the message from being sent. This error will be raised just before the mainon-error
event handler.
(send-data!
{:important-data 42}
{:on-sent (fn []
(println "message sent to server successfully"))
:on-error (fn []
(println "error sending message to server"))})
The return value of these callback functions is ignored.
Session Management
Various functions are available to query the status of the current BrowserChannel session.
(use 'net.thegeez.browserchannel.client)
; is there currently an active session?
(connected?)
=> true
; disconnects/closes any active browserchannel session
; also can be used in the on-close event handler to cancel
; any reconnection attempt if needed
(disconnect!)
(channel-state)
=> :opened
For a list and descriptions of the different BrowserChannel
connection states (returned by channel-state
) see here.
Other Notes
BrowserChannel Session Timeouts
The server-side options do include a session timeout (session-timeout-interval
)
which is the length of time of inactivity before a BrowserChannel
session is automatically closed (timed out).
However it is important to note that this timeout period is only activated when the client does not have an active back channel. If the client is able to consistently re-open back channels as they close automatically over time (which is normal behaviour), then the BrowserChannel session will never time out.
session-timeout-interval
is mainly intended to automatically
cleanup sessions when the client (for whatever reason) suddenly
becomes unable to re-open a back channel or the user closes the
browser or something like that. At this point the session timeout
interval will activate and eventually elapse and clean up the session.
About
Written by: Gijs Stuurman / @thegeez / Blog / GitHub
Many updates in this fork by: Gered King / @geredking / GitHub
License
Copyright (c) 2012 Gijs Stuurman and released under an MIT license.