From b41f17a9b20dbb45fe59a963d08cc518beee8871 Mon Sep 17 00:00:00 2001 From: Andrey Astafyev Date: Sun, 26 Apr 2020 21:38:35 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20Redis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/redis/01_client/client.cpp | 2 + src/myx/redis.old/CMakeLists.txt | 72 - src/myx/redis.old/client-inl.hpp | 74 - src/myx/redis.old/client.hpp | 180 -- src/myx/redis.old/lexer-inl.hpp | 130 -- src/myx/redis.old/lexer.cpp | 5 - src/myx/redis.old/lexer.hpp | 66 - src/myx/redis.old/parser-inl.hpp | 106 -- src/myx/redis.old/parser.cpp | 5 - src/myx/redis.old/parser.hpp | 70 - src/myx/redis.old/reply.hpp | 111 -- src/myx/redis.old/request-inl.hpp | 54 - src/myx/redis.old/request.cpp | 5 - src/myx/redis.old/request.hpp | 83 - src/myx/redis/CMakeLists.txt | 3 + src/myx/redis/base-inl.hpp | 2 +- src/myx/redis/client-inl.hpp | 2280 +++++++++++++++++++++++ src/myx/{redis.old => redis}/client.cpp | 0 src/myx/redis/client.hpp | 157 ++ src/myx/redis/pubsub-inl.hpp | 13 +- 20 files changed, 2450 insertions(+), 968 deletions(-) delete mode 100644 src/myx/redis.old/CMakeLists.txt delete mode 100644 src/myx/redis.old/client-inl.hpp delete mode 100644 src/myx/redis.old/client.hpp delete mode 100644 src/myx/redis.old/lexer-inl.hpp delete mode 100644 src/myx/redis.old/lexer.cpp delete mode 100644 src/myx/redis.old/lexer.hpp delete mode 100644 src/myx/redis.old/parser-inl.hpp delete mode 100644 src/myx/redis.old/parser.cpp delete mode 100644 src/myx/redis.old/parser.hpp delete mode 100644 src/myx/redis.old/reply.hpp delete mode 100644 src/myx/redis.old/request-inl.hpp delete mode 100644 src/myx/redis.old/request.cpp delete mode 100644 src/myx/redis.old/request.hpp create mode 100644 src/myx/redis/client-inl.hpp rename src/myx/{redis.old => redis}/client.cpp (100%) create mode 100644 src/myx/redis/client.hpp diff --git a/examples/redis/01_client/client.cpp b/examples/redis/01_client/client.cpp index 0fda55f..9700faf 100644 --- a/examples/redis/01_client/client.cpp +++ b/examples/redis/01_client/client.cpp @@ -1,6 +1,7 @@ #include "client.hpp" #include +#include #include #include @@ -12,6 +13,7 @@ int main( int argc, char** argv ) QCoreApplication app( argc, argv ); MR::PubSub a; + MR::Client b; RedisClient c( &a ); a.start(); diff --git a/src/myx/redis.old/CMakeLists.txt b/src/myx/redis.old/CMakeLists.txt deleted file mode 100644 index a8fc417..0000000 --- a/src/myx/redis.old/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -# Название основной цели и имя библиотеки в текущем каталоге -set(TRGT redis) - -# cmake-format: off -# Список файлов исходных текстов -set(TRGT_cpp - ${CMAKE_CURRENT_SOURCE_DIR}/client.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/lexer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/parser.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/request.cpp) - -# Список заголовочных файлов (используется для установки) -set(TRGT_moc_hpp - ${CMAKE_CURRENT_SOURCE_DIR}/client.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/lexer.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/parser.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/request.hpp) - -set(TRGT_hpp - ${CMAKE_CURRENT_SOURCE_DIR}/reply.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/client-inl.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/lexer-inl.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/parser-inl.hpp - ${CMAKE_CURRENT_SOURCE_DIR}/request-inl.hpp) - -set(TRGT_headers ${TRGT_moc_hpp} ${TRGT_hpp}) -# cmake-format: on - -qt5_wrap_cpp(TRGT_moc_cpp ${TRGT_moc_hpp}) - -add_library(${TRGT}-header-only INTERFACE) -target_include_directories( - ${TRGT}-header-only SYSTEM INTERFACE "$" - "$") - -if(MYXLIB_BUILD_LIBRARIES) - add_common_library(${TRGT} OUTPUT_NAME myx-${TRGT} SOURCES ${TRGT_moc_cpp} ${TRGT_cpp} ${TRGT_headers}) - common_target_properties(${TRGT}) - - # Создание цели для проверки утилитой clang-tidy - add_clang_tidy_check(${TRGT} ${TRGT_cpp} ${TRGT_headers}) - - # Создание цели для проверки утилитой clang-analyze - add_clang_analyze_check(${TRGT} ${TRGT_cpp} ${TRGT_headers}) - - # Создание цели для проверки утилитой clazy - add_clazy_check(${TRGT} ${TRGT_cpp} ${TRGT_headers}) - - # Создание цели для проверки утилитой pvs-studio - add_pvs_check(${TRGT}) - - # Создание цели для автоматического форматирования кода - add_format_sources(${TRGT} ${TRGT_cpp} ${TRGT_headers}) - - target_compile_definitions(${TRGT} PUBLIC MYXLIB_BUILD_LIBRARIES) - target_include_directories(${TRGT} SYSTEM PUBLIC ${Qt5Core_INCLUDE_DIRS}) - target_include_directories(${TRGT} SYSTEM PUBLIC ${Qt5Network_INCLUDE_DIRS}) - target_include_directories(${TRGT} SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/src) - - cotire(${TRGT}) - install(TARGETS ${TRGT}_static COMPONENT libs-dev ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) - if(BUILD_SHARED_LIBS) - install(TARGETS ${TRGT}_shared COMPONENT main LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) - endif() -endif() - -generate_pkgconfig(myx-${TRGT} COMPONENT base-dev INSTALL_LIBRARY ${MYXLIB_BUILD_LIBRARIES}) -install(FILES ${TRGT_headers} COMPONENT base-dev DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}/${TRGT}) - -# Цель, используемая только для установки заголовочных файлов без компиляции проекта -add_custom_target(${TRGT}-install-headers COMMAND ${CMAKE_COMMAND} -DCMAKE_INSTALL_COMPONENT=base-dev -P - "${CMAKE_BINARY_DIR}/cmake_install.cmake") diff --git a/src/myx/redis.old/client-inl.hpp b/src/myx/redis.old/client-inl.hpp deleted file mode 100644 index c00a5b0..0000000 --- a/src/myx/redis.old/client-inl.hpp +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef MYX_REDIS_CLIENT_INL_HPP_ -#define MYX_REDIS_CLIENT_INL_HPP_ - -#pragma once - -#ifndef MYXLIB_HEADER_ONLY -#include -#endif - -namespace myx { - -namespace redis { - -MYXLIB_INLINE Client::Client( QObject* parent ) : - QObject( parent ), - d ( new ClientPrivate( this ) ) -{ -} - - -MYXLIB_INLINE void Client::connectToHost( const QString& hostName, quint16 port ) -{ - d->m_socket.connectToHost( hostName, port ); -} - - -MYXLIB_INLINE void Client::disconnectFromHost() -{ - d->m_socket.disconnectFromHost(); -} - - -MYXLIB_INLINE bool Client::isConnected() const -{ - return( d->m_socket.state() == QAbstractSocket::ConnectedState ); -} - - -MYXLIB_INLINE Request* Client::sendCommand( const QByteArray& command ) -{ - d->m_socket.write( command + "\r\n" ); - - auto* request = new Request( this ); - d->m_queue.enqueue( request ); - return( request ); -} - - -MYXLIB_INLINE Request* Client::subscribeToChannel( const QByteArray& channel ) -{ - d->m_socket.write( "subscribe " + channel + "\r\n" ); - - auto* request = new Request( channel ); - d->m_list.append( request ); - return( request ); -} - - -MYXLIB_INLINE bool Client::waitForConnected( int msecs ) -{ - return( d->m_socket.waitForConnected( msecs ) ); -} - - -MYXLIB_INLINE bool Client::waitForDisconnected( int msecs ) -{ - return( d->m_socket.waitForDisconnected( msecs ) ); -} - -} // namespace redis - -} // namespace myx - -#endif // ifndef MYX_REDIS_CLIENT_INL_HPP_ diff --git a/src/myx/redis.old/client.hpp b/src/myx/redis.old/client.hpp deleted file mode 100644 index 7ae8a2b..0000000 --- a/src/myx/redis.old/client.hpp +++ /dev/null @@ -1,180 +0,0 @@ -#ifndef MYX_REDIS_CLIENT_HPP_ -#define MYX_REDIS_CLIENT_HPP_ - -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace myx { - -namespace redis { - -QT_FORWARD_DECLARE_CLASS( ClientPrivate ) - -/** - * @brief Provides access to a Redis server - */ -class Client : public QObject -{ - Q_OBJECT - -public: - - /** - * @brief Creates a Redis client - * @param parent the parent QObject - */ - explicit Client( QObject* parent = nullptr ); - - Client( const Client& ) = delete; - Client& operator=( const Client& ) = delete; - Client( Client&& ) = delete; - Client& operator=( Client&& ) = delete; - - /** - * @brief Destroys the client - */ - ~Client() override = 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 ); - - Request* subscribeToChannel( const QByteArray& channel ); - - /** - * @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 Client - -class ClientPrivate : public QObject -{ - Q_OBJECT - - friend class Client; - - explicit ClientPrivate( Client* client = nullptr ) : - m_lexer ( &m_socket ), - m_parser( &m_lexer ) - { - connect( &m_socket, &QTcpSocket::connected, client, &Client::connected ); - connect( &m_socket, &QTcpSocket::disconnected, client, &Client::disconnected ); - connect( &m_parser, &Parser::reply, this, &ClientPrivate::sendReply ); - connect( &m_parser, &Parser::replyMultiBulk, this, &ClientPrivate::sendReplyMultiBulk ); - } - - - QTcpSocket m_socket; - QQueue< Request* > m_queue; - QList< Request* > m_list; - Lexer m_lexer; - Parser m_parser; - - Q_SLOT void sendReply( const myx::redis::Reply& reply ) - { - if ( m_queue.isEmpty() ) { return; } - Q_EMIT m_queue.dequeue()->reply( const_cast< myx::redis::Reply& >( reply ) ); - } - - - Q_SLOT void sendReplyMultiBulk( const myx::redis::Reply& reply ) - { - if ( m_list.isEmpty() ) { return; } - - QVariant v = const_cast< myx::redis::Reply& >( reply ).value(); - if ( !v.isValid() ) { return; } - if ( !v.canConvert< QVariantList >() ) { return; } - auto l = v.toList(); - if ( l.size() != 3 ) { return; } - - auto name = l[1].value< Reply >().value().toByteArray(); - for ( auto& request: m_list ) - { - if ( request->channel() == name ) - { - Q_EMIT request->reply( const_cast< myx::redis::Reply& >( reply ) ); - } - } - } -}; // class ClientPrivate - -} // namespace redis - -} // namespace myx - -#ifdef MYXLIB_HEADER_ONLY -#include "client-inl.hpp" -#endif - - -#endif // ifndef MYX_REDIS_CLIENT_HPP_ diff --git a/src/myx/redis.old/lexer-inl.hpp b/src/myx/redis.old/lexer-inl.hpp deleted file mode 100644 index a6826fb..0000000 --- a/src/myx/redis.old/lexer-inl.hpp +++ /dev/null @@ -1,130 +0,0 @@ -#ifndef MYX_REDIS_LEXER_INL_HPP_ -#define MYX_REDIS_LEXER_INL_HPP_ - -#pragma once - -#ifndef MYXLIB_HEADER_ONLY -#include -#endif - -namespace myx { - -namespace redis { - -MYXLIB_INLINE Lexer::Lexer( QIODevice* device, QObject* parent ) : - QObject ( parent ), - m_device( device ), - m_state ( kDoingNothing ), - m_crlf ( 0 ), - m_length( 0 ) -{ - connect( device, &QIODevice::readyRead, this, &Lexer::readData ); -} - - -MYXLIB_INLINE void Lexer::readData() -{ - m_buffer.append( m_device->readAll() ); - - while ( true ) - { - if ( ( m_state == kDoingNothing ) && !readCharacter() ) - { - break; - } - - switch ( m_state ) - { - case kReadingLength: - case kReadingUnsafeString: - if ( !readUnsafeString() ) { return; } - break; - case kReadingSafeString: - if ( !readSafeString() ) { return; } - break; - case kDoingNothing: - break; - } - - if ( m_state != kReadingSafeString ) - { - m_state = kDoingNothing; - } - } -} // Lexer::readData - - -MYXLIB_INLINE 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 = kReadingUnsafeString; break; - case '$': - m_state = kReadingLength; break; - } - - Q_EMIT character( c ); - return( true ); -} // Lexer::readCharacter - - -MYXLIB_INLINE 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 == kReadingLength ) - { - m_length = s.toInt(); - m_state = kReadingSafeString; - } - else - { - Q_EMIT unsafeString( s ); - } - - m_crlf = 0; - return( true ); -} // Lexer::readUnsafeString - - -MYXLIB_INLINE 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 = kDoingNothing; - return( true ); -} - -} // namespace redis - -} // namespace myx - -#endif // ifndef MYX_REDIS_LEXER_INL_HPP_ diff --git a/src/myx/redis.old/lexer.cpp b/src/myx/redis.old/lexer.cpp deleted file mode 100644 index 7e545e5..0000000 --- a/src/myx/redis.old/lexer.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef MYXLIB_BUILD_LIBRARIES -#error Define MYXLIB_BUILD_LIBRARIES to compile this file. -#endif - -#include diff --git a/src/myx/redis.old/lexer.hpp b/src/myx/redis.old/lexer.hpp deleted file mode 100644 index c53b981..0000000 --- a/src/myx/redis.old/lexer.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef MYX_REDIS_LEXER_HPP_ -#define MYX_REDIS_LEXER_HPP_ - -#pragma once - -#include - -#include - -namespace myx { - -namespace redis { - -class Lexer : public QObject -{ - Q_OBJECT - -public: - - explicit Lexer( QIODevice*, QObject* = nullptr ); - - Lexer( const Lexer& ) = delete; - Lexer& operator=( const Lexer& ) = delete; - Lexer( Lexer&& ) = delete; - Lexer& operator=( Lexer&& ) = delete; - - ~Lexer() override = default; - - Q_SIGNAL void character( char ); - Q_SIGNAL void unsafeString( const QString& ); - Q_SIGNAL void safeString( const QByteArray& ); - -private: - - Q_SLOT void readData(); - - bool readCharacter(); - bool readLength(); - bool readUnsafeString(); - bool readSafeString(); - - QIODevice* m_device; - QByteArray m_buffer; - - enum - { - kDoingNothing, - kReadingLength, - kReadingUnsafeString, - kReadingSafeString - } m_state; - - int m_crlf; - int m_length; -}; // class Lexer - -} // namespace redis - -} // namespace myx - -#ifdef MYXLIB_HEADER_ONLY -#include "lexer-inl.hpp" -#endif - - -#endif // ifndef MYX_REDIS_LEXER_HPP_ diff --git a/src/myx/redis.old/parser-inl.hpp b/src/myx/redis.old/parser-inl.hpp deleted file mode 100644 index 0ce98f5..0000000 --- a/src/myx/redis.old/parser-inl.hpp +++ /dev/null @@ -1,106 +0,0 @@ -#ifndef MYX_REDIS_PARSER_INL_HPP_ -#define MYX_REDIS_PARSER_INL_HPP_ - -#pragma once - -#ifndef MYXLIB_HEADER_ONLY -#include -#endif - -#include - -namespace myx { - -namespace redis { - -MYXLIB_INLINE Parser::Parser( Lexer* lexer, QObject* parent ) : - QObject( parent ), - m_lexer( lexer ) -{ - connect( m_lexer, &Lexer::character, this, &Parser::readCharacter ); - connect( m_lexer, &Lexer::unsafeString, this, &Parser::readUnsafeString ); - connect( m_lexer, &Lexer::safeString, this, &Parser::readSafeString ); -} - - -MYXLIB_INLINE void Parser::readCharacter( const char c ) -{ - switch ( c ) - { - case '+': - m_stack.append( Task( Reply::kStatus ) ); break; - case '-': - m_stack.append( Task( Reply::kError ) ); break; - case ':': - m_stack.append( Task( Reply::kInteger ) ); break; - case '$': - m_stack.append( Task( Reply::kBulk ) ); break; - case '*': - m_stack.append( Task( Reply::kMultiBulk ) ); break; - default: - break; - } -} - - -MYXLIB_INLINE void Parser::readUnsafeString( const QString& value ) -{ - if ( tos().m_reply.type() == Reply::kMultiBulk ) - { - tos().m_count = value.toInt(); - } - else - { - tos().m_reply.value() = value; - } - - descend(); -} - - -MYXLIB_INLINE void Parser::readSafeString( const QByteArray& value ) -{ - tos().m_reply.value() = value; - descend(); -} - - -MYXLIB_INLINE void Parser::descend() -{ - while ( true ) - { - auto& task = tos(); - if ( ( task.m_reply.type() == Reply::kMultiBulk ) && - ( task.m_reply.value().toList().count() < task.m_count ) ) - { - return; - } - - if ( m_stack.count() == 1 ) - { - if ( task.m_reply.type() == Reply::kMultiBulk ) - { - Q_EMIT replyMultiBulk( m_stack.takeLast().m_reply ); - } - else - { - Q_EMIT reply( m_stack.takeLast().m_reply ); - } - return; - } - - auto r = m_stack.takeLast().m_reply; - - Task t = m_stack.takeLast(); - auto l = t.m_reply.value().toList(); - l.append( QVariant::fromValue( r ) ); - t.m_reply.value().setValue( l ); - m_stack.append( t ); - } -} // Parser::descend - -} // namespace redis - -} // namespace myx - -#endif // ifndef MYX_REDIS_PARSER_INL_HPP_ diff --git a/src/myx/redis.old/parser.cpp b/src/myx/redis.old/parser.cpp deleted file mode 100644 index 2480e99..0000000 --- a/src/myx/redis.old/parser.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef MYXLIB_BUILD_LIBRARIES -#error Define MYXLIB_BUILD_LIBRARIES to compile this file. -#endif - -#include diff --git a/src/myx/redis.old/parser.hpp b/src/myx/redis.old/parser.hpp deleted file mode 100644 index 887b95b..0000000 --- a/src/myx/redis.old/parser.hpp +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef MYX_REDIS_PARSER_HPP_ -#define MYX_REDIS_PARSER_HPP_ - -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace myx { - -namespace redis { - -class Parser : public QObject -{ - Q_OBJECT - -public: - explicit Parser( Lexer*, QObject* = nullptr ); - - Parser( const Parser& ) = delete; - Parser& operator=( const Parser& ) = delete; - Parser( Parser&& ) = delete; - Parser& operator=( Parser&& ) = delete; - - ~Parser() override = default; - - Q_SIGNAL void reply( const myx::redis::Reply& ); - Q_SIGNAL void replyMultiBulk( const myx::redis::Reply& ); - -private: - Q_SLOT void readCharacter( char ); - Q_SLOT void readUnsafeString( const QString& ); - Q_SLOT void readSafeString( const QByteArray& ); - - void descend(); - - class Task - { - friend class Parser; - - enum { kUnknown = -2 }; - - explicit Task( Reply::Type type ) : - m_reply( type ), - m_count( kUnknown ) {} - - Reply m_reply; - int m_count; - }; - - Lexer* m_lexer; - QList< Task > m_stack; - - Task& tos() { return( m_stack.last() ); } -}; // class Parser - -} // namespace redis - -} // namespace myx - -#ifdef MYXLIB_HEADER_ONLY -#include "parser-inl.hpp" -#endif - -#endif // ifndef MYX_REDIS_PARSER_HPP_ diff --git a/src/myx/redis.old/reply.hpp b/src/myx/redis.old/reply.hpp deleted file mode 100644 index cd1db37..0000000 --- a/src/myx/redis.old/reply.hpp +++ /dev/null @@ -1,111 +0,0 @@ -#ifndef MYX_REDIS_REPLY_HPP_ -#define MYX_REDIS_REPLY_HPP_ - -#pragma once - -#include - -#include - -namespace myx { - -namespace redis { - -/** - * @brief Represents a Redis reply - */ -class Reply -{ -public: - - /** - * @brief Reply types - */ - enum Type - { - /** - * @brief An invalid reply - * - * This value is only set when the default constructor is used. - */ - kInvalid, - - /** - * @brief A status reply - * - * The value property will contain the status message returned - * by the server as a QString. - */ - kStatus, - - /** - * @brief An error reply - * - * The value property will contain the error message returned by - * the server as a QString. - */ - kError, - - /** - * @brief An integer reply - * - * The value property will contain the integer value returned by - * the server as a qlonglong. - */ - kInteger, - - /** - * @brief A bulk reply - * - * The value property will contain the bulk reply returned by - * the server as a QByteArray. - */ - kBulk, - - /** - * @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. - */ - kMultiBulk - }; - - /** - * @brief Creates an empty reply - */ - Reply() = default; - - /** - * @brief Initializes the reply - * @param type the type of the reply - */ - explicit Reply( Type type ) : - m_type( type ) {} - - /** - * @brief Returns the type of the reply - * @return the reply type - */ - Type type() const { return( m_type ); } - - /** - * @brief Returns a reference to the value of the reply - * @return the reply value - */ - QVariant& value() { return( m_value ); } - -private: - - Type m_type { kInvalid }; - QVariant m_value; -}; // class Reply - -} // namespace redis - -} // namespace myx - -Q_DECLARE_METATYPE( myx::redis::Reply ) // NOLINT - -#endif // ifndef MYX_REDIS_REPLY_HPP_ diff --git a/src/myx/redis.old/request-inl.hpp b/src/myx/redis.old/request-inl.hpp deleted file mode 100644 index 765f339..0000000 --- a/src/myx/redis.old/request-inl.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef MYX_REDIS_REQUEST_INL_HPP_ -#define MYX_REDIS_REQUEST_INL_HPP_ - -#pragma once - -#include - -#ifndef MYXLIB_HEADER_ONLY -#include -#endif - -#include - -namespace myx { - -namespace redis { - -MYXLIB_INLINE Request::Request( QObject* parent ) : - QObject( parent ), - d ( new Loop ) -{ - connect( this, &Request::reply, this, &Request::deleteLater ); -} - - -MYXLIB_INLINE Request::Request( const QByteArray& channel, QObject* parent ) : - QObject( parent ), - d ( new Loop ) -{ - m_channel = channel; -} - - -MYXLIB_INLINE bool Request::waitForReply( int msecs ) -{ - QTimer timer; - timer.setInterval( msecs ); - timer.setSingleShot( true ); - - connect( &timer, &QTimer::timeout, &d->m_loop, &QEventLoop::quit ); - connect( this, &Request::reply, d.data(), &Loop::quitEventLoop ); - - /* - * If the timer fires, the return value will be 0. - * Otherwise, quitEventLoop() will terminate the loop with 1. - */ - return( ( d->m_loop.exec( QEventLoop::ExcludeUserInputEvents ) != 0 ) ); -} - -} // namespace redis - -} // namespace myx - -#endif // ifndef MYX_REDIS_REQUEST_INL_HPP_ diff --git a/src/myx/redis.old/request.cpp b/src/myx/redis.old/request.cpp deleted file mode 100644 index 63f05c1..0000000 --- a/src/myx/redis.old/request.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef MYXLIB_BUILD_LIBRARIES -#error Define MYXLIB_BUILD_LIBRARIES to compile this file. -#endif - -#include diff --git a/src/myx/redis.old/request.hpp b/src/myx/redis.old/request.hpp deleted file mode 100644 index 7d82b36..0000000 --- a/src/myx/redis.old/request.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef MYX_REDIS_REQUEST_HPP_ -#define MYX_REDIS_REQUEST_HPP_ - -#pragma once - -#include -#include - -#include -#include - - -namespace myx { - -namespace redis { - -class Loop : public QObject -{ - Q_OBJECT - - friend class Request; - explicit Loop( QObject* parent = nullptr ) { Q_UNUSED( parent ) } - - QEventLoop m_loop; - Q_SLOT void quitEventLoop() { m_loop.exit( 1 ); } -}; - - -/** - * @brief Represents a Redis command and its response - */ -class Request : public QObject -{ - Q_OBJECT - -public: - - /** - * @brief Initializes the request - * @param parent the parent QObject - */ - explicit Request( QObject* parent = nullptr ); - explicit Request( const QByteArray& channel, QObject* parent = nullptr ); - - Request( const Request& ) = delete; - Request& operator=( const Request& ) = delete; - Request( Request&& ) = delete; - Request& operator=( Request&& ) = delete; - - /** - * @brief Destroys the request - */ - ~Request() override = 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( myx::redis::Reply& ); - QByteArray channel() const { return( m_channel ); } - -private: - - QByteArray m_channel; - const QScopedPointer< Loop > d; -}; // class Request - -} // namespace redis - -} // namespace myx - -#ifdef MYXLIB_HEADER_ONLY -#include "request-inl.hpp" -#endif - -#endif // ifndef MYX_REDIS_REQUEST_HPP_ diff --git a/src/myx/redis/CMakeLists.txt b/src/myx/redis/CMakeLists.txt index 6aaba4c..6e833ec 100644 --- a/src/myx/redis/CMakeLists.txt +++ b/src/myx/redis/CMakeLists.txt @@ -5,16 +5,19 @@ set(TRGT redis) # Список файлов исходных текстов set(TRGT_cpp ${CMAKE_CURRENT_SOURCE_DIR}/base.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/client.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pubsub.cpp ${CMAKE_CURRENT_SOURCE_DIR}/containers.cpp) # Список заголовочных файлов set(TRGT_moc_hpp ${CMAKE_CURRENT_SOURCE_DIR}/base.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/client.hpp ${CMAKE_CURRENT_SOURCE_DIR}/pubsub.hpp) set(TRGT_hpp ${CMAKE_CURRENT_SOURCE_DIR}/base-inl.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/client-inl.hpp ${CMAKE_CURRENT_SOURCE_DIR}/containers.hpp ${CMAKE_CURRENT_SOURCE_DIR}/containers-inl.hpp ${CMAKE_CURRENT_SOURCE_DIR}/pubsub-inl.hpp) diff --git a/src/myx/redis/base-inl.hpp b/src/myx/redis/base-inl.hpp index 6bb94f7..1817554 100644 --- a/src/myx/redis/base-inl.hpp +++ b/src/myx/redis/base-inl.hpp @@ -118,7 +118,7 @@ MYXLIB_INLINE void Base::onReadyRead() /*! - * \brief Добавляет к состоянию флаг Read + * \brief Добавляет к состоянию флаг Write */ MYXLIB_INLINE void Base::onBytesWritten() { diff --git a/src/myx/redis/client-inl.hpp b/src/myx/redis/client-inl.hpp new file mode 100644 index 0000000..6893d82 --- /dev/null +++ b/src/myx/redis/client-inl.hpp @@ -0,0 +1,2280 @@ +#ifndef MYX_REDIS_CLIENT_INL_HPP_ +#define MYX_REDIS_CLIENT_INL_HPP_ + +#pragma once + +#ifndef MYXLIB_HEADER_ONLY +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include + + +namespace myx { + +namespace redis { + +namespace constants { + +extern const QByteArray Separator; // { "\r\n" }; //!< Строковый разделитель слов во фразе + +} // namespace constants + +namespace C = constants; + +/*! + * \brief Создает объект класса + * \param parent Родительский объект + * \param database Идентификатор БД + */ +Client::Client( int database, QObject* parent ) : + Base{ parent } +{ + _database = database; +} + + +/*! + * \brief Удаляет все созданные соединения + */ +Client::~Client() +{ + for ( auto iter: _session ) + { + if ( iter->state() == QTcpSocket::ConnectedState ) + { + iter->disconnectFromHost(); + if ( iter->state() != QTcpSocket::UnconnectedState ) + { + iter->waitForDisconnected(); + } + } + iter->deleteLater(); + } +} + + +/*! + * \brief Возвращает идентификатор БД + * \return Идентификатор БД + */ +int Client::database() const +{ + return( _database ); +} + + +/*! + * \brief Выполняет переключение текущей БД + * \param database Номер новой БД, к которой будет организовано подключение + */ +void Client::select( int database ) +{ + if ( _database == database ) + { + return; + } + _database = database; + _outOfDatabase = { _session.begin(), _session.end() }; +} + + +/*! + * \brief Определяет сколько из переданных ключей существует в redis + * \param keys Rk.xb + * \return Количество существующих ключей + */ +int Client::exists( const QStringList& keys ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( 0 ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( 0 ); + } + socket->write( array( QVariantList { "EXISTS" } + QVariant( keys ).toList() ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( 0 ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + if ( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ) + { + QRegExp rx { "\\d+" }; + rx.indexIn( buffer ); + return( rx.cap().toInt() ); + } + return( -1 ); +} // Client::exists + + +/*! + * \brief Очищает БД + */ +bool Client::flushdb() +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "$7", "FLUSHDB" } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::flushdb + + +/*! + * \brief Удаляет данные по списку ключей + * \param keys Список ключей для удаления + * \return Признак успешной операции + */ +bool Client::del( const QStringList& keys ) +{ + if ( keys.empty() ) + { + return( true ); + } + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "DEL" } + QVariant( keys ).toList() ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::del + + +/*! + * \brief Устанавливает таймаут на значение в секундах + * \param key Ключ значения + * \param secs Таймаут в секундах + * \return Признак успешной установки + */ +bool Client::expire( const QString& key, int secs ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "EXPIRE", key, secs } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( true ); +} // Client::expire + + +/*! + * \brief Устанавливает таймаут на значение в милисекундах + * \param key Ключ значения + * \param secs Таймаут в милисекундах + * \return Признак успешной установки + */ +bool Client::pexpire( const QString& key, int msecs ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "PEXPIRE", key, msecs } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( true ); +} // Client::pexpire + + +/*! + * \brief Удаляет таймаут со значения, делаю его persistent + * \param key Ключ значения + * \return Признак успешного удаления таймаута + */ +bool Client::persist( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "PERSIST", key } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( true ); +} // Client::persist + + +/*! + * \brief Публикует сообщение + * \param channel Канал сообщений + * \param message Текст сообщений + * \return Признак удачной публикации + */ +bool Client::publish( const QString& channel, const QByteArray& message ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + if ( message.isEmpty() ) + { + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "PUBLISH", channel, message } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( true ); +} // Client::publish + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param channel Канал сообщений + * \param message Текст сообщений + * \return Признак удачной публикации + */ +bool Client::publish( const QString& channel, const QByteArray& message, + const QString& host, quint16 port ) +{ + if ( message.isEmpty() ) + { + return( false ); + } + QTcpSocket socket; + do { + if ( socket.state() == QAbstractSocket::UnconnectedState ) + { + socket.connectToHost( host, port ); + } + QCoreApplication::processEvents(); + } while( socket.state() != QAbstractSocket::ConnectedState ); + socket.write( array( { "PUBLISH", channel, message } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket.state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( true ); +} // Client::publish + + +/*! + * \brief Выполняет команду SET протокола RESP + * \param key Ключ-строка + * \param value Значение по ключу + * \return Признак удачной установки значения + */ +bool Client::set( const QString& key, const QVariant& value ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "SET", key, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::set + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ данных + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Строка данных + */ +bool Client::set( const QString& key, const QVariant& value, + int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "SET", key, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket.state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::set + + +/*! + * \brief Загружает lua скрипт на сервер + * \param script Код скрипта + * \return SHA загруженного скрипта (пустая SHA в случае возникновения ошибки) + */ +QByteArray Client::scriptLoad( const QByteArray& script ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "SCRIPT", "LOAD", script } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer.startsWith( "-ERR" ) ) + { + qWarning() << "loading redis script error:" << buffer; + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + return( parts.front() ); + } + return {}; + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::scriptLoad + + +/*! + * \brief Синхронный метод получения значения + * \param key Ключ + * \return Строка данных + */ +QByteArray Client::get( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "GET", key } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer.startsWith( "-WRONG" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + return( parts.front() ); + } + return {}; + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::get + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ данных + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Строка данных + */ +QByteArray Client::get( const QString& key, int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "GET", key } ) ); + QByteArray buffer; + QByteArray value; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + value = parts.front(); + } + break; + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( value ); +} // Client::get + + +/*! + * \brief Задает значение, хранящееся в хэш-таблице + * \param key Ключ таблицы + * \param field Ключ значения + * \param value Значение + * \return Признак успешной операции + */ +bool Client::hset( const QString& key, const QString& field, const QVariant& value ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "HSET", key, field, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::hset + + +/*! + * \brief Заполняет hash по ключу + * \param key Ключ + * \param hash Hash таблица со значениями + * \return Признак корректного заполения + */ +bool Client::hmset( const QString& key, const QVariantHash& hash ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + QVariantList parts { "HMSET", key }; + for ( auto iter = hash.cbegin(); iter != hash.cend(); iter++ ) parts << iter.key() << iter.value(); + socket->write( array( parts ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::hmset + + +/*! + * \brief Заполняет удаляет данные из хэш-таблицы redis + * \param key Ключ таблицы для redis + * \param hashKeys Ключи удаляемых данных + * \return Признак корректного заполения + */ +bool Client::hdel( const QString& redisKey, const QStringList& hashKeys ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "HDEL", redisKey } + QVariant( hashKeys ).toList() ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::hdel + + +/*! + * \brief Заполняет hash по ключу + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ + * \param hash Hash таблица со значениями + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Признак корректного заполения + */ +bool Client::hmset( const QString& key, const QVariantHash& hash, int database, + const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + QVariantList parts { "HMSET", key }; + for ( auto iter = hash.cbegin(); iter != hash.cend(); iter++ ) parts << iter.key() << iter.value(); + socket.write( array( parts ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket.state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( buffer == "+OK\r\n" ); +} // Client::hmset + + +/*! + * \brief Синхронный метод хэш-таблицы + * \param key Ключ + * \return Таблица данных + */ +QVariantHash Client::hgetall( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "HGETALL", key } ) ); + QByteArray buffer; + QVariantHash hash; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer.startsWith( "-WRONG" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + for ( int i = 1; i < parts.size(); i += 2 ) hash[parts[i - 1]] = parts[i]; + return( hash ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::hgetall + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ данных + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Таблица данных + */ +QVariantHash Client::hgetall( const QString& key, int database, + const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "HGETALL", key } ) ); + QByteArray buffer; + QVariantHash hash; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) ) + { + break; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + for ( int i = 1; i < parts.size(); i += 2 ) hash[parts[i - 1]] = parts[i]; + break; + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( hash ); +} // Client::hgetall + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * \param redisKey Ключ данных для redis + * \param hashKey Ключ внутри запрашиваемой таблицы + * \return Значение из таблицы + */ +QByteArray Client::hget( const QString& redisKey, const QString& hashKey ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "HGET", redisKey, hashKey } ) ); + QByteArray value; + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + return( parts.front() ); + } + return {}; + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::hget + + +/*! + * \brief Получает данные из redis без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param redisKey Ключ данных для redis + * \param hashKey Ключ внутри запрашиваемой таблицы + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Значение из таблицы + */ +QByteArray Client::hget( const QString& redisKey, const QString& hashKey, + int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "HGET", redisKey, hashKey } ) ); + QByteArray value; + QByteArray buffer; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + value = parts.front(); + } + break; + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( value ); +} // Client::hget + + +/*! + * \brief Добавляет элементы в конец списка + * \param key Ключ списка + * \param values Добавляемые элементы + * \return Признак успешной операции + */ +bool Client::rpush( const QString& key, const QVariantList& values ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "RPUSH", key } + values ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::rpush + + +/*! + * \brief Добавляет элементы в начало списка + * \param key Ключ списка + * \param values Добавляемые элементы + * \return Признак успешной операции + */ +bool Client::lpush( const QString& key, const QVariantList& values ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "LPUSH", key } + values ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::lpush + + +/*! + * \brief Добавляет задает значение элемента по его индексу + * \param key Ключ списка + * \param index Индекс элемента + * \param index Значение элемента + * \return Признак успешной операции + */ +bool Client::lset( const QString& key, int index, const QVariant& value ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + auto indexString = QByteArray::number( index ); + auto valueString = value.toString(); + socket->write( array( { "LSET", key, index, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::lset + + +/*! + * \brief Оставляет только элементы списка внутри диапазона from...to + * \param key Ключ списка + * \param from Начало диапазона (0-based) + * > 0: от начала списка + * < 0: от конца списка + * \param to Конец диапазона (0-based) + * > 0: от начала списка + * < 0: от конца списка + * \return Признак успешной операции + */ +bool Client::ltrim( const QString& key, int from, int to ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "LTRIM", key, from, to } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == "+OK\r\n" ); +} // Client::ltrim + + +/*! + * \brief Удаляет count первых элементов списка + * \param key Ключ списка + * \param count Количество удаляемых элементов + * count > 0 от начала списка + * count < 0 от конца списка + * count = 0 удаляет все элементы + * \param value Значение удаляемых элементов + * \return Признак успешной операции + */ +bool Client::lrem( const QString& key, const QVariant& value, int count ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "LREM", key, count, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::lrem + + +/*! + * \brief Возвращает длину списка + * \param key Ключ списка + * \return Длина списка или -1 в случае ошибки операции + */ +int Client::llen( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( -1 ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( -1 ); + } + socket->write( array( { "LLEN", key } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( -1 ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + if ( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ) + { + QRegExp rx { "\\d+" }; + rx.indexIn( buffer ); + return( rx.cap().toInt() ); + } + return( -1 ); +} // Client::llen + + +/*! + * \brief Добавляет элементы в начало сета + * + * O(N), N - количество добавляемых элементов + * \param key Ключ сета + * \param values Добавляемые элементы + * \return Признак успешной операции + */ +bool Client::sadd( const QString& key, const QVariantList& values ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "SADD", key } + values ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::sadd + + +/*! + * \brief Удаляет значения из сета + * + * O(N), N - количество добавляемых элементов + * \param key Ключ сета + * \param values Добавляемые элементы + * \return Признак успешной операции + */ +bool Client::srem( const QString& key, const QVariantList& values ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( QVariantList { "SREM", key } + values ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ); +} // Client::srem + + +/*! + * \brief Проверяет является ли значение частью набора + * \param key Ключ набора + * \param value Проверяемое значение + * \return Признак того, что значение входит в набор + */ +bool Client::sismember( const QString& key, const QVariant& value ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( false ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( false ); + } + socket->write( array( { "SISMEMBER", key, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( buffer == ":1\r\n" ); +} // Client::sismember + + +/*! + * \brief Проверяет является ли значение частью набора без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ набора + * \param value Проверяемое значение + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Признак того, что значение входит в набор + */ +bool Client::sismember( const QString& key, const QVariant& value, + int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "SISMEMBER", key, value } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket.state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( buffer == ":1\r\n" ); +} // Client::sismember + + +/*! + * \brief Возвращает количество элементов в наборе + * \param key Ключ набора + * \return КОличество элементов + */ +int Client::scard( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return( -1 ); + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return( -1 ); + } + socket->write( array( { "SCARD", key } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( -1 ); + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + } + else + { + QCoreApplication::processEvents(); + } + } + if ( QRegExp( ":\\d+\\r\\n" ).exactMatch( buffer ) ) + { + QRegExp rx { "\\d+" }; + rx.indexIn( buffer ); + return( rx.cap().toInt() ); + } + return( -1 ); +} // Client::scard + + +/*! + * \brief Возвращает элементы сета + * \param key Ключ сета + * \return Строковые значения сета + */ +QVariantList Client::smembers( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "SMEMBERS", key } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + QVariantList list; + std::copy( parts.begin(), parts.end(), std::back_inserter( list ) ); + return( list ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::smembers + + +/*! + * \brief Удаляет и возвращает последний элемент списка + * \param key Ключ списка + * \return Последний элемент + */ +QByteArray Client::rpop( const QString& key ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "RPOP", key } ) ); + QByteArray buffer; + while ( !buffer.endsWith( C::Separator ) ) + { + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return {}; + } + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + return( parts.size() == 1 ? parts.front() : "" ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( "" ); +} // Client::rpop + + +/*! + * \brief Возвращает элемент по его позиции + * \param key Ключ списка + * \param index Позиция элемента + * \return Текстовое представление элемента + */ +QByteArray Client::lindex( const QString& key, int index ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "LINDEX", key, index } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer == "$-1" + C::Separator ) + { + return {}; + } + buffer.append( socket->readAll() ); + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + if ( parts.size() == 1 ) + { + return( parts.front() ); + } + return {}; + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::lindex + + +/*! + * \brief Реализует метод redis KEYS + * \param pattern Шпблон для поиска ключей + * \return Список ключей, соответствующих шиблону + */ +QStringList Client::keys( const QString& pattern ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( pattern.isEmpty() ? QVariantList { "KEYS" } : + QVariantList { "KEYS", pattern } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) || buffer.startsWith( "-WRONG" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + QStringList list; + std::copy( parts.begin(), parts.end(), std::back_inserter( list ) ); + return( list ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::keys + + +/*! + * \brief Проверяет является ли значение частью набора без создания постоянного подключения + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param pattern Шаблон ключей + * \param host Хост для установки соединения с сервером redis + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Признак того, что значение входит в набор + */ +QStringList Client::keys( const QString& pattern, int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( pattern.isEmpty() ? QVariantList { "KEYS" } : + QVariantList { "KEYS", pattern } ) ); + QByteArray buffer; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) || buffer.startsWith( "-WRONG" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + QStringList list; + std::copy( parts.begin(), parts.end(), std::back_inserter( list ) ); + return( list ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::keys + + +/*! + * \brief Возвращает элементы хранимого списка + * \param key Ключ списка + * \param from Начальный индекс + * \param to Конечный индекс + * \return Строковые значения списка + */ +QVariantList Client::lrange( const QString& key, int from, int to ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( { "LRANGE", key, from, to } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) || buffer.startsWith( "-WRONG" ) ) + { + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + QVariantList list; + std::copy( parts.begin(), parts.end(), std::back_inserter( list ) ); + return( list ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::lrange + + +/*! + * \brief Извлекает из базы вектор и преобразует его к типу вектор json + * + * Специализация + * \param key Ключ для измлечения данных + * \param database Идентификатор БД + * \param port Номер порта redis + * \param from Начало интервала + * \param to Конец интервала + * \param Хост redis + * \return Вектор значений + */ +template<> +std::vector< QJsonObject > Client::lrange< QJsonObject >( const QString& key, int from, int to, + int database, + const QString& host, quint16 port ) +{ + auto list = lrange( key, from, to, database, host, port ); + std::vector< QJsonObject > data; +// TODO +// std::transform( list.begin(), list.end(), std::back_inserter( data ), +// []( const auto& iter ) { return( QJsonDocument::fromJson( iter ).object() ); } ); + return( data ); +} + + +/*! + * \brief Возвращает элементы хранимого списка + * + * Метод отличается более низкой производительностью по сравнению с нестатической реализацией + * \param key Ключ списка + * \param from Начальный индекс + * \param to Конечный индекс + * \param port Порт для установки соединения с сервером redis + * \param database Идентификатор БД + * \return Строковые значения списка + */ +QByteArrayList Client::lrange( const QString& key, int from, int to, int database, + const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( { "LRANGE", key, from, to } ) ); + QByteArray buffer; + QByteArrayList data; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( ( buffer == "$-1\r\n" ) || ( buffer == "*0\r\n" ) || buffer.startsWith( "-WRONG" ) ) + { + break; + } + int splitPos; + data = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + break; + } + else + { + QCoreApplication::processEvents(); + } + } + if ( socket.state() == QAbstractSocket::ConnectedState ) + { + socket.disconnectFromHost(); + } + while ( socket.state() != QAbstractSocket::UnconnectedState ) QCoreApplication::processEvents(); + return( data ); +} // Client::lrange + + +/*! + * \brief Переключает сокет на работу с определенной БД + * \param socket Сокет + * \param database Номер БД + * \return Признак успешной операции + */ +bool Client::select( QTcpSocket* socket, int database ) +{ + socket->write( array( { "SELECT", database } ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + return( buffer == "+OK\r\n" ); + } + else + { + QCoreApplication::processEvents(); + } + } + return( false ); +} // Client::select + + +/*! + * \brief Настраивает сокет для подключения к БД + * \param socket Сокет TCP + * \param database База днных + * \param host Адрес хоста redis + * \param port Номер порта redis + * \return Признак удачной настройки + */ +bool Client::setupSocket( QTcpSocket* socket, int database, const QString& host, quint16 port ) +{ + static constexpr int timeout { 3000 }; + QElapsedTimer timer; + timer.start(); + do { + if ( socket->state() == QAbstractSocket::UnconnectedState ) + { + socket->connectToHost( host, port ); + } + QCoreApplication::processEvents(); + } while( socket->state() != QAbstractSocket::ConnectedState && timer.elapsed() < timeout ); + if ( socket->state() != QAbstractSocket::ConnectedState ) + { + return( false ); + } + if ( ( database != 0 ) && !select( socket, database ) ) + { + return( false ); + } + return( true ); +} // Client::setupSocket + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +QByteArrayList Client::evalSha( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ) +{ + if ( QThread::currentThread() != thread() ) + { + qDebug() << "no redis connection for caller thread"; + return {}; + } + Session instance { _sessionCount }; + auto socket = session( rwTimeout() ); + if ( socket == nullptr ) + { + return {}; + } + socket->write( array( QVariantList { "EVALSHA", sha, keys.size() } + keys + argv ) ); + QByteArray buffer; + while ( socket->state() == QAbstractSocket::ConnectedState ) + { + if ( socket->bytesAvailable() > 0 ) + { + buffer.append( socket->readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer.startsWith( "-ERR" ) || buffer.startsWith( "-NOSCRIPT" ) ) + { + qWarning() << "evaluating redis script error:" << buffer; + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + return( parts ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::evalSha + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Статический вариант + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \param database Идентификатор БД + * \param host Адрес сервера redis + * \param port Номер порта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +QByteArrayList Client::evalSha( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv, + int database, const QString& host, quint16 port ) +{ + QTcpSocket socket; + if ( !setupSocket( &socket, database, host, port ) ) + { + return {}; + } + socket.write( array( QVariantList { "EVALSHA", sha, keys.size() } + keys + argv ) ); + QByteArray buffer; + while ( socket.state() == QAbstractSocket::ConnectedState ) + { + if ( socket.bytesAvailable() > 0 ) + { + buffer.append( socket.readAll() ); + if ( !buffer.endsWith( C::Separator ) ) + { + continue; + } + if ( buffer.startsWith( "-ERR" ) || buffer.startsWith( "-NOSCRIPT" ) ) + { + qWarning() << "evaluating redis script error:" << buffer; + return {}; + } + int splitPos; + auto parts = split( buffer, &splitPos ); + if ( splitPos < buffer.length() ) + { + continue; + } + return( parts ); + } + else + { + QCoreApplication::processEvents(); + } + } + return {}; +} // Client::evalSha + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Специализация для типа QJsonArray + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +template<> +QJsonArray Client::evalSha< QJsonArray >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ) +{ + auto parts = evalSha( sha, keys, argv ); + return( parts.empty() ? QJsonArray {} : QJsonDocument::fromJson( parts.front() ).array() ); +} + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Специализация для типа QJsonArray, static вариант + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \param database Идентификатор БД + * \param host Адрес сервера + * \param port Номер порта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +template<> +QJsonArray Client::evalSha< QJsonArray >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv, + int database, const QString& host, quint16 port ) +{ + auto parts = evalSha( sha, keys, argv, database, host, port ); + return( parts.empty() ? QJsonArray {} : QJsonDocument::fromJson( parts.front() ).array() ); +} + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Специализация для типа QVariant + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +template<> +QVariant Client::evalSha< QVariant >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ) +{ + auto parts = evalSha( sha, keys, argv ); + return( parts.empty() ? QVariant {} : parts.front() ); +} + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Специализация для типа QVariantList + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +template<> +QVariantList Client::evalSha< QVariantList >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ) +{ + auto parts = evalSha( sha, keys, argv ); + QVariantList data; + std::copy( parts.begin(), parts.end(), std::back_inserter( data ) ); + return( data ); +} + + +/*! + * \brief Запускает на выполнение сохраненный на стороне сервера lua скрипт + * + * Специализация для типа QVariantHash + * \param sha SHA сохраненного скрипта + * \param keys Ключи для выполнения скрипта + * \param argv Аргументы для выполнения скрипта + * \return Части результата выполнения скрипта (возвращенного значения) + */ +template<> +QVariantHash Client::evalSha< QVariantHash >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ) +{ + auto parts = evalSha( sha, keys, argv ); + QVariantHash data; + for ( int i = 1; i < parts.size(); i += 2 ) data[parts[i - 1]] = parts[i]; + return( data ); +} + + +/*! + * \brief Создает сокет для неоткрытой сессии + * \param Таймаут на установку соединения в мс + * \return Созданный сокет + */ +QTcpSocket* Client::session( int timeout ) +{ + if ( _sessionCount == 0 ) + { + return( nullptr ); + } + while ( _sessionCount > _session.size() ) + { + _session.push_back( new QTcpSocket { this } ); + connect( _session.back(), &QIODevice::readyRead, this, &Client::onReadyRead ); + connect( _session.back(), &QIODevice::bytesWritten, this, &Client::onBytesWritten ); + } + auto socket = _session[_sessionCount - 1]; + QElapsedTimer timer; + timer.start(); + while ( socket->state() != QAbstractSocket::ConnectedState ) + { + if ( connectionFlags() != kConnecting ) + { + setConnectionFlags( kConnecting ); + emit connectionStateChanged( connectionFlags() ); + } + if ( timer.elapsed() > timeout ) + { + setConnectionFlags( kDisconnected ); + emit connectionStateChanged( connectionFlags() ); + return( nullptr ); + } + if ( socket->state() == QAbstractSocket::UnconnectedState ) + { + socket->connectToHost( host(), port() ); + } + QCoreApplication::processEvents(); + if ( ( _database != 0 ) && !select( socket, _database ) ) + { + setConnectionFlags( kDisconnected ); + emit connectionStateChanged( connectionFlags() ); + return( nullptr ); + } + } + auto iter = _outOfDatabase.find( socket ); + if ( iter != _outOfDatabase.end() ) + { + if ( select( socket, _database ) ) + { + _outOfDatabase.erase( iter ); + } + else + { + setConnectionFlags( kDisconnected ); + emit connectionStateChanged( connectionFlags() ); + return( nullptr ); + } + } + if ( !( isConnected() ) ) + { + setConnectionFlags( kConnected ); + emit connectionStateChanged( connectionFlags() ); + } + return( socket ); +} // Client::session + + +/*! + * \brief Конструктор класса + * \param count Счетчик открытых сессий + */ +Client::Session::Session( uint& count ) : + _count{ count } +{ + _count++; +} + + +/*! + * \brief Деструктор класса + */ +Client::Session::~Session() +{ + _count--; +} + +} // namespace redis + +} // namespace myx + +#endif // ifndef MYX_REDIS_CLIENT_INL_HPP_ diff --git a/src/myx/redis.old/client.cpp b/src/myx/redis/client.cpp similarity index 100% rename from src/myx/redis.old/client.cpp rename to src/myx/redis/client.cpp diff --git a/src/myx/redis/client.hpp b/src/myx/redis/client.hpp new file mode 100644 index 0000000..38399eb --- /dev/null +++ b/src/myx/redis/client.hpp @@ -0,0 +1,157 @@ +#ifndef MYX_REDIS_CLIENT_HPP_ +#define MYX_REDIS_CLIENT_HPP_ + +#pragma once + +#include + +#include +#include +#include +#include + +// QT_FORWARD_DECLARE_CLASS( QTcpSocket ) + +namespace myx { + +namespace redis { + +/*! + * \brief Реализует клиентскую часть в соответствии с RESP + */ +class Client : public Base +{ + Q_OBJECT +public: + Client( int database = 0, QObject* parent = nullptr ); + virtual ~Client(); + int database() const; + int exists( const QStringList& keys ); + void select( int database ); + bool flushdb(); + bool del( const QStringList& keys ); + bool expire( const QString& key, int secs ); + bool pexpire( const QString& key, int msecs ); + bool persist( const QString& key ); + bool publish( const QString& channel, const QByteArray& message ); + static bool publish( const QString& channel, const QByteArray& message, + const QString& host, quint16 port = 6379 ); + bool set( const QString& key, const QVariant& value ); + static bool set( const QString& key, const QVariant& value, + int database, const QString& host = "127.0.0.1", quint16 port = 6379 ); + QByteArray scriptLoad( const QByteArray& script ); + QByteArray get( const QString& key ); + template< typename T > + T get( const QString& key ); + static QByteArray get( const QString& key, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + template< typename T > + static T get( const QString& key, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + bool hdel( const QString& redisKey, const QStringList& hashKeys ); + bool hset( const QString& key, const QString& field, const QVariant& value ); + bool hmset( const QString& key, const QVariantHash& hash ); + static bool hmset( const QString& key, const QVariantHash& hash, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + QVariantHash hgetall( const QString& key ); + static QVariantHash hgetall( const QString& key, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + QByteArray hget( const QString& redisKey, const QString& hashKey ); + static QByteArray hget( const QString& redisKey, const QString& hashKey, + int database, const QString& host = "127.0.0.1", quint16 port = 6379 ); + bool rpush( const QString& key, const QVariantList& values ); + bool lpush( const QString& key, const QVariantList& values ); + bool lset( const QString& key, int index, const QVariant& value ); + bool lrem( const QString& key, const QVariant& value, int count = 1 ); + bool ltrim( const QString& key, int from, int to ); + int llen( const QString& key ); + QVariantList lrange( const QString& key, int from, int to ); + template< typename T > + std::vector< T > lrange( const QString& key ); + static QByteArrayList lrange( const QString& key, int from, int to, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + template< typename T > + static std::vector< T > lrange( const QString& key, int from, int to, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + bool sadd( const QString& key, const QVariantList& values ); + bool srem( const QString& key, const QVariantList& values ); + bool sismember( const QString& key, const QVariant& value ); + static bool sismember( const QString& key, const QVariant& value, int database, + const QString& host = "127.0.0.1", quint16 port = 6379 ); + int scard( const QString& key ); + QVariantList smembers( const QString& key ); + template< typename T > + std::vector< T > smembers( const QString& key ); + QByteArray rpop( const QString& key ); + QByteArray lindex( const QString& key, int index ); + QStringList keys( const QString& pattern = {} ); + static QStringList keys( const QString& pattern, int database, + const QString& host, quint16 port = 6379 ); + template< typename T > + T evalSha( const QByteArray& sha, const QVariantList& keys, const QVariantList& argv ); + template< typename T > + static T evalSha( const QByteArray& sha, const QVariantList& keys, const QVariantList& argv, + int database, const QString& host, quint16 port = 6379 ); + QByteArrayList evalSha( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ); + static QByteArrayList evalSha( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv, + int database, const QString& host, quint16 port = 6379 ); + +private: + /*! + * \brief Класс для синхронизации порядка чтения/записи в poolSocket; + */ + struct Session + { + Session( uint& count ); + ~Session(); + +private: + uint& _count; //!< Счетчик открытых сессий + }; + + int _database; //!< Номер БД в redis + uint _sessionCount { 0 }; //!< Счетчик открытых сессий + std::vector< QTcpSocket* > _session; //!< Сокеты для синхронных сессий чтения/записи + /*! + * \brief Содержит TCP соединения, связанные с БД, отличной от текущей + */ + std::unordered_set< QTcpSocket* > _outOfDatabase; + + static bool setupSocket( QTcpSocket* socket, int database, const QString& host, quint16 port ); + static bool select( QTcpSocket* socket, int database ); + QTcpSocket* session( int timeout ); +}; // class Client + +template<> +QVariant Client::evalSha< QVariant >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ); +template<> +QVariantList Client::evalSha< QVariantList >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ); +template<> +QVariantHash Client::evalSha< QVariantHash >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ); +template<> +QJsonArray Client::evalSha< QJsonArray >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv ); +template<> +QJsonArray Client::evalSha< QJsonArray >( const QByteArray& sha, const QVariantList& keys, + const QVariantList& argv, + int database, const QString& host, quint16 port ); +template<> +std::vector< QJsonObject > Client::lrange< QJsonObject >( const QString & key, int from, int to, + int database, const QString & host, quint16 ); + +} // namespace redis + +} // namespace myx + +#ifdef MYXLIB_HEADER_ONLY +#include "client-inl.hpp" +#endif + +// #include "client_redis.tcc" + +#endif // ifndef MYX_REDIS_CLIENT_HPP_ diff --git a/src/myx/redis/pubsub-inl.hpp b/src/myx/redis/pubsub-inl.hpp index f2706c8..5692045 100644 --- a/src/myx/redis/pubsub-inl.hpp +++ b/src/myx/redis/pubsub-inl.hpp @@ -30,14 +30,15 @@ MYXLIB_INLINE PubSub::PubSub( QObject* parent ) : { connect( m_socket, &QAbstractSocket::stateChanged, this, &PubSub::onSocketStateChanged ); + connect( m_socket, &QIODevice::readyRead, this, &PubSub::read ); connect( m_socket, &QIODevice::readyRead, this, &PubSub::onReadyRead ); connect( m_socket, &QIODevice::bytesWritten, this, &PubSub::onBytesWritten ); + connect( m_socket, static_cast< void ( QAbstractSocket::* )( QAbstractSocket::SocketError ) > ( &QAbstractSocket::error ), this, [this]() { qDebug() << QDateTime::currentDateTimeUtc().toString( QStringLiteral( "hh:mm:ss:zzz" ) ) << QByteArray( "redis pubsub connection error" ) << m_socket->errorString(); } ); - connect( m_socket, &QIODevice::readyRead, this, &PubSub::read ); m_connectionTimer->setInterval( connectionTimeout() ); m_connectionTimer->setSingleShot( true ); @@ -81,7 +82,7 @@ MYXLIB_INLINE void PubSub::stop() /*! - * \brief Создаёт подписку на канал сообщений + * \brief Создает подписку на канал сообщений * \param channel Канал подписки * \param subscriber Объект-подписчик * \param method Имя метода, вызываемого при изменении получении серверного сообщения @@ -120,7 +121,7 @@ MYXLIB_INLINE void PubSub::subscribe( const QString& channel, QObject* subscribe /*! - * \brief Создаёт подписку на канал изменения данных + * \brief Создает подписку на канал изменения данных * \param channel Канал подписки * \param subscriber Объект-подписчик * \param method Имя метода, вызываемого при изменении получении серверного сообщения @@ -161,7 +162,7 @@ MYXLIB_INLINE void PubSub::subscribe( QString channel, QObject* subscriber, cons /*! - * \brief Создаёт подписку на шаблон канала сообщений + * \brief Создает подписку на шаблон канала сообщений * \param channel Шаблон канала подписки * \param subscriber Объект-подписчик * \param method Имя метода, вызываемого при изменении получении серверного сообщения @@ -200,7 +201,7 @@ MYXLIB_INLINE void PubSub::psubscribe( const QString& channel, QObject* subscrib /*! - * \brief Создаёт подписку на шаблон канала изменения данных + * \brief Создает подписку на шаблон канала изменения данных * \param channel Шаблон канала подписки * \param subscriber Объект-подписчик * \param method Имя метода, вызываемого при изменении получении серверного сообщения @@ -503,7 +504,7 @@ MYXLIB_INLINE void PubSub::onSocketStateChanged() /*! * \brief При удалении подписчика уничтожает все его подписки - * \param subscriber Удалённый подписчик + * \param subscriber Удаленный подписчик */ MYXLIB_INLINE void PubSub::onSubscriberDestroyed( QObject* subscriber ) {