Cutelyst 2.4.0 released
Cutelyst, the C++/Qt web framework got another up.
This release includes:
- Fix for our EPoll event loop spotted by our growing community
- An Sql query conversion optimization (also from community), we have helper methods to create QVariantHash, QVariantMap, QVariantList... from QSqlQuery to be used by Grantlee templates
- New Sql query conversion to JSON types (QJsonObject, QJsonArray..) making it easier and faster to build REST services as shown on our blog post about Creating RESTful applications with Qt and Cutelyst
- New boolean methods to test for the HTTP method used (isPUT(), isDelete()...) easing the work on REST apps
- Fix for our CPU affinity logic, thanks to the TechEmpower benchmarks I found out that when creating many workers process they were all being assigned to a single CPU core, I knew that pre-fork was usually slower than threads on modern CPUs but was always intrigued why the difference was so big. Hopefully it will be smaller now.
Since I'm currently building a REST service OAuth2 seems to be something that would be of good use, one user has started working on this, hopefully this will soon be ready.
Have fun https://github.com/cutelyst/cutelyst/releases/tag/v2.4.0
Creating RESTful applications with Qt and Cutelyst
This mini tutorial aims to show you the fundamentals of creating a RESTful application with Qt, as a client and as a server with the help of Cutelyst.
Services with REST APIs have become very popular in recent years, and interacting with them may be necessary to integrate with services and keep your application relevant, as well as it may be interesting to replace your own protocol with a REST implementation.
REST is very associated with JSON, however, JSON is not required for a service to become RESTful, the way data is exchanged is chosen by the one who defines the API, ie it is possible to have REST exchanging messages in XML or another format. We will use JSON for its popularity, simplicity and due to the QJsonDocument class being present in the Qt Core module.
A REST service is mainly characterized by making use of the little-used HTTP headers and methods, browsers basically use GET to get data and POST to send form and file data, however REST clients will use other methods like DELETE, PUT and HEAD, concerning headers many APIs define headers for authentication, for example X-Application-Token can contain a key generated only for the application of a user X, so that if this header does not contain the correct data it will not have access to the data.
Let's start by defining the server API:
- /api/v1/users
- GET - Gets the list of users
- Answer: ["uuid1", "uuid2"]
- POST - Register new user
- Send: {"name": "someone", "age": 32}
- Answer: {"status": "ok / error", "uuid": "new user uuid", "error": "msg in case of error"}
- GET - Gets the list of users
- /api/v1/users/ - where UUID should be replaced by the user's UUID
- GET - Gets user information
- Answer: {"name": "someone", "age": 32}
- PUT - Update user information
- Send: {"name": "someone", "age": 57}
- Answer: {"status": "ok / error", "error": "msg in case of error"}
- DELETE - Delete user
- Answer: {"status": "ok / error", "error": "msg in case of error"}
- GET - Gets user information
For the sake of simplicity we will store the data using QSettings, we do not recommend it for real applications, but Sql or something like that escapes from the scope of this tutorial. We also assume that you already have Qt and Cutelyst installed, the code is available at https://github.com/ceciletti/example-qt-cutelyst-rest
Part 1 - RESTful Server with C ++, Cutelyst and Qt
First we create the server application:
$ cutelyst2 --create-app ServerREST
And then we will create the Controller that will have the API methods:
$ cutelyst2 --controller ApiV1
Once the new class has been instantiated in serverrest.cpp, init() method with:
#include "apiv1.h" bool ServerREST::init() { new ApiV1 (this); ...
Add the following methods to the file "apiv1.h"
C_ATTR(users, :Local :AutoArgs :ActionClass(REST)) void users(Context *c); C_ATTR(users_GET, :Private) void users_GET(Context *c); C_ATTR(users_POST, :Private) void users_POST(Context *c); C_ATTR(users_uuid, :Path('users') :AutoArgs :ActionClass(REST)) void users_uuid(Context *c, const QString &uuid); C_ATTR(users_uuid_GET, :Private) void users_uuid_GET(Context *c, const QString &uuid); C_ATTR(users_uuid_PUT, :Private) void users_uuid_PUT(Context *c, const QString &uuid); C_ATTR(users_uuid_DELETE, :Private) void users_uuid_DELETE(Context *c, const QString &uuid);
The C_ATTR macro is used to add metadata about the class that the MOC will generate, so Cutelyst knows how to map the URLs to those functions.
- :Local - Map method name to URL by generating /api/v1/users
- :AutoArgs - Automatically checks the number of arguments after the Context *, in users_uuid we have only one, so the method will be called if the URL is /api/v1/users/any-thing
- :ActionClass(REST) - Will load the REST plugin that will create an Action class to take care of this method, ActionREST will call the other methods depending on the called method
- :Private - Registers the action as private in Cutelyst, so that it is not directly accessible via URL
This is enough to have an automatic mapping depending on the HTTP method for each function, it is important to note that the first function (without _METHOD) is always executed, for more information see the API of ActionREST
For brevity I will show only the GET code of users, the rest can be seen in GitHub:
void ApiV1::users_GET(Context *c) { QSettings s; const QStringList uuids = s.childGroups(); c->response()->setJsonArrayBody(QJsonArray::fromStringList(uuids)); }
After all the implemented methods start the server:
cutelyst2 -r --server --app-file path_to_it
To test the API you can test a POST with curl:
curl -H "Content-Type: application/json" -X POST -d '{"name": "someone", "age": 32}' http://localhost:3000/api/v1/users
Okay, now you have a REST server application, made with Qt, with one of the fastest answers in the old west :)
No, it's serious, check out the benchmarks.
Now let's go to part 2, which is to create the client application that will consume this API.
Part 2 - REST Client Application
First create a QWidgets project with a QMainWindow, the goal here is just to see how to create REST requests from Qt code, so we assume that you are already familiar with creating graphical interfaces with it.
Our interface will be composed of:
- 1 - QComboBox where we will list users' UUIDs
- 1 - QLineEdit to enter and display the user name
- 1 - QSpinBox to enter and view user age
- 2 - QPushButton
- To create or update a user's registry
- To delete the user record
Once designed the interface, our QMainWindow sub-class needs to have a pointer to QNetworkAccessManager, this is the class responsible for handling communication with network services such as HTTP and FTP. This class works asynchronously, it has the same operation as a browser that will create up to 6 simultaneous connections to the same server, if you have made more requests at the same time it will put them in a queue (or pipeline them if set).
Then create a QNetworkAccessManager *m_nam; as a member of your class so we can reuse it. Our request to obtain the list of users will be quite simple:
QNetworkRequest request(QUrl("http://localhost:3000/api/v1/users")); QNetworkReply *reply = m_nam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply] { reply->deleteLater(); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const QJsonArray array = doc.array(); for (const QJsonValue &value : array) { ui->uuidCB->addItem(value.toString()); } });
This fills with the data via GET from the server our QComboBox, now we will see the registration code which is a little more complex:
QNetworkRequest request(QUrl("http://localhost:3000/api/v1/users")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject obj { {"name", ui->nameLE->text()}, ("age", ui->ageSP->value()} }; QNetworkReply *reply = m_nam->post(request, QJsonDocument(obj).toJson()); connect(reply, &QNetworkReply::finished, this, [this, reply] { reply->deleteLater(); const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll()); const QJsonObject obj = doc.object(); if (obj.value("status").toString() == "ok") { ui->uuidCB->addItem(obj.value("uuid").toString()); } else { qWarning() << "ERROR" << obj.value("error").toString(); } });
With the above code we send an HTTP request using the POST method, like PUT it accepts sending data to the server. It is important to inform the server with what kind of data it will be dealing with, so the "Content-Type" header is set to "application/json", Qt issues a warning on the terminal if the content type has not been defined. As soon as the server responds we add the new UUID in the combobox so that it stays up to date without having to get all UUIDs again.
As demonstrated QNetworkAccessManager already has methods ready for the most common REST actions, however if you wanted to send a request of type OPTIONS for example will have to create a request of type CustomOperation:
m_nam->sendCustomRequest("OPTIONS", request);
Did you like the article? Help by giving a star to Cutelyst and/or supporting me on Patreon
Cutelyst 2.3.0 released
Cutelyst - The C++ Web Framework built with Qt, has a new release.
In this release a behavior change was made, when asking for POST or URL query parameters and cookies that have multiple keys the last inserted one (closer to the right) is returned, previously QMap was filled in reverse order so that values() would have them in left to right order. However this is not desired and most other frameworks also return the last inserted value. To still have the ordered list Request::queryParameters("key") builds a list in the left to right order (while QMap::values() will have them reversed).
Some fixes on FastCGI implementation as well as properly getting values when uWSGI FastCGI protocol was in use.
Lastly a crash when performing TechEmpower benchmarks resulted in the removal of some nested event loops calls that were creating a huge stack, and made me realize that my proposed methods to make async apps easier were broken by design, basically you can't use QEventLoop::exec() to wait for events. So Context::wait() and next() are deprecated and the code disabled, with the exception of WebSockets until I can make some changes to HTTP/1 and FastCGI parser both are to be considered synchronous.
Removing a QCoreApplication::processEvents() that was added in order to try to give other connections an execution slice almost doubled the speed in plain text when benchmarking locally, using a single core the requests per second have gone from 105 K to 190 K, after understanding the stupidity I did this isn't of much surprise :)
Virtlyst 1.1.0 released
Virtlyst - a libvirt web interface to manage virtual machines has a new release.
This release finishes support to connect to TCP or TLS virtd servers, it also fixes creating new instances from the flavor panel. And a few other fixes.
Of course I forgot to add the mandatory screenshot last time so here it goes:
Go get it! https://github.com/cutelyst/Virtlyst/releases/tag/v1.1.0
If you like this software you can also support my work via patreon.
Cutelyst 2.2.0 is out
Cutelyst just got a new release!
Thanks to the release of Virtlyst - a web manager for virtual machines, several bugs were found and fixed in Cutelyst, the most important one in this release is the WebSockets implementation that broke due the addition of HTTP/2 to cutelyst-wsgi2.
Fixing this was quite interesting as when I fixed the issue the first time, it started to make deleteLater() calls on null objects which didn't crash but since Qt warn's you about that it was hurting performance. Then I fixed the deleteLater() calls and WebSockets broke again :) a little of gdb foo and I found out that when I asked for the websocket to close Qt was emitting disconnected as a result of that call, and that signal was deleting the object we rely when we called close, which crashed the app.
As a new feature Context class has two new methods to help with async apps, a wait() method that creates a counter based local event loop and a next() method to decrease the event loop counter till quitting it. This way you can for example connect two QNetworkReply::finished() signals to Context::next() and call Context::wait(2); so once the two requests are finished wait() returns and the client receives the data.
Codacy was also added to increase the number of checks in our code base, and it has already shown some real issues.
UPDATE: Right after release I found a cppcheck code fix broke listening on TCP sockets, yes, writing more unit tests especially for the WSGI module is top on my TODO list. So 2.2.1 is out now :)
Get it here https://github.com/cutelyst/cutelyst/releases/tag/v2.2.1