Compare commits

...

10 Commits

13 changed files with 228 additions and 305 deletions

1
.gitignore vendored
View File

@ -18,3 +18,4 @@ moc_*.cpp
qrc_*.cpp qrc_*.cpp
Makefile Makefile
*-build-* *-build-*
build/

View File

@ -13,8 +13,8 @@ set(QREDIS_PATCH 0)
set(QREDIS_VERSION "${QREDIS_MAJOR}.${QREDIS_MINOR}.${QREDIS_PATCH}") set(QREDIS_VERSION "${QREDIS_MAJOR}.${QREDIS_MINOR}.${QREDIS_PATCH}")
# The QtCore and QtNetwork libraries are both required. # The QtCore and QtNetwork libraries are both required.
find_package(Qt5Core REQUIRED) find_package(Qt6Core REQUIRED)
find_package(Qt5Network REQUIRED) find_package(Qt6Network REQUIRED)
# Specify where QRedis includes are located. # Specify where QRedis includes are located.
include_directories(include) include_directories(include)
@ -27,7 +27,7 @@ set(QREDIS_SRC
src/request.cpp) src/request.cpp)
# QRedis header files requiring MOC. # QRedis header files requiring MOC.
qt5_wrap_cpp(QREDIS_MOC qt6_wrap_cpp(QREDIS_MOC
include/qredis/client.h include/qredis/client.h
include/qredis/request.h include/qredis/request.h
src/client_p.h src/client_p.h
@ -46,7 +46,7 @@ set_target_properties(qredis PROPERTIES
SOVERSION ${QREDIS_MAJOR}) SOVERSION ${QREDIS_MAJOR})
# This causes the appropriate libraries to be included in the link process. # This causes the appropriate libraries to be included in the link process.
qt5_use_modules(qredis Core Network) target_link_libraries(qredis Qt6::Core Qt6::Network)
# Specify the proper installation paths. # Specify the proper installation paths.
install(TARGETS qredis install(TARGETS qredis

View File

@ -1,6 +1,6 @@
## QRedis ## QRedis
QRedis provides a modern Qt client library for communicating with a [Redis server](http://redis.io/). The code compiles exclusively with Qt 5, ensuring years of compatibility down the road. QRedis provides a modern Qt client library for communicating with a [Redis server](http://redis.io/). The code compiles exclusively with Qt 5, ensuring years of compatibility down the road. Modified with Qt 6 compatibility by Jesse Qu.
### Requirements ### Requirements

103
include/qredis/reply.h Normal file
View File

@ -0,0 +1,103 @@
#ifndef QREDIS_REPLY_H
#define QREDIS_REPLY_H
#include <QVariant>
#include "qredis_export.h"
namespace QRedis
{
/**
* @brief Represents a Redis reply
*/
class QREDIS_EXPORT Reply
{
public:
/**
* @brief Reply types
*/
enum Type {
/**
* @brief An invalid reply
*
* This value is only set when the default constructor is used.
*/
Invalid,
/**
* @brief A status reply
*
* The value property will contain the status message returned
* by the server as a QString.
*/
Status,
/**
* @brief An error reply
*
* The value property will contain the error message returned by
* the server as a QString.
*/
Error,
/**
* @brief An integer reply
*
* The value property will contain the integer value returned by
* the server as a qlonglong.
*/
Integer,
/**
* @brief A bulk reply
*
* The value property will contain the bulk reply returned by
* the server as a QByteArray.
*/
Bulk,
/**
* @brief A multi-bulk reply
*
* The value property will contain the multi-bulk reply returned
* by the server as a QVariantList. Each entry in the list is of
* type Reply.
*/
MultiBulk
};
/**
* @brief Creates an empty reply
*/
Reply() : _type(Invalid) {}
/**
* @brief Initializes the reply
* @param type the type of the reply
*/
Reply(Type type) : _type(type) {}
/**
* @brief Returns the type of the reply
* @return the reply type
*/
Type type() const { return _type; }
/**
* @brief Returns a reference to the value of the reply
* @return the reply value
*/
QVariant & value() { return _value; }
private:
Type _type;
QVariant _value;
};
}
Q_DECLARE_METATYPE(QRedis::Reply)
#endif // QREDIS_REPLY_H

View File

@ -3,8 +3,8 @@
#include <QObject> #include <QObject>
#include <QScopedPointer> #include <QScopedPointer>
#include <QVariantList>
#include <qredis/reply.h>
#include "qredis_export.h" #include "qredis_export.h"
namespace QRedis namespace QRedis
@ -12,7 +12,7 @@ namespace QRedis
class QREDIS_EXPORT RequestPrivate; class QREDIS_EXPORT RequestPrivate;
/** /**
* @brief Represents a request and its reply * @brief Represents a Redis command and its response
*/ */
class QREDIS_EXPORT Request : public QObject class QREDIS_EXPORT Request : public QObject
{ {
@ -21,8 +21,8 @@ namespace QRedis
public: public:
/** /**
* @brief Creates a request * @brief Initializes the request
* @param parent * @param parent the parent QObject
*/ */
explicit Request(QObject * parent = 0); explicit Request(QObject * parent = 0);
@ -41,39 +41,10 @@ namespace QRedis
Q_SIGNALS: Q_SIGNALS:
/** /**
* @brief Emitted when a bulk reply is received * @brief Emitted when a reply is received
* @param value the value as a byte array * @param reply the reply received
*/ */
void bulk(const QByteArray & value); void reply(Reply & reply);
/**
* @brief Emitted when an error reply is received
* @param message a descriptive error message
*/
void error(const QString & message);
/**
* @brief Emitted when an integer reply is received
* @param value the integer value
*/
void integer(qlonglong value);
/**
* @brief Emitted when a multi-bulk reply is received
* @param a list of bulk values
*/
void multiBulk(const QVariantList & values);
/**
* @brief Emitted when any reply is received
*/
void reply();
/**
* @brief Emitted when a status reply is received
* @param message a descriptive status message
*/
void status(const QString & message);
private: private:

View File

@ -4,124 +4,16 @@
using namespace QRedis; using namespace QRedis;
ClientPrivate::ClientPrivate(Client * client) ClientPrivate::ClientPrivate(Client * client)
: q(client) : lexer(&socket), parser(&lexer)
{ {
connect(&socket, &QTcpSocket::connected, q, &Client::connected); connect(&socket, SIGNAL(connected()), client, SIGNAL(connected()));
connect(&socket, &QTcpSocket::disconnected, q, &Client::disconnected); connect(&socket, SIGNAL(disconnected()), client, SIGNAL(disconnected()));
connect(&socket, &QTcpSocket::disconnected, this, &ClientPrivate::reset); connect(&parser, SIGNAL(reply(QRedis::Reply&)), SLOT(sendReply(Reply&)));
connect(&socket, &QTcpSocket::readyRead, this, &ClientPrivate::readReply);
} }
int ClientPrivate::readInteger(qlonglong & value) void ClientPrivate::sendReply(Reply & reply)
{ {
int pos = buffer.indexOf("\r\n"); emit queue.dequeue()->reply(reply);
if(pos != -1)
{
value = buffer.mid(0, pos).toLongLong();
buffer.remove(0, pos + 2);
}
return pos;
}
/*
* Note: error replies actually contain a type and then the error description
* but we just combine them here for simplicity.
*/
bool ClientPrivate::readStatusOrErrorReply()
{
/* Check if the reply contains \r\n. */
int pos = buffer.indexOf("\r\n");
if(pos != -1)
{
Request * request = queue.dequeue();
if(buffer[0] == '+')
emit request->status(buffer.mid(1, pos - 1));
else
emit request->error(buffer.mid(1, pos - 1));
buffer.remove(0, pos + 2);
return true;
}
return false;
}
bool ClientPrivate::readIntegerReply()
{
/* Check if the reply contains \r\n. */
int pos = buffer.indexOf("\r\n");
if(pos != -1)
{
emit queue.dequeue()->integer(buffer.mid(1, pos -1).toLongLong());
buffer.remove(0, pos + 2);
return true;
}
return false;
}
bool ClientPrivate::readBulkReply()
{
/* Check if the reply contains \r\n. */
int pos = buffer.indexOf("\r\n");
if(pos != -1)
{
int length = buffer.mid(1, pos -1).toInt();
if(buffer.size() >= pos + length + 4)
{
emit queue.dequeue()->bulk(buffer.mid(pos + 2, length));
buffer.remove(0, pos + length + 4);
return true;
}
}
return false;
}
bool ClientPrivate::readMultiBulkReply()
{
return false;
}
void ClientPrivate::reset()
{
foreach(Request * request, queue)
request->deleteLater();
queue.clear();
}
// TODO: unrecognized replies in the switch should be handled.
void ClientPrivate::readReply()
{
buffer.append(socket.readAll());
while(!buffer.isEmpty())
{
bool finished;
switch(buffer[0])
{
case '+':
case '-':
finished = readStatusOrErrorReply();
break;
case ':':
finished = readIntegerReply();
break;
case '$':
finished = readBulkReply();
break;
case '*':
finished = readMultiBulkReply();
break;
}
if(!finished)
break;
}
} }
Client::Client(QObject * parent) Client::Client(QObject * parent)

View File

@ -6,7 +6,10 @@
#include <QTcpSocket> #include <QTcpSocket>
#include <qredis/client.h> #include <qredis/client.h>
#include <qredis/reply.h>
#include <qredis/request.h> #include <qredis/request.h>
#include "lexer.h"
#include "parser.h"
namespace QRedis namespace QRedis
{ {
@ -18,27 +21,15 @@ namespace QRedis
ClientPrivate(Client *); ClientPrivate(Client *);
/* Utility methods for reading items from the buffer. */
int readInteger(qlonglong &);
bool readStatusOrErrorReply();
bool readIntegerReply();
bool readBulkReply();
bool readMultiBulkReply();
QTcpSocket socket; QTcpSocket socket;
QQueue<Request *> queue; QQueue<Request *> queue;
QByteArray buffer;
public Q_SLOTS: Lexer lexer;
Parser parser;
void reset(); private Q_SLOTS:
void readReply();
private: void sendReply(Reply &);
Client * const q;
}; };
} }

View File

@ -1,5 +1,7 @@
#include "lexer.h" #include "lexer.h"
using namespace QRedis;
Lexer::Lexer(QIODevice * device, QObject * parent) Lexer::Lexer(QIODevice * device, QObject * parent)
: QObject(parent), device(device), state(DoingNothing), crlf(0) : QObject(parent), device(device), state(DoingNothing), crlf(0)
{ {
@ -22,8 +24,8 @@ void Lexer::readData()
switch(state) switch(state)
{ {
case ReadingLength: case ReadingLength:
case ReadingUnsafeString: if(!readUnsafeString()) return; break; case ReadingUnsafeString: if(!readUnsafeString()) return;
case ReadingSafeString: if(!readSafeString()) return; break; case ReadingSafeString: if(!readSafeString()) return;
} }
if(state != ReadingSafeString) if(state != ReadingSafeString)
@ -73,6 +75,7 @@ bool Lexer::readUnsafeString()
else else
emit unsafeString(s); emit unsafeString(s);
crlf = 0;
return true; return true;
} }
@ -85,5 +88,7 @@ bool Lexer::readSafeString()
buffer.remove(0, length + 2); buffer.remove(0, length + 2);
emit safeString(d); emit safeString(d);
state = DoingNothing;
return true; return true;
} }

View File

@ -1,47 +1,50 @@
#ifndef LEXER_H #ifndef QREDIS_LEXER_H
#define LEXER_H #define QREDIS_LEXER_H
#include <QIODevice> #include <QIODevice>
#include <QObject> #include <QObject>
class Lexer : public QObject namespace QRedis
{ {
Q_OBJECT class Lexer : public QObject
{
Q_OBJECT
public: public:
Lexer(QIODevice *, QObject * = 0); Lexer(QIODevice *, QObject * = 0);
virtual ~Lexer(); virtual ~Lexer();
Q_SIGNALS: Q_SIGNALS:
void character(char); void character(char);
void unsafeString(const QString &); void unsafeString(const QString &);
void safeString(const QByteArray &); void safeString(const QByteArray &);
private Q_SLOTS: private Q_SLOTS:
void readData(); void readData();
private: private:
bool readCharacter(); bool readCharacter();
bool readLength(); bool readLength();
bool readUnsafeString(); bool readUnsafeString();
bool readSafeString(); bool readSafeString();
QIODevice * device; QIODevice * device;
QByteArray buffer; QByteArray buffer;
enum { enum {
DoingNothing, DoingNothing,
ReadingLength, ReadingLength,
ReadingUnsafeString, ReadingUnsafeString,
ReadingSafeString ReadingSafeString
} state; } state;
int crlf; int crlf;
int length; int length;
}; };
}
#endif // LEXER_H #endif // QREDIS_LEXER_H

View File

@ -1,5 +1,7 @@
#include "parser.h" #include "parser.h"
using namespace QRedis;
Parser::Parser(Lexer * lexer, QObject * parent) Parser::Parser(Lexer * lexer, QObject * parent)
: QObject(parent) : QObject(parent)
{ {
@ -16,70 +18,48 @@ void Parser::readCharacter(char c)
{ {
switch(c) switch(c)
{ {
case '+': stack.append(Task(Task::ReadStatus)); break; case '+': stack.append(Task(Reply::Status)); break;
case '-': stack.append(Task(Task::ReadError)); break; case '-': stack.append(Task(Reply::Error)); break;
case ':': stack.append(Task(Task::ReadInteger)); break; case ':': stack.append(Task(Reply::Integer)); break;
case '$': stack.append(Task(Task::ReadBulk)); break; case '$': stack.append(Task(Reply::Bulk)); break;
case '*': stack.append(Task(Task::ReadMultiBulk)); break; case '*': stack.append(Task(Reply::MultiBulk)); break;
default: default:
emit warning(tr("Skipping unexpected character '%1'").arg(static_cast<int>(c), 0, 16)); qWarning("Skipping unexpected character '%x'", static_cast<int>(c));
break; break;
} }
} }
void Parser::readUnsafeString(const QString & value) void Parser::readUnsafeString(const QString & value)
{ {
if(tos().action == Task::ReadMultiBulk) if(tos().reply.type() == Reply::MultiBulk)
tos().count = value.toInt(); tos().count = value.toInt();
else else
{ tos().reply.value() = value;
stack.removeLast();
if(tos().action == Task::ReadStatus) descend();
emit status(value);
else if(tos().action == Task::ReadError)
{
int pos = value.indexOf(' ');
emit error(value.left(pos), value.right(pos + 1));
}
else if(!stack.count())
emit integer(value.toLongLong());
else
{
tos().value.toList().append(value);
descend();
}
}
} }
void Parser::readSafeString(const QByteArray & value) void Parser::readSafeString(const QByteArray & value)
{ {
stack.removeLast(); tos().reply.value() = value;
descend();
if(!stack.count())
emit bulk(value);
else
{
tos().value.toList().append(value);
descend();
}
} }
void Parser::descend() void Parser::descend()
{ {
while(true) while(true)
{ {
if(tos().value.toList().count() < tos().count) if(tos().reply.type() == Reply::MultiBulk &&
break; tos().reply.value().toList().count() < tos().count)
return;
if(stack.count() == 1) if(stack.count() == 1)
{ {
emit multiBulk(tos().value.toList()); emit reply(stack.takeLast().reply);
stack.removeLast(); return;
break;
} }
Task task = stack.takeLast(); Reply r = stack.takeLast().reply;
tos().value.toList().append(task.value); tos().reply.value().toList().append(QVariant::fromValue(r));
} }
} }

View File

@ -1,66 +1,55 @@
#ifndef PARSER_H #ifndef QREDIS_PARSER_H
#define PARSER_H #define QREDIS_PARSER_H
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QPair>
#include <QVariant> #include <QVariant>
#include <QVariantList>
#include <qredis/reply.h>
#include "lexer.h" #include "lexer.h"
class Parser : public QObject namespace QRedis
{ {
Q_OBJECT class Parser : public QObject
{
Q_OBJECT
public: public:
Parser(Lexer *, QObject * = 0); Parser(Lexer *, QObject * = 0);
virtual ~Parser(); virtual ~Parser();
Q_SIGNALS: Q_SIGNALS:
void status(const QString &); void reply(const Reply &);
void error(const QString &, const QString &);
void integer(qlonglong);
void bulk(const QByteArray &);
void multiBulk(const QVariantList &);
void warning(const QString &); private Q_SLOTS:
private Q_SLOTS: void readCharacter(char);
void readUnsafeString(const QString &);
void readSafeString(const QByteArray &);
void readCharacter(char); private:
void readUnsafeString(const QString &);
void readSafeString(const QByteArray &);
private: void descend();
void descend(); class Task
{
public:
class Task enum { Unknown = -2 };
{
public:
enum Action { Task(Reply::Type type) : reply(type), count(Unknown) {}
ReadStatus,
ReadError,
ReadInteger,
ReadBulk,
ReadMultiBulk
};
enum { Unknown = -2 }; Reply reply;
int count;
};
Task(Action action) : action(action), count(Unknown) {} QList<Task> stack;
Action action; Task & tos() { return stack.last(); }
int count; };
QVariant value; }
};
QList<Task> stack; #endif // QREDIS_PARSER_H
inline Task & tos() { return stack.last(); }
};
#endif // PARSER_H

View File

@ -13,19 +13,7 @@ void RequestPrivate::quitEventLoop()
Request::Request(QObject * parent) Request::Request(QObject * parent)
: QObject(parent), d(new RequestPrivate) : QObject(parent), d(new RequestPrivate)
{ {
/* connect(this, SIGNAL(reply(Reply&)), SLOT(deleteLater()));
* Each of these signals also causes the generic reply() signal to be emitted.
*/
connect(this, SIGNAL(bulk(QByteArray)), SIGNAL(reply()));
connect(this, SIGNAL(error(QString)), SIGNAL(reply()));
connect(this, SIGNAL(integer(qlonglong)), SIGNAL(reply()));
connect(this, SIGNAL(multiBulk(QVariantList)), SIGNAL(reply()));
connect(this, SIGNAL(status(QString)), SIGNAL(reply()));
/*
* We also need to ensure that this object is deleted when the reply is received.
*/
connect(this, SIGNAL(reply()), SLOT(deleteLater()));
} }
Request::~Request() Request::~Request()
@ -38,12 +26,12 @@ bool Request::waitForReply(int msecs)
timer.setInterval(msecs); timer.setInterval(msecs);
timer.setSingleShot(true); timer.setSingleShot(true);
connect(&timer, SIGNAL(timeout()), &d->loop, SLOT(quit())); connect(&timer, SIGNAL(timeout()), &d->loop, SLOT(quit()));
connect(this, SIGNAL(reply()), d.data(), SLOT(quitEventLoop())); connect(this, SIGNAL(reply(Reply&)), d.data(), SLOT(quitEventLoop()));
/* /*
* If the timer fires, the return value will be 0. * If the timer fires, the return value will be 0.
* Otherwise, quitEventLoop() will terminate the loop with 1. * Otherwise, quitEventLoop() will terminate the loop with 1.
*/ */
return d->loop.exec(); return d->loop.exec(QEventLoop::ExcludeUserInputEvents);
} }

View File

@ -1,5 +1,5 @@
# The QtTest libraries are required for building the tests. # The QtTest libraries are required for building the tests.
find_package(Qt5Test REQUIRED) find_package(Qt6Test REQUIRED)
# Specify the source files for the tests. # Specify the source files for the tests.
set(QREDIS_TESTS_SRC set(QREDIS_TESTS_SRC
@ -7,7 +7,7 @@ set(QREDIS_TESTS_SRC
testclient.cpp) testclient.cpp)
# Specify the files that need to be MOC'd. # Specify the files that need to be MOC'd.
qt5_wrap_cpp(QREDIS_TESTS_MOC qt6_wrap_cpp(QREDIS_TESTS_MOC
testclient.h) testclient.h)
# Create the test executable. # Create the test executable.
@ -16,7 +16,7 @@ add_executable(qredis-tests
${QREDIS_TESTS_MOC}) ${QREDIS_TESTS_MOC})
# Specify the Qt modules the test executable links against. # Specify the Qt modules the test executable links against.
qt5_use_modules(qredis-tests Core Test) target_link_libraries(qredis-tests Qt6::Core Qt6::Test)
# Naturally, we also link against the QRedis library. # Naturally, we also link against the QRedis library.
target_link_libraries(qredis-tests qredis) target_link_libraries(qredis-tests qredis)