Простой REST сервер на Qt с
рефлексией
Василий Сорокин
Москва 2017
Введение
● Qt и moc
● Abstract Server
● Concrete Server
● Рефлексия
● Authorization (and tags)
● Сложности/Проблемы
● Рефлексия в тестировании
● Заключение
Meta-Object Compiler
● Когда запускается
● Что делает
● Почему это важно
● Ограничения
Что должен уметь сервер?
● Получать запросы
● Разбирать данные
● Возвращать ответы
● Обрабатывать ошибки
● Авторизация
Abstract Server
#ifndef Q_MOC_RUN
# define NO_AUTH_REQUIRED
#endif
class AbstractRestServer : public QTcpServer
{
public:
explicit AbstractRestServer(const QString &pathPrefix, int port, QObject *parent = 0);
Q_INVOKABLE void startListen();
Q_INVOKABLE void stopListen();
protected:
void incomingConnection(qintptr socketDescriptor) override;
Abstract Server
void tryToCallMethod(QTcpSocket *socket, const
QString &type, const QString &method, QStringList
headers, const QByteArray &body);
QStringList makeMethodName(const QString &type,
const QString &name);
MethodNode *findMethod(const QStringList
&splittedMethod, QStringList &methodVariableParts);
void fillMethods();
void addMethodToTree(const QString &realMethod,
const QString &tag);
Abstract Server
void sendAnswer(QTcpSocket *socket, const
QByteArray &body, const QString &contentType, const
QHash<QString, QString> &headers,
int returnCode = 200, const QString &reason
= QString());
void registerSocket(QTcpSocket *socket);
void deleteSocket(QTcpSocket *socket, WorkerThread
*worker);
Abstract Server
private:
QThread *m_serverThread = nullptr;
QList<WorkerThreadInfo> m_threadPool;
QSet<QTcpSocket *> m_sockets;
QMutex m_socketsMutex;
MethodNode m_methodsTreeRoot;
int m_maxThreadsCount;
WorkerThread
class WorkerThread: public QThread
…
public:
WorkerThread(Proof::AbstractRestServer *const _server);
void sendAnswer(QTcpSocket *socket, const QByteArray &body, const
QString &contentType,
const QHash<QString, QString> &headers, int returnCode, const
QString &reason);
void handleNewConnection(qintptr socketDescriptor);
void deleteSocket(QTcpSocket *socket);
void onReadyRead(QTcpSocket *socket);
void stop();
WorkerThread
private:
Proof::AbstractRestServer* const m_server;
QHash<QTcpSocket *, SocketInfo> m_sockets;
WorkerThreadInfo
struct WorkerThreadInfo
{
explicit WorkerThreadInfo(WorkerThread *thread,
quint32 socketCount)
: thread(thread), socketCount(socketCount) {}
WorkerThread *thread;
quint32 socketCount;
};
SocketInfo
struct SocketInfo
{
Proof::HttpParser parser;
QMetaObject::Connection readyReadConnection;
QMetaObject::Connection disconnectConnection;
QMetaObject::Connection errorConnection;
};
Abstract Server implementation
static const QString NO_AUTH_TAG = QString("NO_AUTH_REQUIRED");
AbstractRestServer::AbstractRestServer(...) : QTcpServer(parent) {
m_serverThread = new QThread(this);
m_maxThreadsCount = QThread::idealThreadCount();
if (m_maxThreadsCount < MIN_THREADS_COUNT)
m_maxThreadsCount = MIN_THREADS_COUNT;
else
m_maxThreadsCount += 2;
moveToThread(m_serverThread);
m_serverThread->moveToThread(m_serverThread);
m_serverThread->start();
Abstract Server implementation
void AbstractRestServer::startListen()
{
if (!PrObject::call(this, &AbstractRestServer::startListen)) {
fillMethods();
bool isListen = listen(QHostAddress::Any, m_port);
}
}
void AbstractRestServer::stopListen()
{
if (!PrObject::call(this, &AbstractRestServer::stopListen, Proof::Call::Block))
close();
}
Make route tree
void AbstractRestServer::fillMethods() {
m_methodsTreeRoot.clear();
for (int i = 0; i < metaObject()->methodCount(); ++i) {
QMetaMethod method = metaObject()->method(i);
if (method.methodType() == QMetaMethod::Slot) {
QString currentMethod = QString(method.name());
if (currentMethod.startsWith(REST_METHOD_PREFIX))
addMethodToTree(currentMethod, method.tag());
}
}
}
Make route tree
void AbstractRestServer::addMethodToTree(const QString &realMethod,
const QString &tag)
{
QString method =
realMethod.mid(QString(REST_METHOD_PREFIX).length());
for (int i = 0; i < method.length(); ++i) {
if (method[i].isUpper()) {
method[i] = method[i].toLower();
if (i > 0 && method[i - 1] != '_')
method.insert(i++, '-');
}
} // rest_get_SourceList => get_source-list
Make route tree
QStringList splittedMethod = method.split("_");
MethodNode *currentNode = &m_methodsTreeRoot;
for (int i = 0; i < splittedMethod.count(); ++i) {
if (!currentNode->contains(splittedMethod[i]))
(*currentNode)[splittedMethod[i]] = MethodNode();
currentNode = &(*currentNode)[splittedMethod[i]];
}
currentNode->setValue(realMethod);
currentNode->setTag(tag);
}
Make route tree
class MethodNode {
public:
MethodNode();
bool contains(const QString &name) const;
void clear();
operator QString();
MethodNode &operator [](const QString &name);
const MethodNode operator [](const QString &name) const;
void setValue(const QString &value);
QString tag() const;
void setTag(const QString &tag);
private:
QHash<QString, MethodNode> m_nodes;
QString m_value = "";
QString m_tag;
};
New connection handling
void AbstractRestServer::incomingConnection(qintptr socketDescriptor) {
WorkerThread *worker = nullptr;
if (!m_threadPool.isEmpty()) {
auto iter = std::min_element(d->threadPool.begin(), d->threadPool.end(),
[](const WorkerThreadInfo &lhs, const WorkerThreadInfo &rhs)
{
return lhs.socketCount < rhs.socketCount;
});
if (iter->socketCount == 0 || m_threadPool.count() >= m_maxThreadsCount) {
worker = iter->thread;
++iter->socketCount;
}
}
New connection handling
if (worker == nullptr) {
worker = new WorkerThread(this);
worker->start();
m_threadPool << WorkerThreadInfo{worker, 1};
}
worker->handleNewConnection(socketDescriptor);
}
New connection handling
void WorkerThread::handleNewConnection(qintptr socketDescriptor) {
if (PrObject::call(this, &WorkerThread::handleNewConnection, socketDescriptor))
return;
QTcpSocket *tcpSocket = new QTcpSocket();
m_server->registerSocket(tcpSocket);
SocketInfo info;
info.readyReadConnection = connect(tcpSocket, &QTcpSocket::readyRead, this,
[tcpSocket, this] { onReadyRead(tcpSocket); }, Qt::QueuedConnection);
void (QTcpSocket:: *errorSignal)(QAbstractSocket::SocketError) =
&QTcpSocket::error;
info.errorConnection = connect(tcpSocket, errorSignal, this, [tcpSocket, this] {…},
Qt::QueuedConnection);
info.disconnectConnection = connect(tcpSocket, &QTcpSocket::disconnected, this,
[tcpSocket, this] {...}, Qt::QueuedConnection);
New connection handling
if (!tcpSocket->setSocketDescriptor(socketDescriptor)) {
m_server->deleteSocket(tcpSocket, this);
return;
}
sockets[tcpSocket] = info;
}
New connection handling
void WorkerThread::onReadyRead(QTcpSocket *socket) {
SocketInfo &info = m_sockets[socket];
HttpParser::Result result = info.parser.parseNextPart(socket->readAll());
switch (result) {
case HttpParser::Result::Success:
disconnect(info.readyReadConnection);
m_server->tryToCallMethod(socket, info.parser.method(), info.parser.uri(),
info.parser.headers(), info.parser.body());
break;
case HttpParser::Result::Error:
disconnect(info.readyReadConnection);
sendAnswer(socket, "", "text/plain; charset=utf-8", QHash<QString, QString>(), 400, "Bad
Request");
break;
case HttpParser::Result::NeedMore:
break;
}
}
Call method
void AbstractRestServer::tryToCallMethod(QTcpSocket *socket, const
QString &type, const QString &method, QStringList headers, const
QByteArray &body)
{
QStringList splittedByParamsMethod = method.split('?');
QStringList methodVariableParts;
QUrlQuery queryParams;
if (splittedByParamsMethod.count() > 1)
queryParams = QUrlQuery(splittedByParamsMethod.at(1));
MethodNode *methodNode = findMethod(makeMethodName(type,
splittedByParamsMethod.at(0)), methodVariableParts);
QString methodName = methodNode ? (*methodNode) : QString();
Call method
if (methodNode) {
bool isAuthenticationSuccessful = true;
if (methodNode->tag() != NO_AUTH_TAG) {
QString encryptedAuth;
for (int i = 0; i < headers.count(); ++i) {
if (headers.at(i).startsWith("Authorization", Qt::CaseInsensitive)) {
encryptedAuth = parseAuth(socket, headers.at(i));
break;
}
}
isAuthenticationSuccessful = (!encryptedAuth.isEmpty() && q-
>checkBasicAuth(encryptedAuth));
}
Call method
if (isAuthenticationSuccessful) {
QMetaObject::invokeMethod(this,
methodName.toLatin1().constData(), Qt::DirectConnection,
Q_ARG(QTcpSocket *,socket), Q_ARG(const
QStringList &, headers),
Q_ARG(const QStringList &, methodVariableParts),
Q_ARG(const QUrlQuery &, queryParams),
Q_ARG(const QByteArray &, body));
} else { sendNotAuthorized(socket); }
} else { sendNotFound(socket, "Wrong method"); }
}
Concrete Server
class RestServer : public Proof::AbstractRestServer
{
Q_OBJECT
public:
explicit RestServer(QObject *parent = 0);
protected slots:
NO_AUTH_REQUIRED void rest_get_Status(QTcpSocket *socket, const
QStringList &headers, const QStringList &methodVariableParts,
const QUrlQuery &query, const QByteArray &body);
void rest_get_Items_ValidList(...);
// GET /items/valid-list
}
Concrete Server implementation
void RestServer::rest_get_Items_ValidList(QTcpSocket *socket, const
QStringList &, const QStringList &, const QUrlQuery &, const
QByteArray &)
{
QJsonArray answerArray = m_somethingDataWorker->
getItems(ItemStatus::Valid);
sendAnswer(socket, QJsonDocument(answerArray).toJson(),
"text/json");
}
Сложности/Проблемы
● /press/123/start, /press/123/stop, item/321/transition
● Если нельзя вернуть данные сразу
Рефлексия в тестировании
TEST_F(AddressTest, updateFrom)
{
QList<QSignalSpy *> spies = spiesForObject(addressUT.data());
addressUT->updateFrom(addressUT2);
for (QSignalSpy *spy: spies)
EXPECT_EQ(1, spy->count()) << spy->signal().constData();
qDeleteAll(spies);
spies.clear();
EXPECT_EQ(addressUT2->city(), addressUT->city());
EXPECT_EQ(addressUT2->state(), addressUT->state());
EXPECT_EQ(addressUT2->postalCode(), addressUT->postalCode());
}
Рефлексия в тестировании
QList<QSignalSpy *> spiesForObject(QObject *obj, const QStringList &excludes)
{
QList<QSignalSpy *> spies;
for (int i = obj->metaObject()->methodOffset(); i < obj->metaObject()->methodCount(); ++i) {
if (obj->metaObject()->method(i).methodType() == QMetaMethod::Signal) {
QByteArray sign = obj->metaObject()->method(i).methodSignature();
if (excludes.contains(sign))
continue;
//Because QSignalSpy can't signals without SIGNAL() macros, but this hack cheating it
//# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
sign.prepend("2");
spies << new QSignalSpy(obj, qFlagLocation(sign.constData()));
}
}
return spies;
}
Заключение / Вопросы
Спасибо
vasiliy.a.sorokin@gmail.com

Qt Rest Server

  • 1.
    Простой REST серверна Qt с рефлексией Василий Сорокин Москва 2017
  • 2.
    Введение ● Qt иmoc ● Abstract Server ● Concrete Server ● Рефлексия ● Authorization (and tags) ● Сложности/Проблемы ● Рефлексия в тестировании ● Заключение
  • 3.
    Meta-Object Compiler ● Когдазапускается ● Что делает ● Почему это важно ● Ограничения
  • 4.
    Что должен уметьсервер? ● Получать запросы ● Разбирать данные ● Возвращать ответы ● Обрабатывать ошибки ● Авторизация
  • 5.
    Abstract Server #ifndef Q_MOC_RUN #define NO_AUTH_REQUIRED #endif class AbstractRestServer : public QTcpServer { public: explicit AbstractRestServer(const QString &pathPrefix, int port, QObject *parent = 0); Q_INVOKABLE void startListen(); Q_INVOKABLE void stopListen(); protected: void incomingConnection(qintptr socketDescriptor) override;
  • 6.
    Abstract Server void tryToCallMethod(QTcpSocket*socket, const QString &type, const QString &method, QStringList headers, const QByteArray &body); QStringList makeMethodName(const QString &type, const QString &name); MethodNode *findMethod(const QStringList &splittedMethod, QStringList &methodVariableParts); void fillMethods(); void addMethodToTree(const QString &realMethod, const QString &tag);
  • 7.
    Abstract Server void sendAnswer(QTcpSocket*socket, const QByteArray &body, const QString &contentType, const QHash<QString, QString> &headers, int returnCode = 200, const QString &reason = QString()); void registerSocket(QTcpSocket *socket); void deleteSocket(QTcpSocket *socket, WorkerThread *worker);
  • 8.
    Abstract Server private: QThread *m_serverThread= nullptr; QList<WorkerThreadInfo> m_threadPool; QSet<QTcpSocket *> m_sockets; QMutex m_socketsMutex; MethodNode m_methodsTreeRoot; int m_maxThreadsCount;
  • 9.
    WorkerThread class WorkerThread: publicQThread … public: WorkerThread(Proof::AbstractRestServer *const _server); void sendAnswer(QTcpSocket *socket, const QByteArray &body, const QString &contentType, const QHash<QString, QString> &headers, int returnCode, const QString &reason); void handleNewConnection(qintptr socketDescriptor); void deleteSocket(QTcpSocket *socket); void onReadyRead(QTcpSocket *socket); void stop();
  • 10.
  • 11.
    WorkerThreadInfo struct WorkerThreadInfo { explicit WorkerThreadInfo(WorkerThread*thread, quint32 socketCount) : thread(thread), socketCount(socketCount) {} WorkerThread *thread; quint32 socketCount; };
  • 12.
    SocketInfo struct SocketInfo { Proof::HttpParser parser; QMetaObject::ConnectionreadyReadConnection; QMetaObject::Connection disconnectConnection; QMetaObject::Connection errorConnection; };
  • 13.
    Abstract Server implementation staticconst QString NO_AUTH_TAG = QString("NO_AUTH_REQUIRED"); AbstractRestServer::AbstractRestServer(...) : QTcpServer(parent) { m_serverThread = new QThread(this); m_maxThreadsCount = QThread::idealThreadCount(); if (m_maxThreadsCount < MIN_THREADS_COUNT) m_maxThreadsCount = MIN_THREADS_COUNT; else m_maxThreadsCount += 2; moveToThread(m_serverThread); m_serverThread->moveToThread(m_serverThread); m_serverThread->start();
  • 14.
    Abstract Server implementation voidAbstractRestServer::startListen() { if (!PrObject::call(this, &AbstractRestServer::startListen)) { fillMethods(); bool isListen = listen(QHostAddress::Any, m_port); } } void AbstractRestServer::stopListen() { if (!PrObject::call(this, &AbstractRestServer::stopListen, Proof::Call::Block)) close(); }
  • 15.
    Make route tree voidAbstractRestServer::fillMethods() { m_methodsTreeRoot.clear(); for (int i = 0; i < metaObject()->methodCount(); ++i) { QMetaMethod method = metaObject()->method(i); if (method.methodType() == QMetaMethod::Slot) { QString currentMethod = QString(method.name()); if (currentMethod.startsWith(REST_METHOD_PREFIX)) addMethodToTree(currentMethod, method.tag()); } } }
  • 16.
    Make route tree voidAbstractRestServer::addMethodToTree(const QString &realMethod, const QString &tag) { QString method = realMethod.mid(QString(REST_METHOD_PREFIX).length()); for (int i = 0; i < method.length(); ++i) { if (method[i].isUpper()) { method[i] = method[i].toLower(); if (i > 0 && method[i - 1] != '_') method.insert(i++, '-'); } } // rest_get_SourceList => get_source-list
  • 17.
    Make route tree QStringListsplittedMethod = method.split("_"); MethodNode *currentNode = &m_methodsTreeRoot; for (int i = 0; i < splittedMethod.count(); ++i) { if (!currentNode->contains(splittedMethod[i])) (*currentNode)[splittedMethod[i]] = MethodNode(); currentNode = &(*currentNode)[splittedMethod[i]]; } currentNode->setValue(realMethod); currentNode->setTag(tag); }
  • 18.
    Make route tree classMethodNode { public: MethodNode(); bool contains(const QString &name) const; void clear(); operator QString(); MethodNode &operator [](const QString &name); const MethodNode operator [](const QString &name) const; void setValue(const QString &value); QString tag() const; void setTag(const QString &tag); private: QHash<QString, MethodNode> m_nodes; QString m_value = ""; QString m_tag; };
  • 19.
    New connection handling voidAbstractRestServer::incomingConnection(qintptr socketDescriptor) { WorkerThread *worker = nullptr; if (!m_threadPool.isEmpty()) { auto iter = std::min_element(d->threadPool.begin(), d->threadPool.end(), [](const WorkerThreadInfo &lhs, const WorkerThreadInfo &rhs) { return lhs.socketCount < rhs.socketCount; }); if (iter->socketCount == 0 || m_threadPool.count() >= m_maxThreadsCount) { worker = iter->thread; ++iter->socketCount; } }
  • 20.
    New connection handling if(worker == nullptr) { worker = new WorkerThread(this); worker->start(); m_threadPool << WorkerThreadInfo{worker, 1}; } worker->handleNewConnection(socketDescriptor); }
  • 21.
    New connection handling voidWorkerThread::handleNewConnection(qintptr socketDescriptor) { if (PrObject::call(this, &WorkerThread::handleNewConnection, socketDescriptor)) return; QTcpSocket *tcpSocket = new QTcpSocket(); m_server->registerSocket(tcpSocket); SocketInfo info; info.readyReadConnection = connect(tcpSocket, &QTcpSocket::readyRead, this, [tcpSocket, this] { onReadyRead(tcpSocket); }, Qt::QueuedConnection); void (QTcpSocket:: *errorSignal)(QAbstractSocket::SocketError) = &QTcpSocket::error; info.errorConnection = connect(tcpSocket, errorSignal, this, [tcpSocket, this] {…}, Qt::QueuedConnection); info.disconnectConnection = connect(tcpSocket, &QTcpSocket::disconnected, this, [tcpSocket, this] {...}, Qt::QueuedConnection);
  • 22.
    New connection handling if(!tcpSocket->setSocketDescriptor(socketDescriptor)) { m_server->deleteSocket(tcpSocket, this); return; } sockets[tcpSocket] = info; }
  • 23.
    New connection handling voidWorkerThread::onReadyRead(QTcpSocket *socket) { SocketInfo &info = m_sockets[socket]; HttpParser::Result result = info.parser.parseNextPart(socket->readAll()); switch (result) { case HttpParser::Result::Success: disconnect(info.readyReadConnection); m_server->tryToCallMethod(socket, info.parser.method(), info.parser.uri(), info.parser.headers(), info.parser.body()); break; case HttpParser::Result::Error: disconnect(info.readyReadConnection); sendAnswer(socket, "", "text/plain; charset=utf-8", QHash<QString, QString>(), 400, "Bad Request"); break; case HttpParser::Result::NeedMore: break; } }
  • 24.
    Call method void AbstractRestServer::tryToCallMethod(QTcpSocket*socket, const QString &type, const QString &method, QStringList headers, const QByteArray &body) { QStringList splittedByParamsMethod = method.split('?'); QStringList methodVariableParts; QUrlQuery queryParams; if (splittedByParamsMethod.count() > 1) queryParams = QUrlQuery(splittedByParamsMethod.at(1)); MethodNode *methodNode = findMethod(makeMethodName(type, splittedByParamsMethod.at(0)), methodVariableParts); QString methodName = methodNode ? (*methodNode) : QString();
  • 25.
    Call method if (methodNode){ bool isAuthenticationSuccessful = true; if (methodNode->tag() != NO_AUTH_TAG) { QString encryptedAuth; for (int i = 0; i < headers.count(); ++i) { if (headers.at(i).startsWith("Authorization", Qt::CaseInsensitive)) { encryptedAuth = parseAuth(socket, headers.at(i)); break; } } isAuthenticationSuccessful = (!encryptedAuth.isEmpty() && q- >checkBasicAuth(encryptedAuth)); }
  • 26.
    Call method if (isAuthenticationSuccessful){ QMetaObject::invokeMethod(this, methodName.toLatin1().constData(), Qt::DirectConnection, Q_ARG(QTcpSocket *,socket), Q_ARG(const QStringList &, headers), Q_ARG(const QStringList &, methodVariableParts), Q_ARG(const QUrlQuery &, queryParams), Q_ARG(const QByteArray &, body)); } else { sendNotAuthorized(socket); } } else { sendNotFound(socket, "Wrong method"); } }
  • 27.
    Concrete Server class RestServer: public Proof::AbstractRestServer { Q_OBJECT public: explicit RestServer(QObject *parent = 0); protected slots: NO_AUTH_REQUIRED void rest_get_Status(QTcpSocket *socket, const QStringList &headers, const QStringList &methodVariableParts, const QUrlQuery &query, const QByteArray &body); void rest_get_Items_ValidList(...); // GET /items/valid-list }
  • 28.
    Concrete Server implementation voidRestServer::rest_get_Items_ValidList(QTcpSocket *socket, const QStringList &, const QStringList &, const QUrlQuery &, const QByteArray &) { QJsonArray answerArray = m_somethingDataWorker-> getItems(ItemStatus::Valid); sendAnswer(socket, QJsonDocument(answerArray).toJson(), "text/json"); }
  • 29.
    Сложности/Проблемы ● /press/123/start, /press/123/stop,item/321/transition ● Если нельзя вернуть данные сразу
  • 30.
    Рефлексия в тестировании TEST_F(AddressTest,updateFrom) { QList<QSignalSpy *> spies = spiesForObject(addressUT.data()); addressUT->updateFrom(addressUT2); for (QSignalSpy *spy: spies) EXPECT_EQ(1, spy->count()) << spy->signal().constData(); qDeleteAll(spies); spies.clear(); EXPECT_EQ(addressUT2->city(), addressUT->city()); EXPECT_EQ(addressUT2->state(), addressUT->state()); EXPECT_EQ(addressUT2->postalCode(), addressUT->postalCode()); }
  • 31.
    Рефлексия в тестировании QList<QSignalSpy*> spiesForObject(QObject *obj, const QStringList &excludes) { QList<QSignalSpy *> spies; for (int i = obj->metaObject()->methodOffset(); i < obj->metaObject()->methodCount(); ++i) { if (obj->metaObject()->method(i).methodType() == QMetaMethod::Signal) { QByteArray sign = obj->metaObject()->method(i).methodSignature(); if (excludes.contains(sign)) continue; //Because QSignalSpy can't signals without SIGNAL() macros, but this hack cheating it //# define SIGNAL(a) qFlagLocation("2"#a QLOCATION) sign.prepend("2"); spies << new QSignalSpy(obj, qFlagLocation(sign.constData())); } } return spies; }
  • 32.