Tutorial: Web
The org.immutant/web
library changed quite a bit from Immutant 1.x
to 2.x, both its API and its foundation: the Undertow web server.
Among other things, this resulted in
much better performance
(~35% more throughput than v1.1.1) and built-in support for
websockets.
The Namespaces
The primary namespace, immutant.web, is comprised of just two functions and a convenient macro:
run
- runs your handler in a specific environment, responding to web requests matching a given host, port, path and virtual host. The handler may be either a Ring function, Servlet instance, or Undertow HttpHandlerrun-dmc
- runs your handler in Development Mode (the 'C' is silent)stop
- stops your handler[s]
The immutant.web.middleware namespace includes two Ring middleware functions:
wrap-session
- enables session sharing among your Ring handler and its websockets, as well as automatic session replication when your app is deployed to a WildFly or EAP cluster.wrap-development
- included automatically byrun-dmc
, this aggregates some middleware handy during development.
WebSockets are created using the immutant.web.websocket namespace, which includes the following:
Channel
- a protocol for WebSocket interaction.Handshake
- a protocol for obtaining attributes of the initial upgrade requestwrap-websocket
- middleware to attach websocket callback functions to a Ring handler
Finally, immutant.web.undertow exposes tuning options for Undertow, should your app require that level of configuration.
A sample REPL session
Now, let's fire up a REPL and work through some of the features of the library.
If you haven't already, you should read through the installation
tutorial and require the immutant.web
namespace at a REPL to follow
along:
(require '[immutant.web :refer :all])
Common Usage
First, you'll need a Ring handler. If you generated your app using a
template from Compojure, Luminus, Caribou or some other
Ring-based library, yours will be associated with the :handler
key
of your :ring
map in your project.clj
file. Of course, a far less
fancy handler will suffice:
(defn app [request] {:status 200 :body "Hello world!"})
To make the app available at http://localhost:8080/, do this:
(run app)
Which, if we make the default values explicit, is equivalent to this:
(run app {:host "localhost" :port 8080 :path "/"})
Or, since run
takes options as either an explicit map or keyword
arguments (kwargs), this:
(run app :host "localhost" :port 8080 :path "/")
The options passed to run
determine the URL used to invoke your
handler: http://{host}:{port}{path}
To replace your app
handler with another, just call run again with
the same options, and it'll replace the old handler with the new:
(run (fn [_] {:status 200 :body "hi!"}))
To stop the handler, do this:
(stop)
Which is equivalent to this:
(stop {:host "localhost" :port 8080 :path "/"})
Or like run
, if you prefer kwargs, this:
(stop :host "localhost" :port 8080 :path "/")
Alternatively, you can save the return value from run
and pass it to
stop to stop your handler.
(def server (run app {:port 4242 :path "/hello"})) ... (stop server)
Stopping your handlers isn't strictly necessary if you're content to just let the JVM exit, but it can be handy at a REPL.
Virtual Hosts
The :host
option denotes the IP interface to which the web server is
bound, which may not be publicly accessible. You can extend access to
other hosts using the :virtual-host
option, which takes either a
single hostname or multiple:
(run app :virtual-host "yourapp.com") (run app :virtual-host ["app.io" "app.us"])
Multiple applications can run on the same :host
and :port
as long
as each has a unique combination of :virtual-host
and :path
.
Advanced Usage
The run
function returns a map that includes the options passed to
it, so you can thread run
calls together, useful when your
application runs multiple handlers. For example,
(def everything (-> (run hello) (assoc :path "/howdy") (->> (run howdy)) (merge {:path "/" :port 8081}) (->> (run ola))))
The above actually creates two Undertow web server instances: one
serving requests for the hello
and howdy
handlers on port 8080,
and one serving ola
responses on port 8081.
You can stop all three apps (and shutdown the two web servers) like so:
(stop everything)
Alternatively, you could stop only the ola
app like so:
(stop {:path "/" :port 8081})
You could even omit :path
since "/" is the default. And because ola
was the only app running on the web server listening on port 8081, it
will be shutdown automatically.
Handler Types
Though the handlers you run will typically be Ring functions, you can
also pass any valid implementation of javax.servlet.Servlet
or
io.undertow.server.HttpHandler
. For an example of the former, here's
a very simple Pedestal service running on Immutant:
(ns testing.hello.service (:require [io.pedestal.http :as http] [io.pedestal.http.route.definition :refer [defroutes]] [ring.util.response :refer [response]] [immutant.web :refer [run]])) (defn home-page [request] (response "Hello World!")) (defroutes routes [[["/" {:get home-page}]]]) (def service {::http/routes routes}) (defn start [options] (run (::http/servlet (http/create-servlet service)) options))
Development Mode
The run-dmc
macro resulted from a desire to provide a no-fuss way to
enjoy all the benefits of REPL-based development. Before calling
run
, run-dmc
will first ensure that your Ring handler is
var-quoted and wrapped in the reload
and stacktrace
middleware
from the ring-devel
library (which must be included among your
[:profiles :dev :dependencies]
in project.clj
). It'll then open
your app in a browser.
Both run
and run-dmc
accept the same options. You can even mix
them within a single threaded call.
WebSockets
Also included in the org.immutant/web
library is the
immutant.web.websocket namespace, which includes the
wrap-websocket
function that attaches a map of callback functions to
your Ring handler. Though it looks like Ring middleware, it actually
returns an HttpHandler
instead of a function, so it must come last
in your middleware chain.
The valid websocket event keywords and their corresponding callback
signatures are as follows, where channel is an instance of the
Channel
protocol, and handshake is an instance of Handshake
:
:on-message (fn [channel message]) :on-open (fn [channel handshake]) :on-close (fn [channel {:keys [code reason]}]) :on-error (fn [channel throwable])
To create your websocket endpoint, pass the result from
wrap-websocket
to immutant.web/run
. Here's an example that
asynchronously returns the upper-cased equivalent of whatever message
it receives:
(ns whatever (:require [immutant.web :as web] [immutant.web.websocket :as ws] [ring.middleware.resource :refer [wrap-resource]] [ring.util.response :refer [redirect]] [clojure.string :refer [upper-case]])) (defn create-websocket [] (web/run (-> (fn [{c :context}] (redirect (str c "/index.html"))) (wrap-resource "public") (ws/wrap-websocket {:on-message (fn [c m] (ws/send! c (upper-case m)))})) {:path "/ws"}))
After running the above, a request to http://localhost:8080/ws
should return a 302 redirect to http://localhost:8080/ws/index.html.
Assuming the wrap-resource
middleware can find public/index.html
in your classpath (typically in your project's resources/
dir), a
<script>
that attempts to open a WebSocket connection to
ws://localhost:8080/ws should work, and an upper-cased version of
any text the browser sends should be returned to it through that
WebSocket.
Note the :path
argument applies to both the Ring handler and the
WebSocket, distinguished only by the request protocol, e.g. http://
vs ws://
.
The WebSocket Handshake
Often, applications require access to data in the original upgrade
request associated with a WebSocket connection, perhaps for user
authentication or some such. That data is made available via the
immutant.web.websocket/Handshake
protocol, an instance of which is
passed to the :on-open
callback.
In particular, you can access all the headers sent in the upgrade
request, and if you're using the wrap-session
middleware, you can
even access any session data stored on behalf of the user by the Ring
handler. Here's a contrived example in which the Ring handler stores a
random id in the session that is then sent back to the user when he
opens a WebSocket:
(ns whatever (:require [immutant.web :as web] [immutant.web.websocket :as ws] [immutant.web.middleware :refer [wrap-session]] [ring.util.response :refer [response]])) (def callbacks {:on-open (fn [c h] (ws/send! c (:id (ws/session h))))} (defn share-session-with-websocket [] (web/run (-> (fn [{{:keys [id] :or {id (str (rand))}} :session}] (-> id response (assoc :session {:id id}))) (wrap-session) (ws/wrap-websocket callbacks)) {:path "/ws"}))
Feature Demo
We maintain a Leiningen project called the Immutant Feature Demo demonstrating all the Immutant namespaces, including examples of simple Web and WebSocket apps.
You should be able to clone it somewhere, cd there, and lein run
.
Have fun!