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

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

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

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

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

Василий СорокинМосква 2017

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

Введение

● Qt и moc

● Abstract Server

● Concrete Server

● Рефлексия

● Authorization (and tags)

● Сложности/Проблемы

● Рефлексия в тестировании

● Заключение

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

Meta-Object Compiler

● Когда запускается

● Что делает

● Почему это важно

● Ограничения

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

Что должен уметь сервер?

● Получать запросы

● Разбирать данные

● Возвращать ответы

● Обрабатывать ошибки

● Авторизация

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

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;

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

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);

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

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);

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

Abstract Server

private:

QThread *m_serverThread = nullptr;

QList<WorkerThreadInfo> m_threadPool;

QSet<QTcpSocket *> m_sockets;

QMutex m_socketsMutex;

MethodNode m_methodsTreeRoot;

int m_maxThreadsCount;

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

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();

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

WorkerThread

private:

Proof::AbstractRestServer* const m_server;

QHash<QTcpSocket *, SocketInfo> m_sockets;

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

WorkerThreadInfo

struct WorkerThreadInfo

{

explicit WorkerThreadInfo(WorkerThread *thread, quint32 socketCount)

: thread(thread), socketCount(socketCount) {}

WorkerThread *thread;

quint32 socketCount;

};

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

SocketInfo

struct SocketInfo

{

Proof::HttpParser parser;

QMetaObject::Connection readyReadConnection;

QMetaObject::Connection disconnectConnection;

QMetaObject::Connection errorConnection;

};

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

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();

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

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();

}

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

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());

}

}

}

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

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

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

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);

}

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

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;

};

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

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;

}

}

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

New connection handling

if (worker == nullptr) {

worker = new WorkerThread(this);

worker->start();

m_threadPool << WorkerThreadInfo{worker, 1};

}

worker->handleNewConnection(socketDescriptor);

}

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

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);

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

New connection handling

if (!tcpSocket->setSocketDescriptor(socketDescriptor)) {

m_server->deleteSocket(tcpSocket, this);

return;

}

sockets[tcpSocket] = info;

}

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

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;

}

}

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

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();

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

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));

}

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

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"); }

}

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

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

}

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

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");

}

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

Сложности/Проблемы

● /press/123/start, /press/123/stop, item/321/transition

● Если нельзя вернуть данные сразу

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

Рефлексия в тестировании

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());

}

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

Рефлексия в тестировании

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;

}

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

Заключение / Вопросы

Спасибо

[email protected]