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 primary namespace, [[immutant.web]], includes the two main functions you'll use to run your handlers:
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 HttpHandlerstop - stops your handler[s]Also included:
run-dmc - runs your handler in Development Mode (the 'C' is silent)server - provides finer-grained control over the embedded web
server hosting your handler[s].The [[immutant.web.middleware]] namespace provides some Ring middleware:
wrap-websocket - attach websocket callbacks to your Ring handlerwrap-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 cluster.wrap-development - included automatically by run-dmc, this
aggregates some middleware handy during development.The [[immutant.web.async]] namespace enables the creation of WebSockets and HTTP streams. And support for Server-Sent Events is provided by [[immutant.web.sse]].
The [[immutant.web.undertow]] namespace exposes tuning options for Undertow, the ability to open additional listeners, and flexible SSL configuration.
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
guide and require the immutant.web namespace at a REPL to follow
along:
(require '[immutant.web :refer :all])
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.
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 hola))))
The above actually creates two Undertow web server instances: one
serving requests for the hello and howdy handlers on port 8080,
and one serving hola 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 hola app like so:
(stop {:path "/" :port 8081})
You could even omit :path since "/" is the default. And because
hola was the only app running on the web server listening on port
8081, it will be shutdown automatically.
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.
The [[immutant.web.undertow]] namespace includes a number of
composable functions that turn a map of various keywords into a map
containing an io.undertow.Undertow$Builder instance mapped to the
keyword, :configuration. So Undertow configuration is exposed via a
composite of these functions called [[immutant.web.undertow/options]].
For a contrived example, say we wanted our handler to run with 42
worker threads, and listen for requests on two ports, 8888 and 9999.
Weird, but possible. To do it, we'll need to pass the :port option
twice, in a manner of speaking:
(require '[immutant.web.undertow :refer (options)])
(def opts (-> (options :port 8888 :worker-threads 42)
(assoc :port 9999)
options))
(run app opts)
SSL and Java is a notoriously gnarly combination that is way outside
the scope of this guide. Ultimately, Undertow needs either a
javax.net.ssl.SSLContext or a javax.net.ssl.KeyManager[] and an
optional javax.net.ssl.TrustManager[].
You may also pass a KeyStore instance or a path to one on disk, and
the SSLContext will be created for you. For example,
(run app (immutant.web.undertow/options
:ssl-port 8443
:keystore "/path/to/keystore.jks"
:key-password "password"))
Another option is to use the less-awful-ssl library; maybe something along these lines:
(def context (less.awful.ssl/ssl-context "client.pkcs8" "client.crt" "ca.crt"))
(run app (immutant.web.undertow/options
:ssl-port 8443
:ssl-context context))
Client authentication may be specified using the :client-auth
option, where possible values are :want and :need. Or, if you're
fancy, :requested and :required.
There are three steps to enabling HTTP/2 or SPDY:
:http2? option to truealpn-boot.jar to your bootclasspathYou'll need to consult the ALPN docs to know which version of
alpn-boot.jar is appropriate for your JVM version. Most importantly,
it needs to be prepended (note the /p) to the bootclasspath, e.g.
java -Xbootclasspath/p:{/path/to/alpn-boot.jar} ...
See the Immutant Feature Demo for an HTTP/2 configuration example, including the use of a plugin to set the bootclasspath for REPL development.
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))
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, HTTP streams, and Server-Sent Events are all enabled by the [[immutant.web.async/as-channel]] function, which should be called from your Ring handler, as it takes a request map and some callbacks and returns a valid response map. Its polymorphic design enables graceful degradation from bidirectional WebSockets to unidirectional chunked responses, e.g. streams. In either case, data is sent from the server using [[immutant.web.async/send!]].
It's important to note that as-channel returns a normal Ring
response map, so it's completely compatible with Ring middleware that
might affect other entries in the response, allowing you to assoc
:status, :headers, etc on to it. The only requirement is that the
:body entry needs to be ultimately returned by any downstream
middleware.
The signatures of the callback functions supported by as-channel are
as follows:
:on-open (fn [channel])
:on-close (fn [channel {:keys [code reason]}])
:on-error (fn [channel throwable])
:on-message (fn [channel message])
The :on-message handler is only relevant to WebSockets, as are the
:code and :reason keys passed to :on-close: they will be nil for
HTTP streams.
Creating chunked responses is straightforward, as the following Ring handler demonstrates:
(require '[immutant.web.async :as async])
(defn app [request]
(async/as-channel request
{:on-open (fn [stream]
(dotimes [msg 10]
(async/send! stream (str msg) {:close? (= msg 9)})
(Thread/sleep 1000))})))
(run app)
When a client connects to our app, the :on-open handler is
asynchronously called with the appropriate channel. Our contrived
callback sends a number to the client every second. On the 10th time
it sets the :close? option to true. Its default value is false,
causing the channel to remain open after the data is sent.
If you don't know the status or headers that you need to send until
the send! call, you can pass a map of the form {:body msg :status code :headers [...]} in place of the message, but only on the first
send to that channel. A :status or :headers value in that map will
override the :status or :headers returned by the Ring handler
invocation that called as-channel.
The message passed to send! (or the :body of a map passed to
send!) can be any of the standard Ring body types (String, File,
InputStream, ISeq), as well as byte[].
To support graceful client degradation, WebSockets are coded exactly
like HTTP Streams, except that an additional callback option is
supported, :on-message, for bidirectional communication.
(def callbacks
{:on-message (fn [ch msg]
(async/send! ch (.toUpperCase msg)))})
(defn app [request]
(async/as-channel request callbacks))
(run app)
The message passed to send! can be any of the standard Ring body
types (String, File, InputStream, ISeq), as well as
byte[]. Note that each entry in an ISeq will pass through send!,
so will be sent as at least one message (more if the entry itself is a
type that triggers multiple messages). Files and InputStreams may
also be broken up in to multiple messages if they are too large (we
hint that they should be sent as up to 16KB messages, but the actual
sizes of the messages may vary, depending on the WildFly or Undertow
heuristics and configuration).
You can identify a WebSocket upgrade request by the presence of
:websocket? in the request map. This enables you to construct your
handlers so that they correctly respond to both normal HTTP requests
as well as WebSockets.
(defn app [request]
(if (:websocket? request)
(async/as-channel request callbacks)
(-> request
(get-in [:params "msg"])
.toUpperCase
ring.util.response/response)))
(run app)
Immutant provides a convenient Ring middleware function that encapsulates the check for the upgrade request: [[immutant.web.middleware/wrap-websocket]].
(web/run (-> my-app
(wrap-websocket callbacks)))
But using wrap-websocket means losing the request closure in your
Ring handler, representing the original WebSocket upgrade request from
the client. You can still access it, however, with
[[immutant.web.async/originating-request]].
Note the :path argument to [[immutant.web/run]] applies to both the
Ring handler and the WebSocket, distinguished only by the request
protocol. Given a :path of "/foo", for example, you'd have both
http://your.host.com/foo and ws://your.host.com/foo.
Server-Sent Events are a stream of specially-formatted chunked
responses with a Content-Type header of text/event-stream. The
[[immutant.web.sse]] namespace provides its own send! and
as-channel functions that are composed from their
[[immutant.web.async]] counterparts. Events are polymorphic: any
Object other than a Collection or Map is considered a simple
data field that will be string-ified, prefixed with "data:", and
suffixed with "\n". A Collection represents a multi-line data field.
And a Map is expected to contain at least one of the following keys:
:event, :data, :id, and :retry.
Let's modify the HTTP streaming example to use SSE:
(require '[immutant.web.sse :as sse])
(defn app [request]
(sse/as-channel request
{:on-open (fn [stream]
(dotimes [e 10]
(sse/send! stream e)
(Thread/sleep 1000))
(sse/send! stream {:event "close", :data "bye!"}))}))
(run app)
Because we're using sse/send! the client will receive
newline-delimited messages formatted with field names, e.g.
data: 0
data: 1
...
data: 8
data: 9
event: close
data: bye!
And note that most EventSource clients will attempt to reconnect if the server closes the connection, so instead we send a special "close" event on which our client can dispatch to initiate the close.
Calling send! (sse/ or async/) is an async operation - the send
is immediately queued, and send! returns to the caller. To know when
the send has completed, you can provide an :on-success callback. You
can also provide an :on-error callback to know when an error occurs:
(async/send! ch a-message
:on-success #(println "yay!")
:on-error (fn [e] (println "boo!" e)))
We maintain a Leiningen project called the Immutant Feature Demo demonstrating all the Immutant namespaces, including simple examples of the features described herein.
You should be able to clone it somewhere, cd there, and lein run.
Have fun!
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |