{% extends "clj_wamp_example/views/layout.html" %} {% block content %}
This guide walks through the basics of clj-wamp and the WAMP subprotocol.
Let's create a new clj-wamp project from scratch so you can see it in action.
The guide assumes you already have java and leiningen installed on your system.
From the command line, run the following commands:
% lein new clj-wamp wamptutorial Created new clj-wamp project: wamptutorial % cd wamptutorial % lein run
...and point your browser to localhost:8080, where you'll see:
Open your browser's web developer console, and click the buttons.
You should see console debug messages of the calls being sent and events received.
You'll have a better idea of what's happening if we take a quick look at the WAMP protocol itself...
The WAMP specification defines 9 types of messages that are used between client and server. They are categorized into 3 groups: Auxiliary, RPC, and PubSub
Message | Type ID | Direction | Category |
---|---|---|---|
WELCOME | 0 | Server-to-client | Auxiliary |
PREFIX | 1 | Client-to-server | Auxiliary |
CALL | 2 | Client-to-server | RPC |
CALLRESULT | 3 | Server-to-client | RPC |
CALLERROR | 4 | Server-to-client | RPC |
SUBSCRIBE | 5 | Client-to-server | PubSub |
UNSUBSCRIBE | 6 | Client-to-server | PubSub |
PUBLISH | 7 | Client-to-server | PubSub |
EVENT | 8 | Server-to-client | PubSub |
All WAMP messages are transmitted as JSON UTF-8 encoded strings to/from the server.
For example, here is a WELCOME message that the server sends to the client once a connection has been established:
[0,"1372019248579-1",1,"clj-wamp/1.0.0"]
Which is defined as:
[ TYPE_ID_WELCOME , sessionId , protocolVersion, serverIdent ]
For more details on the WAMP specification, check out the WAMP.ws documentation. It's good stuff.
We will cover some of the message types later in the tutorial.
The clj-wamp new project template generates several files, but only two files really apply to this tutorial:
The JavaScript client: resources/public/index.html
and the Clojure server: src/wamptutorial/websocket.clj
wamptutorial ├── README.md ├── project.clj ├── resources │ └── public │ └── index.html <- *client* ├── resources-dev │ ├── config.clj │ └── log4j.properties └── src └── wamptutorial ├── config.clj ├── main.clj ├── routes.clj └── websocket.clj <- *server*
The meat of the client code is the AutobahnJS JavaScript library for WAMP WebSocket communication.
AutobahnJS has some neat features, like:
To connect to a WAMP WebSocket server, specify the server URI, options, and connect/disconnect callbacks:
ab.connect( // WAMP server 'ws://wamp-server-uri', // on connect function (sess) { ... }, // on disconnect function (code, reason) { ... }, // options {'maxRetries': 60, 'retryDelay': 30000, 'skipSubprotocolCheck': true} );
On connect, the AutobahnJS library will automatically process
the WELCOME
message. The session ID is stored in the session object,
and can be retrieved via sess.sessionid()
.
The server uses clj-wamp's http-kit-handler
to define
callbacks for the open, close, call, and pubsub events.
This wamp-handler
attaches to a
Compojure
route in src/wamptutorial/routes.clj
, which provides
the WebSocket endpoint: ws://localhost:8080/ws
Now that you've gotten a high-level overview of everything, let's take a closer look into WAMP's primary message categories.
In the next sections you will find that the PubSub topics and RPC procedures are identified in the client with CURIEs (Compact URI Expressions).
For example:
sess.prefix("event", "http://wamptutorial/event#"); sess.subscribe("event:chat", onChatEvent);
The "event:chat"
string in the subscribe call is a CURIE
of the "http://wamptutorial/event#chat"
URI.
The PREFIX
message allows the client to abbreviate
fully qualified URIs in order to reduce communication volume with the
server: [1,"event","http://wamptutorial/event#"]
See the PREFIX Message docs @ WAMP.ws for more info.
The following is a basic example of PubSub events with clj-wamp, where
the server relays all PUBLISH
messages to subscribed clients, as-is.
In the client, during connection of the WebSocket, we will
subscribe an event handler for the event:chat
topic:
...and upon click of the chat button, a PUBLISH
message
will be sent to the server and broadcast out to all subscribed
clients. Open up an additional browser window and watch the events
be delivered to both.
Taking a look at the server code:
The boolean in the :on-subscribe map
{(evt-url "chat") true}
tells clj-wamp to allow all clients to subscribe
to this topic.
Similarly, the boolean in the :on-publish map tells clj-wamp to relay all received messages to subscribed clients.
Note:
You can also use a callback function, instead of a boolean, to restrict
certain clients from subscribing, or to broker/rewrite messages that are published.
See the
API docs
for more information, specifically the on-subscribe-fn?
and on-publish-fn
callbacks.
In our example, if we were to trace the PubSub messages from the client to the server and back again, we would see the following order of events:
SUBSCRIBE
message to the server:
[5,"event:chat"]
SUBSCRIBE
message, and adds the
client to its internal list of subscribers on the "event:chat" topic.PUBLISH
message to the server:
[7,"event:chat","foo"]
[via sess.publish("event:chat", "foo");
]PUBLISH
message and checks it's map for a topic match.
Since there is a match with a value of true
, the server
allows the EVENT
message to be sent to all subscribed clients:
[8,"http://wamptutorial/event#chat","foo"]
EVENT
message (since it's subscribed), and the
onChatEvent
JS handler is called.Also note that during a client-side publish, you have additional options for whitelisting and blacklisting other clients.
RPC calls are different than PubSub messages in that they apply only to the server and the one client who made the call.
The following is a example of a remote "echo" procedure call, where the parameter is echoed back to the client in the response.
In the client, upon click of a button, we send a CALL
message to the server:
The sess.call()
method returns a promise that will trigger callbacks upon a
CALLRESULT
or CALLERROR
response.
In the server, we've mapped an RPC topic to the function identity
:
In our example, if we were to trace the RPC messages from the client to the server and back again, we would see the following order of events:
CALL
message to the server:
[2,"0.arg98rkv3aowp14i","rpc:echo","test"]
[via sess.call("rpc:echo", "test");
]CALL
message and checks it's :on-call map for a topic match.
Since there is a match with a function, the server apply
s
the call parameters to the identity
function and returns a
CALLRESULT
message to the client:
[3,"0.arg98rkv3aowp14i","test"]
CALLRESULT
message, and triggers the
appropriate callback handler.
Note: if any exceptions are thrown during a function call on the server,
a CALLERROR
result will be sent out to the client:
[4,"0.wg3eowm7t6pf1or","http://api.wamp.ws/error#internal","internal error","An exception"]
WAMP RPC calls are asynchronous. It is possible that multiple calls could be in progress, where the client has yet to receive a result.
AutobahnJS provides a simple way of doing authentication using the WAMP protocol. Here's how it works...
authreq
is made to the server with the credential key,
and a response is sent back to the client with a challenge hash string.auth
with this digital signature.To enable WAMP-CRA in clj-wamp, add a :on-auth
map with:
:secret
A callback function for obtaining the user's secret
(typically from a database).
:permissions
A callback function that returns the PubSub/RPC permissions
for the user.While the permissions are sent back to inform the client of what is allowed, they are primarily used on the server to allow/deny access to the various RPC/PubSub topics.
If an RPC topic is not allowed, an unauthorized
error is sent back.
And if a PubSub topic is not allowed, the publish or subscription is dropped/ignored.
In the client, we issue the two authreq
and auth
RPC calls
during the connection process: