Overview

This tutorial demonstrates how Mongoose Library can be used to implement JSON-RPC functionality over Websocket:

  • Start a server that hosts a simple web page
  • A web page starts a Websocket connection to the server
  • On that websocket connection, a web page and a backend exchange JSON-RPC frames
  • A web page receives a real-time notifications (RPC frame without id)
  • A web page can send RPC requests to a call RPC functions on the backend

A 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 request and response:

{"id": 12, "method": "Sum", "params": [2,3]}   // Request
{"id": 12, "result": 5}                        // Response

One might ask, why do we need it? Isn't 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 few differences, some of them can be crucial and 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 implementation that 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:

$ git clone https://github.com/cesanta/mongoose
$ cd mongoose/examples/json-rpc-over-websocket
$ make
cc main.c mjson.c ../../mongoose.c -I../.. -W -Wall -DMG_ENABLE_LINES=1  -o example
./example 
Starting WS listener on ws://localhost:8000/websocket

Start your browser on URL http://localhost:8000 - and you should see a simple web page:

demo web page

Click on "connect" button. A 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, see a notification from the server:

JSON-RPC notification

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

WS server

Notice log message 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. A JavaScript code in the browser is simple and straightforward. We get get elements that present on the page. On a "connect" button click, start a 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 return an object with two methods, close() and call(). The close() method simply closes Websocket connection - we need that if user clicks on a "disconnect" button. The call() method is triggered when user presses on a "calculation" buttons. Note that call parameters are hardcoded just to make an example simpler:

Server side

On the server side, we use mjson library which adds mjson.h and mjson.c files to the source code. A server initialisation is as usual, with two extra additions - we register JSON-RPC instance using mjson API.

In the JSON RPC instance, we register two functions sum() and mul() to add and multiply numbers, here are their callbacks:

Another thing we do is we register a timer which kicks in every 5 seconds and sends JSON-RPC notifications to all connected clients:

At lastly, the event handler. It is quite simple:

If a connection is still HTTP, we upgrade it to a Websocket if the URI i /webscket - unless, we serve static content from a web_root directory. When a Websocket connection gets established, we set a mark in c->label to let timer function know who should be notified. When a Websocket frame is received from a client, we call jsonrpc_process() to create a response, and send it back.