Doing RPC in 2013

Some history

18 years ago I bought a book titled “Power programming with RPC”; I was surprised to see that the 1992 book is still available now ! After reading it, at the time I was really excited about RPC (Remote Procedure Call) which is indeed a useful thing to have.

But I was also very confused about how to actually implement it. Many solutions have been proposed over the years, from Sun’s Open Network Computing (ONC) which is the technology presented in that book: CORBA (who still remembers about that ?), Microsoft DCOM and .NET, SOAP, XML-RPC, JSON-RPC, Java RMI (Remote Method Invocation), the Internet Communications Engine (ICE), Autobahn

None of these seem to fit my requirements:

In exchange for those, I can live without discovery and an Interface Description Language (IDL).

Enter Qt and QJsonRpc

But wait, since RPC is basically calling remote procedures on a server by sending over strings, Qt’s meta object system is a good basis to build on: it is portable, it has reflection to pick up the method to call from the string, it provides signals-slots to implement call-backs, it has a variant type. Qt also provides interexchangable QTcpSocket for two-way connections over the network and QLocalSocket for local connections (QLocalSocket uses a named pipe on Windows and a local domain socket on Unix).

While musing about those facts, I stumbled upon QJsonRpc, a Qt implementation of the JSON-RPC protocol available here. It is compatible with both Qt4 and Qt5, and it is licensed under the LGPLv2.1. It is developed by devonit, an innovative firm which BTW has just introduced a smart new device which plugs into the HDMI port of any monitor and transforms it into a thin client.

There is not much documentation around on QJsonRpc so to understand what this thing does, let’s try it out – here is how I did on Debian wheezy with Qt4 and on jessie with Qt5;  to set-up on a fresh install of Debian jessie, do this:

sudo apt-get install qtbase5-dev g++

Then:

wget https://bitbucket.org/devonit/qjsonrpc/get/81c851a5c77a.zip
unzip 81c851a5c77a.zip
cd devonit-qjsonrpc-81c851a5c77a/

If you are using Qt4, issue:

qmake

whereas on jessie to use Qt5, do:

/usr/lib/i386-linux-gnu/qt5/bin/qmake)

Then:

make
cd tests/manual
cat > ./localserver/runserver
#!/bin/bash
/lib/ld-linux.so.2 --library-path ../../src ./localserver/server
^D
chmod u+x ./localserver/runserver
cat > ./qjsonrpc/runqjsonrpc
#!/bin/bash
/lib/ld-linux.so.2 --library-path ../../src ./qjsonrpc/qjsonrpc $@
^D
chmod u+x ./qjsonrpc/runqjsonrpc
cat > ./localclient/runclient
#!/bin/bash
/lib/ld-linux.so.2 --library-path ../../src ./localclient/localclient
^D
chmod u+x ./localclient/runclient

Now start the local server:

./localserver/runserver

then open a new console and start the local client:

./localclient/runclient

The local client will print this:

response received: QJsonRpcMessage(type=QJsonRpcMessage::Response, id=1, result=QVariant(, ) )
response received: QJsonRpcMessage(type=QJsonRpcMessage::Response, id=2, result=QVariant(, ) )
response received: QJsonRpcMessage(type=QJsonRpcMessage::Response, id=3, result=QVariant(, ) )
response received: QJsonRpcMessage(type=QJsonRpcMessage::Response, id=4, result=QVariant(QString, "Hello matt") )

whereas the localserver will print this:

void TestService::testMethod() called
void TestService::testMethodWithParams(const QString&, bool, double) called with parameters:
first: "one"
second: false
third: 10
void TestService::testMethodWithVariantParams(const QString&, bool, double, const QVariant&) called with variant parameters:
first: "one"
second: false
third: 10
fourth: QVariant(double, 2.5)
QString TestService::testMethodWithParamsAndReturnValue(const QString&) called

