CrossClj

0.3.2 docs

SourceDocs


project

docs index

NAMESPACES
net.thegeez

RECENT

    clj-browserchannel

    Clojars

    May 25, 2016


    OWNER
    Gered King
    Toronto, Canada
    gered+github@blarg.ca
    www.blarg.ca

    Readme

    Index of all namespaces


    « Project + dependencies

    BrowserChannel server implementation in Clojure, with a ClojureScript wrapper for the BrowserChannel API included in Google Closure.

    The README below is fetched from the published project artifact. Some relative links may be broken.

    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.2"]
    

    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:

    (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, then on-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 though on-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 after send-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 main on-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.