Overview
This tutorial demonstrates how Mongoose Library can be used to implement JSON-RPC functionality over WebSocket:
- We start a server that hosts a simple web page
- The user requests this page with a browser, which processes its contents
- The JavaScript code in the web page starts a WebSocket connection to our server
- The web page and our backend exchange JSON-RPC frames over that WebSocket connection
- The web page receives real-time notifications (RPC frames without
id
) - The web page can send RPC requests to call RPC functions on the backend
Full source code for this tutorial is at https://github.com/cesanta/mongoose/tree/master/examples/json-rpc-over-websocket
Why JSON-RPC
JSON-RPC is a remote procedure call protocol encoded in JSON. Here is an example of a request and its response:
{"id": 12, "method": "sum", "params": [2,3]} // Request
{"id": 12, "result": 5} // Response
One might ask, why do we need it? Are RESTful capabilities not enough? RESTful is doing the same thing, request/response, and it can also wrap data into JSON, right? Right. But there are a few differences, some of them can be crucial and hence be a decision factor.
- Notifications. With RESTful, there is no straight way to send
notifications from server to client. With JSON-RPC, any side at any point can
send requests. JSON-RPC requests without
id
do not require responses, thus, they can be treated as notifications. - Performance. A single WebSocket connection, once established, can be used to send multiple requests in either direction. With RESTful, requests are either pipelined or require multiple connections.
In this tutorial, we present a simple JavaScript JSON-RPC client and a backend server implementation. The client can send requests to the server and receive asynchronous real-time notifications as well.
Build and test
Assuming we're on a Mac or Linux workstation, start a new terminal and execute the following to build and start the example:
- If you've not already done so, clone the Mongoose Library repo
$ git clone https://github.com/cesanta/mongoose
- Build the example, this will also start our server:
$ cd mongoose/examples/json-rpc-over-websocket $ make clean all
- Start your browser on
http://localhost:8000
and you should see a simple web page:

Click on the "connect" button. The JavaScript code on the web page starts a JSON-RPC instance, which makes a WebSocket connection to the server.
Wait for a couple of seconds, you'll see a notification from the server:

- Now click on the "Calculate" buttons. Each click will trigger a JSON-RPC request. In the event
log, you can see the response frames. Notice that for each request, a new
id
is generated. This allows the client to differentiate responses, because they could come out of order.

- Notice the log messages on the terminal window that runs the server:
2021-11-23 10:05:06 2 main.c:52:fn [{"id":0,"method":"sum","params":[1,2]}] -> [{"id":0,"result":3}] 2021-11-23 10:05:09 2 main.c:52:fn [{"id":1,"method":"mul","params":[2,3]}] -> [{"id":1,"result":6}]
Client side
First, let's see what's on the client side. The JavaScript code in the browser is simple and straightforward. We get the elements that are present on the page. On a "connect" button click, we start an RPC instance:
The JSON-RPC client code is located in a different file:
The jsonrpc()
call accepts a WebSocket URL, and 3 callbacks: when a WebSocket
connection opens, when it closes, and when a JSON-RPC notification arrives.
It returns an object with two methods, close()
and call()
. The close()
method simply closes the WebSocket connection - we need that to handle when the user clicks on
the "disconnect" button. The call()
method is triggered when the user presses one of
the "Calculate" buttons. Note that call parameters are hardcoded just to
make the example simpler:
Server side
The server side is a bit more involved, as we have to parse the JSON-RPC requests and handle the different methods we support.
The server initialization is similar to what we've seen on the WebSocket server tutorial.
Here, we also initialized a timer to handle sending notifications, as can be seen on the timers tutorial. This timer will kick in every 5 seconds and send JSON-RPC notifications to all connected clients:
To craft these JSON messages, we take advantage of the function mg_mprintf(). It is similar to printf(), though it returns an allocated memory buffer, which we free at the end of the timer callback handler. Here we use a convenient extension to the syntax, in which the identifier Q is used to print double-quoted JSON-escaped strings.
The event handler is also quite similar to what we've seen on the WebSocket server tutorial:
If a connection is still HTTP, we upgrade it to WebSocket if the URI
is /websocket
; otherwise we serve static content from the web_root
directory.
When a WebSocket connection gets established, we set a mark in c->label
in order to
let the timer function know who should be notified.
Finally, when a WebSocket frame is received from a client, we call our local function
process_json_message()
, which will parse the JSON-RPC message and call the requested method handler function. This will in turn parse the parameters and create a result, which process_json_message()
will turn into a full response and send it back to the client.
The JSON API
The Mongoose library includes a set of functions to make life easier when dealing with JSON messages. We've already introduced mg_mprintf()
above, let's focus on some of the JSON parsing functions:
int mg_json_get(const char *buf, int len, const char *path, int *toklen);
bool mg_json_get_num(struct mg_str json, const char *path, double *v);
bool mg_json_get_bool(struct mg_str json, const char *path, bool *v);
char *mg_json_get_str(struct mg_str json, const char *path);
The function mg_json_get()
will parse a JSON string in a buffer pointed to by parameter buf
, with length len
, searching for the element specified by the JSON path path
. If successful, it will store the element length at placeholder toklen
and return its offset inside the buffer. Otherwise, it will return an error code.
The other three functions make use of an internal Mongoose structure, mg_str. This form to represent a string is used in many places in Mongoose, for example the struct mg_ws_message we received in the listing above as the parameter for the event MG_EV_WS_MSG
uses it.
We can convert a NULL-terminated C string into a struct mg_str
with the function mg_str(), or we can also use mg_str_n() that requires us to specify a length and handles non-NULL-terminated strings.
The function mg_json_get_num()
will parse a JSON string in a struct mg_str
, searching for a valid numeric value at the JSON path path
. If successful, it will store its value at placeholder v
and return true; returning false otherwise.
The function mg_json_get_bool()
is similar, though it searches for a boolean value.
The function mg_json_get_str()
will parse a JSON string in a struct mg_str
, searching for a string value at JSON path path
. If successful, it will allocate memory, un-escape the string, and return a pointer to it; returning NULL otherwise. The caller must free the buffer later.
For detailed information, see the documentation.
JSON-RPC processor
Once we got ourselves a JSON message over our WebSocket connection, we will proceed to parse it. Our request messages have this format:
{"id": 12, "method": "sum", "params": [2,3]}
We first search for the method, get the id, and the parameter list.
Then we validate the format and return an error indication on failure.
If everything looks OK, we compare the requested method to those we support, and if found we call its respective function handler, that will return a string. We then frame that string into a response JSON message and send it over the WebSocket connection. On exit, we free the returned string buffers.
Finally, the two functions for our supported methods, sum()
and mul()
, to add and multiply numbers respectively.
We extract each parameter from the parameter list, and return the result as a string that will be freed by the caller.