Mux.jl gives your Julia web services some closure. Mux allows you to define servers in terms of highly modular and composable components called middleware, with the aim of making both simple and complex servers as simple as possible to throw together.
using Mux @app test = ( Mux.defaults, page(respond("
Hello World!")), page("/about", probabilty(0.1, respond("
About Me")), page("/user/:user", req -> "
Hello, $(req[:params][:user])!"), Mux.notfound()) serve(test)
You can run this demo by entering the successive forms into the Julia
REPL. The code displays a "hello, world" at
localhost:8000, with an
about page at
/about and another hello at
@app macro allows the server to be redefined on the fly, and you
can test this by editing the
hello text and re-evaluating. (don't
Mux.jl is at heart a control flow library, with a very small core. It's not important to understand that code exactly as long as you understand what it achieves.
There are three concepts core to Mux.jl: Middleware (which should be familiar from the web libraries of other languages), stacking, and branching.
An app or endpoint is simply a function of a request which produces a response:
function myapp(req) return "
Hello, $(req[:params][:user])!" end
In principle this should say "hi" to our lovely user. But we have a problem – where does the user's name come from? Ideally, our app function doesn't need to know – it's simply handled at some point up the chain (just the same as we don't parse the raw HTTP data, for example).
One way to solve this is via middleware. Say we get
:user from a cookie:
function username(app, req) req[:params][:user] = req[:cookies][:user] return app(req) # We could also alter the response, but don't want to here end
Middleware simply takes our request and modifies it appropriately, so that data needed later on is available. This example is pretty trivial, but we could equally have middleware which handles authentication and encryption, processes cookies or file uploads, provides default headers, and more.
We can then call our new version of the app like this:
In fact, we can generate a whole new version of the app which has username support built in:
function app2(req) return username(myapp, req) end
But if we have a lot of middleware, we're going to end up with a lot of
For that reason we can use the
mux function instead, which creates the new app for us:
This returns a new function which is equivalent to
app2 above. We
just didn't have to write it by hand.
Now suppose you have lots of middleware – one to parse the HTTP request into
a dict of properties, one to check user authentication, one to catches errors,
mux handles this too – just pass it multiple arguments:
mux(todict, auth, catch_errors, app)
mux returns a whole new app (a
request -> response function)
for us, this time wrapped with the three middlewares we provided.
todict will be the first to make changes to the incoming request, and
the last to alter the outgoing response.
Another neat thing we can do is to compose middleware into more middleware:
mymidware = stack(todict, auth, catch_errors) mux(mymidware, app)
This is effectively equivalent to the
mux call above, but creating a
new middleware function from independent parts means we're able to
factor out our service to make things more readable. For example, Mux
Mux.default middleware which is actually just a stack of
stack is self-flattening, i.e.
stack(a, b, c, d) == stack(a, stack(b, c), d) == stack(stack(a, b, c), d)
Mux.jl goes further with middleware, and expresses routing and decisions
as middleware themselves. The key to this is the
mux(branch(_ -> rand() < 0.1, respond("Hello")), respond("Hi"))
In this example, we ignore the request and simply return true 10% of the time. You can test this in the repl by calling
mux(branch(_ -> rand() < 0.1, respond("Hello")), respond("Hi"))(nothing)
(since the request is ignored anyway, it doesn't matter if we set it to
We can also define a function to wrap the branch
probabilty(x, app) = branch(_ -> rand() < x, app)
Despite the fact that endpoints and middleware are so important in Mux,
it's common to not write them by hand. For example,
creates a function
_ -> "hi" which can be used as an endpoint.
Equally, functions like
status(404) will create middleware which
applies the given status to the response. Mux.jl's "not found" endpoint
is therefore defined as
notfound(s = "Not found") = mux(status(404), respond(s))
which is a much more declarative approach.
respond(x)– creates an endpoint that responds with
x, regardless of the request.
route("/path/here", app)– branches to
appif the request location matches
page("/path/here", app)– branches to
appif the request location exactly matches
Mux.pkgfiles middleware (included in
Mux.defaults) serves static files under the
assets directory in any Julia package at
You can easily integrate a general HTTP server and a WebSocket server with Mux, here is an example:
Firstly, let's import some modules:
using Mux import Mux.WebSockets
Next, let's define the behavior of HTTP server and WebSocket server:
@app h = ( Mux.defaults, page("/", respond("<h1>Hello World!</h1>")), Mux.notfound()); function ws_io(x) conn = x[:socket] while !eof(conn) data = WebSockets.readguarded(conn) data_str = String(data) println("Received data: " * data_str) WebSockets.writeguarded(conn, "Hey, I've received " * data_str) end end @app w = ( Mux.wdefaults, route("/ws_io", ws_io), Mux.wclose, Mux.notfound());
Run the server:
WebSockets.serve( WebSockets.ServerWS( Mux.http_handler(h), Mux.ws_handler(w), ), 2333);
And finally, in a separate Julia process, run a client:
using Mux import Mux.WebSockets WebSockets.open("ws://localhost:2333/ws_io") do ws_client WebSockets.writeguarded(ws_client, "Hello World") data, success = WebSockets.readguarded(ws_client) if success println(stderr, ws_client, " received:", String(data)) end end
Now, if you run both programs, you'll see a
Hello World message, as the
server is replying the same message back to the client.
2 days ago