Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Василий Сорокин, Простой REST сервер на Qt с рефлексией

5,592 views

Published on

Библиотека Qt имеет довольно мощную систему рефлексии. На примере простого в использовании класса, позволяющего с помощью наследования быстро построить REST сервер под ваши нужды, я покажу как элегантно ее можно задействовать в реальной жизни. В заключительной части, покажу еще один пример когда рефлексия Qt помогает красиво протестировать испускание сигналов классом.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Василий Сорокин, Простой REST сервер на Qt с рефлексией

  1. 1. Простой REST сервер на Qt с рефлексией Василий Сорокин Москва 2017
  2. 2. Введение ● Qt и moc ● Abstract Server ● Concrete Server ● Рефлексия ● Authorization (and tags) ● Сложности/Проблемы ● Рефлексия в тестировании ● Заключение
  3. 3. Meta-Object Compiler ● Когда запускается ● Что делает ● Почему это важно ● Ограничения
  4. 4. Что должен уметь сервер? ● Получать запросы ● Разбирать данные ● Возвращать ответы ● Обрабатывать ошибки ● Авторизация
  5. 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. 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. 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. 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. 9. 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();
  10. 10. WorkerThread private: Proof::AbstractRestServer* const m_server; QHash<QTcpSocket *, SocketInfo> m_sockets;
  11. 11. WorkerThreadInfo struct WorkerThreadInfo { explicit WorkerThreadInfo(WorkerThread *thread, quint32 socketCount) : thread(thread), socketCount(socketCount) {} WorkerThread *thread; quint32 socketCount; };
  12. 12. SocketInfo struct SocketInfo { Proof::HttpParser parser; QMetaObject::Connection readyReadConnection; QMetaObject::Connection disconnectConnection; QMetaObject::Connection errorConnection; };
  13. 13. 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();
  14. 14. 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(); }
  15. 15. 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()); } } }
  16. 16. 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
  17. 17. 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); }
  18. 18. 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; };
  19. 19. 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; } }
  20. 20. New connection handling if (worker == nullptr) { worker = new WorkerThread(this); worker->start(); m_threadPool << WorkerThreadInfo{worker, 1}; } worker->handleNewConnection(socketDescriptor); }
  21. 21. 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);
  22. 22. New connection handling if (!tcpSocket->setSocketDescriptor(socketDescriptor)) { m_server->deleteSocket(tcpSocket, this); return; } sockets[tcpSocket] = info; }
  23. 23. 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; } }
  24. 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. 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. 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. 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. 28. 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"); }
  29. 29. Сложности/Проблемы ● /press/123/start, /press/123/stop, item/321/transition ● Если нельзя вернуть данные сразу
  30. 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. 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. 32. Заключение / Вопросы Спасибо vasiliy.a.sorokin@gmail.com

×