socket-json-rpc is a Clojure library that simplifies creating async JSON-RPC servers and clients that communicate using TCP sockets.
First, require the server:
(require '[socket-json-rpc.server :as server])
Procedures are defined with defprocedure
, and is used in the form
(server/defprocedure [name] [[named-args]] [body])
The named-args argument tells the server how to arrange named arguments into a vector so that the body can use the arguments in a consistent manner. For example, the following procedure allows both unnamed and named argument usage.
(server/defprocedure subtract
["minuend" "subtrahend"]
(server/respond (- (first args) (second args))))
Note: The body automatically receives the variables args
and notification
.
args
is a vector of supplied arguments, matching the order specified in the
named-args
vector supplied when the procedure was defined. notification
is a
boolean value that is true when the client does not want to receive a reply with
the result of the procedure call. If the procedure does not respect the
notification
variable, the server will still not send the result anyway.
Note: Normally, the number of unnamed arguments supplied must match the number
of named arguments supplied in the procedure definition, but to allow for
variable arguments, the special named argument "..."
can be used. For example:
(server/defprocedure add
["a" "..."]
(server/respond (reduce + args)))
The client must still provide at least the number of named arguments other than
"..."
. In the previous example, at least one number would have to be specified.
The subtract
method can be called with either of the following JSON:
// Unnamed arguments
{
"jsonrpc": "2.0",
"method": "subtract",
"params": [42, 23],
"id": 1
}
// Named arguments
{
"jsonrpc": "2.0",
"method": "subtract",
"params": {
"subtrahend": 23,
"minuend": 42
},
"id": 1
}
And both would return the same result:
{
"jsonrpc": "2.0",
"result": "19",
"id": 1
}
The server can then be started in one of two ways:
; This version blocks the calling thread, so it won't close.
(defn -main
[& args]
...
(server/start [port] [backlog] [bind-addr]))
; This version allows the calling thread to continue immediately, meaning that
; if the main thread closes, the server will also be shut down.
(defn -main
[& args]
...
(server/start-async [port] [backlog] [bind-addr])
...)
The only required argument to either form is port
. backlog
specifies how many
pending connections should be buffered, and is 50 by default. bind-addr
specifies the host address that the server should listen on.
First, require the client:
(require '[socket-json-rpc.client :as client :refer (notify)])
The client is very simple, only exposing one macro - with-server
, and is used
in the form
(client/with-server [host] [port] [body])
This form allows the client to call functions on the server almost as if they
were local functions. For example, our subtract
procedure defined above could
be called on a local server running on port 9001
by either:
Using the ordered argument form:
(client/with-server "localhost" 9001
(subtract 42 23))
Or, using the named argument form:
(client/with-server "localhost" 9001
(subtract :subtrahend 23 :minuend 42))
Both forms would return the same result:
[(false 19)]
with-server
returns a vector of function results. Each item in the vector is a
list containing
'([error]
[result] || [code] [message])
; i.e. successful function call
'(false 19)
; i.e. error condition
'(true -32601 "Method not found")
A remote procedure call can be wrapped in notify
if you aren't interested in
receiving a reply with the result of the function call.
Multiple functions can be grouped together into a single "batch" by calling them
successively inside the with-server
block. Note that each function will be
called on the server in-order.
(client/with-server "localhost" 9001
(subtract 42 23)
(add 1 2 3 4 5)
(subtract :subtrahend 42 :minuend 23)
(notify (add 15 15))
(doesntexist true))
; => [(false 19)
; (false 15)
; (false -19)
; (true -32601 "Method not found")]
Notice that the return values arrive in the same order as the calls were delivered.
ClassCastException java.lang.String cannot be cast to clojure.lang.Associative
This usually means that you forgot to add (respond [val])
around your procedure's
return value.
Copyright © 2017 Carl Albrecht
Distributed under the Eclipse Public License version 1.0