diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c85ca6..de7f42b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ add_subdirectory(src/myx/base) add_subdirectory(src/myx/filesystem) add_subdirectory(src/myx/qt) add_subdirectory(src/myx/math) +add_subdirectory(src/myx/redis) # Примеры if (BUILD_EXAMPLES) diff --git a/src/myx/redis/CMakeLists.txt b/src/myx/redis/CMakeLists.txt new file mode 100644 index 0000000..a71b528 --- /dev/null +++ b/src/myx/redis/CMakeLists.txt @@ -0,0 +1,62 @@ +# Название основной цели и имя библиотеки в текущем каталоге +set(current_target redis) + +# Список файлов исходных текстов +set(current_target_sources + ${CMAKE_CURRENT_SOURCE_DIR}/client.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/config.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/lexer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/request.cpp +) + +# Список заголовочных файлов (используется для установки) +set(current_target_headers + ${CMAKE_CURRENT_SOURCE_DIR}/client.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/config.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/lexer.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/parser.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/reply.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/request.hpp +) + +add_common_library(TARGET ${current_target} + OUTPUT_NAME myx-${current_target} + SOURCES ${current_target_sources}) +common_target_properties(${current_target}) + +add_clang_tidy_check(${current_target} ${current_target_sources}) +add_clang_analyze_check(${current_target} ${current_target_sources}) +add_clazy_check(${current_target} ${current_target_sources}) +add_pvs_check(${current_target}) + +# Форматирование исходников +add_format_sources(${current_target} ${current_target_sources} ${current_target_headers}) + +target_include_directories(${current_target} SYSTEM PUBLIC ${Qt5Core_INCLUDE_DIRS}) +target_include_directories(${current_target} SYSTEM PUBLIC ${Qt5Network_INCLUDE_DIRS}) +target_include_directories(${current_target} SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/src) +target_include_directories(${current_target} PRIVATE ${CMAKE_BINARY_DIR}/include) + +# Цель, используемая только для установки заголовочных файлов без компиляции проекта +add_custom_target(${current_target}-install-headers + COMMAND "${CMAKE_COMMAND}" + -DCMAKE_INSTALL_COMPONENT=headers -P "${CMAKE_BINARY_DIR}/cmake_install.cmake" + ) + +set_target_properties(${current_target} + PROPERTIES + OUTPUT_NAME qxredis + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR} + ) + +# Правила для установки +install(TARGETS ${current_target}_static ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +if(BUILD_SHARED_LIBS) + install(TARGETS ${current_target}_shared LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) +endif() +install(FILES ${current_target_headers} + COMPONENT headers + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${current_target}) +install(FILES ${CMAKE_BINARY_DIR}/${current_target}.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) + diff --git a/src/myx/redis/client.cpp b/src/myx/redis/client.cpp new file mode 100644 index 0000000..3315d0b --- /dev/null +++ b/src/myx/redis/client.cpp @@ -0,0 +1,66 @@ +#include +#include + +using namespace qxredis; + +ClientPrivate::ClientPrivate( Client* client ) : + lexer ( &socket ), + parser( &lexer ) +{ + connect( &socket, &QTcpSocket::connected, client, &Client::connected ); + connect( &socket, &QTcpSocket::disconnected, client, &Client::disconnected ); + connect( &parser, &Parser::reply, this, &ClientPrivate::sendReply ); +} + + +void ClientPrivate::sendReply( const Reply& reply ) +{ + Q_EMIT queue.dequeue()->reply( reply ); +} + + +Client::Client( QObject* parent ) : + QObject( parent ), + d ( new ClientPrivate( this ) ) +{ +} + + +void Client::connectToHost( const QString& hostName, quint16 port ) +{ + d->socket.connectToHost( hostName, port ); +} + + +void Client::disconnectFromHost() +{ + d->socket.disconnectFromHost(); +} + + +bool Client::isConnected() const +{ + return( d->socket.state() == QAbstractSocket::ConnectedState ); +} + + +Request* Client::sendCommand( const QByteArray& command ) +{ + d->socket.write( command + "\r\n" ); + + auto request = new Request( this ); + d->queue.enqueue( request ); + return( request ); +} + + +bool Client::waitForConnected( int msecs ) +{ + return( d->socket.waitForConnected( msecs ) ); +} + + +bool Client::waitForDisconnected( int msecs ) +{ + return( d->socket.waitForDisconnected( msecs ) ); +} diff --git a/src/myx/redis/client.hpp b/src/myx/redis/client.hpp new file mode 100644 index 0000000..966e65f --- /dev/null +++ b/src/myx/redis/client.hpp @@ -0,0 +1,110 @@ +#ifndef QXREDIS_CLIENT_HPP_ +#define QXREDIS_CLIENT_HPP_ + +#include +#include + +#include +#include + +namespace qxredis +{ + +class QXREDIS_EXPORT ClientPrivate; + +/** + * @brief Provides access to a Redis server + */ +class QXREDIS_EXPORT Client : public QObject +{ + Q_OBJECT + +public: + + /** + * @brief Creates a Redis client + * @param parent the parent QObject + */ + explicit Client( QObject* parent = nullptr ); + + /** + * @brief Destroys the client + */ + virtual ~Client() = default; + + /* + * Note: we specifically avoid an overload of connectToHost that + * uses QHostAddress since that would force anyone using the client + * library to use the QtNetwork module, which we wish to avoid. + */ + + /** + * @brief Attempts to connect to the specified Redis server + * @param hostName the hostname of the Redis server + * @param port the port that the Redis server is listening on + * + * If the connection was successful, the connected() signal will be + * emitted. + */ + void connectToHost( const QString& hostName, quint16 port = 6379 ); + + /** + * @brief Disconnects from the Redis server + */ + void disconnectFromHost(); + + /** + * @brief Indicates whether the client is connected to a Redis server + * @return true if the client is connected + */ + bool isConnected() const; + + /** + * @brief Sends the specified command to the Redis server + * @param command the command to execute + * @return an object representing the request + */ + Request* sendCommand( const QByteArray& command ); + + /** + * @brief Attempts to set the specified key to the specified value + * @param name the name of the key + * @param value the value of the key + * @return the request issued + */ + Request* set( const QByteArray& name, const QByteArray& value ); + + /** + * @brief Waits for the socket to finish connecting + * @param msecs the amount of time in milliseconds to wait + * @return true if the connection was completed + * + * Note: to wait indefinitely, pass a value of -1. + */ + bool waitForConnected( int msecs = 30000 ); + + /** + * @brief Waits for the socket to finish disconnecting + * @param msecs the amount of time in milliseconds to wait + * @return true if the disconnection was completed + */ + bool waitForDisconnected( int msecs = 30000 ); + + /** + * @brief Emitted when the client establishes a connection with the Redis server + */ + Q_SIGNAL void connected(); + + /** + * @brief Emitted when the client disconnects from the Redis server + */ + Q_SIGNAL void disconnected(); + +private: + + const QScopedPointer< ClientPrivate > d; +}; // class QXREDIS_EXPORT + +} // namespace QXRedis + +#endif // QXREDIS_CLIENT_HPP_ diff --git a/src/myx/redis/client_p.hpp b/src/myx/redis/client_p.hpp new file mode 100644 index 0000000..f8da808 --- /dev/null +++ b/src/myx/redis/client_p.hpp @@ -0,0 +1,36 @@ +#ifndef QREDIS_CLIENT_P_H +#define QREDIS_CLIENT_P_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace qxredis +{ + +class ClientPrivate : public QObject +{ + Q_OBJECT + +public: + ClientPrivate( Client* ); + + QTcpSocket socket; + QQueue< Request* > queue; + + Lexer lexer; + Parser parser; + +private: + Q_SLOT void sendReply(const qxredis::Reply & ); +}; // class ClientPrivate + +} // namespace QRedis + +#endif // QREDIS_CLIENT_H diff --git a/src/myx/redis/config.cpp b/src/myx/redis/config.cpp new file mode 100644 index 0000000..6a7852b --- /dev/null +++ b/src/myx/redis/config.cpp @@ -0,0 +1 @@ +#include "config.hpp" diff --git a/src/myx/redis/config.hpp b/src/myx/redis/config.hpp new file mode 100644 index 0000000..5350c28 --- /dev/null +++ b/src/myx/redis/config.hpp @@ -0,0 +1,15 @@ +#ifndef QXREDIS_CONFIG_HPP_ +#define QXREDIS_CONFIG_HPP_ + +#include + +// #if defined( qredis_EXPORTS ) + #define QXREDIS_EXPORT Q_DECL_EXPORT +// #else +// #define QXREDIS_EXPORT Q_DECL_IMPORT +// #endif + +#include "cmlib_config.hpp" +#include "config_flags.hpp" + +#endif // QXREDIS_CONFIG_HPP_ diff --git a/src/myx/redis/config_flags.hpp.in b/src/myx/redis/config_flags.hpp.in new file mode 100644 index 0000000..8f19958 --- /dev/null +++ b/src/myx/redis/config_flags.hpp.in @@ -0,0 +1,7 @@ +#ifndef @CMLIB_PROJECT_NAME_CANONICAL@_CONFIG_FLAGS_HPP_ +#define @CMLIB_PROJECT_NAME_CANONICAL@_CONFIG_FLAGS_HPP_ + +// #cmakedefine + +#endif /* @CMLIB_PROJECT_NAME_CANONICAL@_CONFIG_FLAGS_HPP_ */ + diff --git a/src/myx/redis/lexer.cpp b/src/myx/redis/lexer.cpp new file mode 100644 index 0000000..9f4ad43 --- /dev/null +++ b/src/myx/redis/lexer.cpp @@ -0,0 +1,115 @@ +#include "lexer.hpp" + +using namespace qxredis; + +Lexer::Lexer( QIODevice* device, QObject* parent ) : + QObject ( parent ), + m_device( device ), + m_state ( DoingNothing ), + m_crlf ( 0 ), + m_length( 0 ) +{ + connect( device, &QIODevice::readyRead, this, &Lexer::readData ); +} + + +void Lexer::readData() +{ + m_buffer.append( m_device->readAll() ); + + while ( true ) + { + if ( ( m_state == DoingNothing ) && !readCharacter() ) + { + break; + } + + switch ( m_state ) + { + case ReadingLength: + case ReadingUnsafeString: + if ( !readUnsafeString() ) { return; } + break; + case ReadingSafeString: + if ( !readSafeString() ) { return; } + break; + case DoingNothing: + break; + } + + if ( m_state != ReadingSafeString ) + { + m_state = DoingNothing; + } + } +} // Lexer::readData + + +bool Lexer::readCharacter() +{ + if ( m_buffer.isEmpty() ) + { + return( false ); + } + + char c = m_buffer.at( 0 ); + m_buffer.remove( 0, 1 ); + + switch ( c ) + { + case '+': + case '-': + case ':': + case '*': + m_state = ReadingUnsafeString; break; + case '$': + m_state = ReadingLength; break; + } + + Q_EMIT character( c ); + return( true ); +} // Lexer::readCharacter + + +bool Lexer::readUnsafeString() +{ + m_crlf = m_buffer.indexOf( "\r\n", m_crlf ); + if ( m_crlf == -1 ) + { + m_crlf = m_buffer.size(); + return( false ); + } + + QString s = m_buffer.mid( 0, m_crlf ); + m_buffer.remove( 0, m_crlf + 2 ); + + if ( m_state == ReadingLength ) + { + m_length = s.toInt(); + m_state = ReadingSafeString; + } + else + { + Q_EMIT unsafeString( s ); + } + + m_crlf = 0; + return( true ); +} // Lexer::readUnsafeString + + +bool Lexer::readSafeString() +{ + if ( m_buffer.size() < m_length + 2 ) + { + return( false ); + } + + QByteArray d = m_buffer.mid( 0, m_length ); + m_buffer.remove( 0, m_length + 2 ); + + Q_EMIT safeString( d ); + + m_state = DoingNothing; + return( true ); +} diff --git a/src/myx/redis/lexer.hpp b/src/myx/redis/lexer.hpp new file mode 100644 index 0000000..5cc8169 --- /dev/null +++ b/src/myx/redis/lexer.hpp @@ -0,0 +1,51 @@ +#ifndef QXREDIS_LEXER_HPP_ +#define QXREDIS_LEXER_HPP_ + +#include +#include + +namespace qxredis +{ + +class Lexer : public QObject +{ + Q_OBJECT + +public: + + Lexer( QIODevice*, QObject* = nullptr ); + virtual ~Lexer() = default; + + Q_SIGNAL void character( char ); + Q_SIGNAL void unsafeString( const QString& ); + Q_SIGNAL void safeString( const QByteArray& ); + +private: + + Q_SLOT void readData(); + +private: + + bool readCharacter(); + bool readLength(); + bool readUnsafeString(); + bool readSafeString(); + + QIODevice* m_device; + QByteArray m_buffer; + + enum + { + DoingNothing, + ReadingLength, + ReadingUnsafeString, + ReadingSafeString + } m_state; + + int m_crlf; + int m_length; +}; // class Lexer + +} // namespace QRedis + +#endif // QREDIS_LEXER_H diff --git a/src/myx/redis/parser.cpp b/src/myx/redis/parser.cpp new file mode 100644 index 0000000..f74699b --- /dev/null +++ b/src/myx/redis/parser.cpp @@ -0,0 +1,76 @@ +#include "parser.hpp" + +using namespace qxredis; + +Parser::Parser( Lexer* lexer, QObject* parent ) : + QObject( parent ) +{ + connect( lexer, &Lexer::character, this, &Parser::readCharacter ); + connect( lexer, &Lexer::unsafeString, this, &Parser::readUnsafeString ); + connect( lexer, &Lexer::safeString, this, &Parser::readSafeString ); +} + + +void Parser::readCharacter( const char c ) +{ + switch ( c ) + { + case '+': + stack.append( Task( Reply::Status ) ); break; + case '-': + stack.append( Task( Reply::Error ) ); break; + case ':': + stack.append( Task( Reply::Integer ) ); break; + case '$': + stack.append( Task( Reply::Bulk ) ); break; + case '*': + stack.append( Task( Reply::MultiBulk ) ); break; + default: + break; + } +} + + +void Parser::readUnsafeString( const QString& value ) +{ + if ( tos().reply.type() == Reply::MultiBulk ) + { + tos().count = value.toInt(); + } + else + { + tos().reply.value() = value; + } + + descend(); +} + + +void Parser::readSafeString( const QByteArray& value ) +{ + tos().reply.value() = value; + descend(); +} + + +void Parser::descend() +{ + while ( true ) + { + if ( ( tos().reply.type() == Reply::MultiBulk ) && + ( tos().reply.value().toList().count() < tos().count ) ) + { + return; + } + + if ( stack.count() == 1 ) + { + auto r = stack.takeLast().reply; + Q_EMIT reply( r ); + return; + } + + auto r = stack.takeLast().reply; + tos().reply.value().toList().append( QVariant::fromValue( r ) ); + } +} diff --git a/src/myx/redis/parser.hpp b/src/myx/redis/parser.hpp new file mode 100644 index 0000000..1f027ec --- /dev/null +++ b/src/myx/redis/parser.hpp @@ -0,0 +1,54 @@ +#ifndef QREDIS_PARSER_H +#define QREDIS_PARSER_H + +#include +#include +#include +#include + +#include +#include + +namespace qxredis +{ + +class Parser : public QObject +{ + Q_OBJECT + +public: + Parser( Lexer*, QObject* = nullptr ); + virtual ~Parser() = default; + + Q_SIGNAL void reply( const qxredis::Reply& ); + +private: + Q_SLOT void readCharacter( const char ); + Q_SLOT void readUnsafeString( const QString& ); + Q_SLOT void readSafeString( const QByteArray& ); + +private: + void descend(); + + class Task + { +public: + + enum { Unknown = -2 }; + + Task( Reply::Type type ) : + reply( type ), + count( Unknown ) {} + + Reply reply; + int count; + }; + + QList< Task > stack; + + Task& tos() { return( stack.last() ); } +}; // class Parser + +} // namespace QRedis + +#endif // QREDIS_PARSER_H diff --git a/src/myx/redis/reply.hpp b/src/myx/redis/reply.hpp new file mode 100644 index 0000000..37d502f --- /dev/null +++ b/src/myx/redis/reply.hpp @@ -0,0 +1,107 @@ +#ifndef QXREDIS_REPLY_HPP_ +#define QXREDIS_REPLY_HPP_ + +#include + +#include + +namespace qxredis +{ + +/** + * @brief Represents a Redis reply + */ +class QXREDIS_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; +}; // class QXREDIS_EXPORT Reply + +} // namespace qxredis + +Q_DECLARE_METATYPE( qxredis::Reply ) + +#endif // QXREDIS_REPLY_HPP_ diff --git a/src/myx/redis/request.cpp b/src/myx/redis/request.cpp new file mode 100644 index 0000000..468218c --- /dev/null +++ b/src/myx/redis/request.cpp @@ -0,0 +1,36 @@ +#include + +#include +#include + +using namespace qxredis; + +void RequestPrivate::quitEventLoop() +{ + loop.exit( 1 ); +} + + +Request::Request( QObject* parent ) : + QObject( parent ), + d ( new RequestPrivate ) +{ + connect( this, &Request::reply, this, &Request::deleteLater ); +} + + +bool Request::waitForReply( int msecs ) +{ + QTimer timer; + timer.setInterval( msecs ); + timer.setSingleShot( true ); + + connect( &timer, &QTimer::timeout, &d->loop, &QEventLoop::quit ); + connect( this, &Request::reply, d.data(), &RequestPrivate::quitEventLoop ); + + /* + * If the timer fires, the return value will be 0. + * Otherwise, quitEventLoop() will terminate the loop with 1. + */ + return( ( d->loop.exec( QEventLoop::ExcludeUserInputEvents ) != 0 ) ); +} diff --git a/src/myx/redis/request.hpp b/src/myx/redis/request.hpp new file mode 100644 index 0000000..9f2d7d7 --- /dev/null +++ b/src/myx/redis/request.hpp @@ -0,0 +1,55 @@ +#ifndef QREDIS_REQUEST_H +#define QREDIS_REQUEST_H + +#include +#include + +#include +#include + +namespace qxredis +{ + +class QXREDIS_EXPORT RequestPrivate; + +/** + * @brief Represents a Redis command and its response + */ +class QXREDIS_EXPORT Request : public QObject +{ + Q_OBJECT + +public: + + /** + * @brief Initializes the request + * @param parent the parent QObject + */ + explicit Request( QObject* parent = nullptr ); + + /** + * @brief Destroys the request + */ + virtual ~Request() = default; + + /** + * @brief Waits for the reply to be received + * @param msecs the amount of time in milliseconds to wait + * @return true if the reply was received + */ + bool waitForReply( int msecs = 10000 ); + + /** + * @brief Emitted when a reply is received + * @param reply the reply received + */ + Q_SIGNAL void reply( const qxredis::Reply& reply ); + +private: + + const QScopedPointer< RequestPrivate > d; +}; // class QXREDIS_EXPORT + +} // namespace QRedis + +#endif // QREDIS_REQUEST_H diff --git a/src/myx/redis/request_p.hpp b/src/myx/redis/request_p.hpp new file mode 100644 index 0000000..8a6f896 --- /dev/null +++ b/src/myx/redis/request_p.hpp @@ -0,0 +1,23 @@ +#ifndef QREDIS_REQUEST_P_H +#define QREDIS_REQUEST_P_H + +#include +#include + +namespace qxredis +{ + +class RequestPrivate : public QObject +{ + Q_OBJECT + +public: + explicit RequestPrivate( QObject* parent = nullptr ); + + QEventLoop loop; + Q_SLOT void quitEventLoop(); +}; + +} // namespace QRedis + +#endif // QREDIS_REQUEST_P_H