To better understand what is going on, look into the code ! For example the key client-side code for the last request is (let’s skip error handling for ease of understanding):

QJsonRpcSocket *m_client = new QJsonRpcSocket(socket, this);
QJsonRpcServiceReply *reply = m_client->invokeRemoteMethod("agent.testMethod");
reply = m_client->invokeRemoteMethod("agent.testMethodWithParamsAndReturnValue", "matt");
connect(reply, SIGNAL(finished()), this, SLOT(processResponse()));

and the call-back:

void LocalClient::processResponse() {
  QJsonRpcServiceReply *reply = static_cast<QJsonRpcServiceReply *>(sender());
  qDebug() << "response received: " << reply->response();
}

whereas the key server-side code is:

TestService service;
QJsonRpcLocalServer rpcServer;
rpcServer.addService(&service);
rpcServer.listen(serviceName);

and the server-side implementation of the testMethodWithParamsAndReturnValue method:

QString TestService::testMethodWithParamsAndReturnValue(const QString &name) {
  return QString("Hello %1").arg(name);
}

Now shut down the client by hitting ^C in the client console, then launch the command-line client:

./qjsonrpc/runqjsonrpc testservice agent.testMethod

this will print:

QVariant(, )

or on Qt5:

QVariant(Invalid)

whereas

./qjsonrpc/runqjsonrpc testservice agent.testMethodWithParamsAndReturnValue matt

will print:

QVariant(QString, "Hello matt")

The command-line interface is only able to connect to a local server, but as to the server / client, there is also a demo over TCP on port 5555.
If we diff the localclient (using Qt’s QLocalSocket) and the tcpclient (using QTcpSocket), there are very few differences, the main one is:

< QLocalSocket *socket = new QLocalSocket(this);
< socket->connectToServer(serviceName);
---
> QTcpSocket *socket = new QTcpSocket(this);
> socket->connectToHost(QHostAddress::LocalHost, 5555);

where serviceName is a QString obtained by calling QDir::absoluteFilePath on the file /tmp/testservice.

Diffing the server also shows very few differences, the main one is:

< if (!rpcServer.listen(serviceName)) {
---
> if (!rpcServer.listen(QHostAddress::LocalHost, 5555)) {

and the testservice implementation is actually the same !

Enter Websockets and QWebSockets

Now connecting over a WAN over port 5555 with a custom protocol calls for firewall troubles. Your IT people will complain they have to open new ports and your customer’s antivirus will harass them. Also no other application on earth will talk to your server.

Better use Websockets, a web technology providing full-duplex communications channels over a single TCP connection over port 80, configured as an upgrade to the HTTP protocol, and compatible with a number of browsers (Google Chrome, Internet Explorer, Firefox, Safari and Opera) which can act as clients if you develop an HTML5 interface.

And lo ! I stumbled upon QWebSockets, a pure Qt implementation of client and server WebSockets; it has no other dependencies that Qt5 and it is licensed under LGPL v2.1. One painful limitation is that it currently lacks the WSS protocol (the secure version of the ws:// protocol); but since it’s open-source, maybe somebody will care to implement it !

Here is how I tried it out on Debian jessie with Qt5:

wget https://codeload.github.com/KurtPattyn/QWebSockets/legacy.tar.gz/master
tar xzf KurtPattyn-QWebSockets-v0.9.0-97-g2f6831e.tar.gz
cd KurtPattyn-QWebSockets-2f6831e
/usr/lib/i386-linux-gnu/qt5/bin/qmake
make

now let’s test the provided example:

cd examples
./echoserver/echoserver

prints:

Echoserver listening on port 1234

then in a new console:

./echoclient/echoclient

prints:

Websocket connected
Message received: "Hello, world!"

Both libraries are LGPL licensed, so it would be great to set up a QJsonRpcWebServer based on the QJsonRpcTcpServer from QJsonRpc, to use the QWebSocketServer from QWebSockets in place of Qt’s builtin QTcpServer.