Initial commit.
This commit is contained in:
commit
b3daaceb3a
12
.editorconfig
Normal file
12
.editorconfig
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build/
|
||||||
|
.DS_Store
|
127
CMakeLists.txt
Normal file
127
CMakeLists.txt
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
|
||||||
|
project(resply VERSION 0.1.0 DESCRIPTION "Modern redis client for C++")
|
||||||
|
set(CMAKE_CXX_STANDARD 14)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
if (POLICY CMP0025)
|
||||||
|
cmake_policy(SET CMP0025 NEW)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
option(BUILD_DOC "Build documentation using doxygen" ON)
|
||||||
|
|
||||||
|
add_definitions(-DASIO_STANDALONE)
|
||||||
|
|
||||||
|
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR
|
||||||
|
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" OR
|
||||||
|
"${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic-errors -Werror -Wall -Wextra")
|
||||||
|
# Exceptions are not used, asio is used with explicit error checking.
|
||||||
|
# Apparently, protobuf uses __builtin_offsetof in a way which clang declares as
|
||||||
|
# a extension to the language - gcc not. Disable that warning to get it to compile.
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -Wno-extended-offsetof")
|
||||||
|
|
||||||
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g")
|
||||||
|
else () # currently not maintained any more:
|
||||||
|
add_definitions(-DASIO_HAS_STD_ADDRESSOF)
|
||||||
|
add_definitions(-DASIO_HAS_STD_ARRAY)
|
||||||
|
add_definitions(-DASIO_HAS_CSTDINT)
|
||||||
|
add_definitions(-DASIO_HAS_STD_SHARED_PTR)
|
||||||
|
add_definitions(-DASIO_HAS_STD_TYPE_TRAITS)
|
||||||
|
add_definitions(-DASIO_HAS_STD_ATOMIC)
|
||||||
|
add_definitions(-D_WIN32_WINNT=0x0501)
|
||||||
|
add_definitions(/Wall /EHsc)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
include_directories(include)
|
||||||
|
|
||||||
|
# https://github.com/chriskohlhoff/asio
|
||||||
|
include_directories(ASIO_INCLUDE_PATH)
|
||||||
|
|
||||||
|
# https://github.com/nlohmann/json
|
||||||
|
# should refer to the directory 'src' of json
|
||||||
|
include_directories(JSON_INCLUDE_PATH)
|
||||||
|
|
||||||
|
# https://github.com/muellan/clipp
|
||||||
|
include_directories(CLIPP_INCLUDE_PATH)
|
||||||
|
|
||||||
|
# https://github.com/gabime/spdlog
|
||||||
|
include_directories(SPDLOG_INCLUDE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
# protobuf
|
||||||
|
find_package(Protobuf REQUIRED)
|
||||||
|
include_directories(${Protobuf_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
file(GLOB protos protos/*.proto)
|
||||||
|
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${protos})
|
||||||
|
|
||||||
|
# libresply
|
||||||
|
file(GLOB libresply_source src/libresply.cc)
|
||||||
|
add_library(libresply OBJECT ${libresply_source})
|
||||||
|
target_compile_definitions(libresply PRIVATE RESPLY_VERSION="${PROJECT_VERSION}")
|
||||||
|
|
||||||
|
add_library(resply-shared SHARED $<TARGET_OBJECTS:libresply>)
|
||||||
|
add_library(resply-static STATIC $<TARGET_OBJECTS:libresply>)
|
||||||
|
|
||||||
|
set_target_properties(resply-static PROPERTIES OUTPUT_NAME resply)
|
||||||
|
set_target_properties(resply-shared PROPERTIES OUTPUT_NAME resply)
|
||||||
|
|
||||||
|
install(TARGETS resply-shared DESTINATION lib)
|
||||||
|
install(TARGETS resply-static DESTINATION lib)
|
||||||
|
install(FILES include/resply.h DESTINATION include)
|
||||||
|
|
||||||
|
|
||||||
|
# cli
|
||||||
|
add_executable(resply-cli src/resply-cli.cc)
|
||||||
|
target_link_libraries(resply-cli resply-static)
|
||||||
|
|
||||||
|
add_executable(proto-cli src/proto-cli.cc ${PROTO_SRCS})
|
||||||
|
target_link_libraries(proto-cli ${PROTOBUF_LIBRARIES})
|
||||||
|
|
||||||
|
add_executable(grpc-cli src/grpc-cli.cc)
|
||||||
|
|
||||||
|
|
||||||
|
# proxy
|
||||||
|
add_executable(proxy src/proxy.cc ${PROTO_SRCS})
|
||||||
|
target_link_libraries(proxy resply-static ${PROTOBUF_LIBRARIES})
|
||||||
|
|
||||||
|
|
||||||
|
# tests
|
||||||
|
file(GLOB tests_source tests/*.cc)
|
||||||
|
foreach (test_source ${tests_source})
|
||||||
|
get_filename_component(name ${test_source} NAME_WE)
|
||||||
|
set(tests "${tests} ${name}")
|
||||||
|
|
||||||
|
add_executable(${name} ${test_source})
|
||||||
|
target_link_libraries(${name} resply-static)
|
||||||
|
set_target_properties(${name} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests)
|
||||||
|
endforeach ()
|
||||||
|
|
||||||
|
string(STRIP ${tests} tests)
|
||||||
|
|
||||||
|
add_custom_target(
|
||||||
|
tests
|
||||||
|
COMMAND for t in \"${tests}\"\; do ./tests/$$t\; done
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
DEPENDS ${tests}
|
||||||
|
COMMENT "Running tests")
|
||||||
|
|
||||||
|
|
||||||
|
# documentation
|
||||||
|
find_package(Doxygen)
|
||||||
|
if (BUILD_DOC AND DOXYGEN_FOUND)
|
||||||
|
set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in)
|
||||||
|
set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile)
|
||||||
|
|
||||||
|
configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY)
|
||||||
|
|
||||||
|
add_custom_target(
|
||||||
|
doc
|
||||||
|
COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
COMMENT "Generating API documentation with Doxygen"
|
||||||
|
VERBATIM)
|
||||||
|
else ()
|
||||||
|
message("Doxygen need to be installed to generate the doxygen documentation")
|
||||||
|
endif ()
|
2482
Doxyfile.in
Normal file
2482
Doxyfile.in
Normal file
File diff suppressed because it is too large
Load diff
26
LICENSE
Normal file
26
LICENSE
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Boost Software License - Version 1.0 - August 17th, 2003
|
||||||
|
|
||||||
|
Copyright (c) 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person or organization
|
||||||
|
obtaining a copy of the software and accompanying documentation covered by
|
||||||
|
this license (the "Software") to use, reproduce, display, distribute,
|
||||||
|
execute, and transmit the Software, and to prepare derivative works of the
|
||||||
|
Software, and to permit third-parties to whom the Software is furnished to
|
||||||
|
do so, all subject to the following:
|
||||||
|
|
||||||
|
The copyright notices in the Software and this entire statement, including
|
||||||
|
the above license grant, this restriction and the following disclaimer,
|
||||||
|
must be included in all copies of the Software, in whole or in part, and
|
||||||
|
all derivative works of the Software, unless such copies or derivative
|
||||||
|
works are solely in the form of machine-executable object code generated by
|
||||||
|
a source language processor.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||||
|
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
|
6
README.md
Normal file
6
README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# resply - a modern C++ redis client :honeybee:
|
||||||
|
|
||||||
|
|
||||||
|
# License
|
||||||
|
resply is licensed under the terms of the
|
||||||
|
[Boost Software License, Version 1.0](http://www.boost.org/LICENSE_1_0.txt)
|
51
include/cli-common.h
Normal file
51
include/cli-common.h
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
#include <clipp.h>
|
||||||
|
|
||||||
|
#include "resply.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace common {
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
Options() : host{"localhost"}, port{"6379"}, show_version{} { }
|
||||||
|
|
||||||
|
std::string host;
|
||||||
|
std::string port;
|
||||||
|
bool show_version;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Options parse_commandline(int argc, char** argv)
|
||||||
|
{
|
||||||
|
Options options;
|
||||||
|
bool show_help{};
|
||||||
|
|
||||||
|
auto cli = (
|
||||||
|
clipp::option("-h", "--host").set(options.host)
|
||||||
|
.doc("Set the host to connect to [default: localhost]"),
|
||||||
|
|
||||||
|
clipp::option("-p", "--port").set(options.port)
|
||||||
|
.doc("Set the port to connect to [default: 6379]"),
|
||||||
|
|
||||||
|
clipp::option("--help").set(show_help).doc("Show help and exit."),
|
||||||
|
clipp::option("--version").set(options.show_version).doc("Show version and exit.")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!clipp::parse(argc, argv, cli) || show_help) {
|
||||||
|
std::cout << clipp::make_man_page(cli, argv[0]) << std::endl;
|
||||||
|
std::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
148
include/resply.h
Normal file
148
include/resply.h
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <sstream>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
|
namespace resply {
|
||||||
|
/*! \return The version of the resply library. */
|
||||||
|
const std::string& version();
|
||||||
|
|
||||||
|
|
||||||
|
/*! \brief Holds the response of a redis command. */
|
||||||
|
struct Result {
|
||||||
|
Result() : type{Type::Nil} { }
|
||||||
|
|
||||||
|
/*! \brief Indicates the type of the response. */
|
||||||
|
enum class Type {
|
||||||
|
String,
|
||||||
|
Integer,
|
||||||
|
Array,
|
||||||
|
ProtError,
|
||||||
|
IOError,
|
||||||
|
Nil
|
||||||
|
};
|
||||||
|
|
||||||
|
/*! \brief Holds the type of the response. */
|
||||||
|
Type type;
|
||||||
|
|
||||||
|
/*! \brief Use when type is String, ProtError or IOError */
|
||||||
|
std::string string;
|
||||||
|
|
||||||
|
/*! \brief Use when type is Integer */
|
||||||
|
long long integer;
|
||||||
|
|
||||||
|
/*! \brief Use when type is Array */
|
||||||
|
std::vector<Result> array;
|
||||||
|
|
||||||
|
/*! \brief This outputs the stringify'd version of the response into the supplied stream.
|
||||||
|
*
|
||||||
|
* It acts according to the #type field.
|
||||||
|
* If #type is either Type::ProtError or Type::IOError, "(error) " is
|
||||||
|
* prepended to the error message. If #type is Type::Nil, the output is "(nil)".
|
||||||
|
*/
|
||||||
|
friend std::ostream& operator<<(std::ostream& ostream, const Result& result);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class ClientImpl;
|
||||||
|
|
||||||
|
/*! \brief Redis client interface
|
||||||
|
*
|
||||||
|
* This class implements the protocol RESP to communicate with a redis server.
|
||||||
|
*/
|
||||||
|
class Client {
|
||||||
|
public:
|
||||||
|
/*! \brief Constructs a new redis client which connects to localhost:6379. */
|
||||||
|
Client();
|
||||||
|
|
||||||
|
/*! \brief Constructs a new redis client.
|
||||||
|
* \param address Redis server address in the format "<host>[:<port>]".
|
||||||
|
* The ":port" component may be omitted, in which case it defaults to "6379".
|
||||||
|
* \param timeout Timeout in milliseconds when connecting to server. Default are 500ms.
|
||||||
|
*/
|
||||||
|
Client(const std::string& address, size_t timeout=500);
|
||||||
|
|
||||||
|
/*! \brief Constructs a new redis client.
|
||||||
|
* \param host Redis server address.
|
||||||
|
* \param port Redis server port.
|
||||||
|
* \param timeout Timeout in milliseconds when connecting to server. Default are 500ms.
|
||||||
|
*/
|
||||||
|
Client(const std::string& host, const std::string& port, size_t timeout=500);
|
||||||
|
|
||||||
|
/*! \brief Closes the connection to the redis server. */
|
||||||
|
~Client();
|
||||||
|
|
||||||
|
/*! \brief Establishes a connection to the server. */
|
||||||
|
void connect();
|
||||||
|
|
||||||
|
/*! \brief Closes the connection to the server.
|
||||||
|
*
|
||||||
|
* Optional, is also called in the destructor.
|
||||||
|
*/
|
||||||
|
void close();
|
||||||
|
|
||||||
|
/*! \brief Send a command to the server.
|
||||||
|
* \param command List of command name and its parameters.
|
||||||
|
*
|
||||||
|
* The command and parameters are automatically serialized into the RESP protocol
|
||||||
|
* as specificed at <https://redis.io/topics/protocol>.
|
||||||
|
*/
|
||||||
|
Result command(const std::vector<std::string>& str);
|
||||||
|
|
||||||
|
/*! \brief Send a command to the server.
|
||||||
|
* \param str The name of the command.
|
||||||
|
* \param args A series of command arguments.
|
||||||
|
*
|
||||||
|
* The command and parameters are automatically serialized into the RESP protocol
|
||||||
|
* as specificed at <https://redis.io/topics/protocol>.
|
||||||
|
*/
|
||||||
|
template <typename... ArgTypes>
|
||||||
|
Result command(const std::string& str, ArgTypes... args)
|
||||||
|
{
|
||||||
|
if (str.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream builder;
|
||||||
|
|
||||||
|
builder << '*' << sizeof...(ArgTypes) + 1 << "\r\n";
|
||||||
|
return command(builder, str, args...);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <typename... ArgTypes>
|
||||||
|
Result command(std::stringstream& builder, const std::string& str, ArgTypes... args)
|
||||||
|
{
|
||||||
|
builder << '$' << str.length() << "\r\n" << str << "\r\n";
|
||||||
|
return command(builder, args...);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
template <typename T,
|
||||||
|
typename = typename std::enable_if<std::is_integral<T>::value, T>::type,
|
||||||
|
typename... ArgTypes>
|
||||||
|
Result command(std::stringstream& builder, T num, ArgTypes... args)
|
||||||
|
{
|
||||||
|
builder << ':' << num << "\r\n";
|
||||||
|
return command(builder, args...);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result command(const std::stringstream& builder);
|
||||||
|
|
||||||
|
std::unique_ptr<ClientImpl> impl_;
|
||||||
|
};
|
||||||
|
}
|
35
protos/resp.proto
Normal file
35
protos/resp.proto
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package rslp;
|
||||||
|
|
||||||
|
|
||||||
|
message Command {
|
||||||
|
enum Type {
|
||||||
|
SIMPLE_STRING = 0;
|
||||||
|
ERROR = 1;
|
||||||
|
INTEGER = 2;
|
||||||
|
BULK_STRING = 3;
|
||||||
|
ARRAY = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Data {
|
||||||
|
oneof data {
|
||||||
|
string str = 1;
|
||||||
|
sint64 int = 2;
|
||||||
|
Command subdata = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Type type = 1;
|
||||||
|
repeated Data data = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
17
src/grpc-cli.cc
Normal file
17
src/grpc-cli.cc
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "cli-common.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
auto options{common::parse_commandline(argc, argv)};
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
299
src/libresply.cc
Normal file
299
src/libresply.cc
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <sstream>
|
||||||
|
#include <cctype>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include <asio.hpp>
|
||||||
|
|
||||||
|
#include "resply.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool is_number(const std::string& str) {
|
||||||
|
return !str.empty() && std::all_of(str.begin(), str.end(), ::isdigit);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace resply {
|
||||||
|
|
||||||
|
const std::string& version()
|
||||||
|
{
|
||||||
|
const static std::string version{RESPLY_VERSION};
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& ostream, const Result& result)
|
||||||
|
{
|
||||||
|
switch (result.type) {
|
||||||
|
case Result::Type::ProtError:
|
||||||
|
case Result::Type::IOError:
|
||||||
|
ostream << "(error) ";
|
||||||
|
/* fallthrough */
|
||||||
|
|
||||||
|
case Result::Type::String:
|
||||||
|
ostream << result.string;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Result::Type::Integer:
|
||||||
|
ostream << result.integer;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Result::Type::Nil:
|
||||||
|
ostream << "(nil)";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Result::Type::Array:
|
||||||
|
for (const auto& res: result.array) {
|
||||||
|
ostream << res;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ostream;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ClientImpl {
|
||||||
|
public:
|
||||||
|
ClientImpl(const std::string& host, const std::string& port, size_t timeout)
|
||||||
|
: host_{host}, port_{port}, timeout_{timeout}, socket_{io_context_}
|
||||||
|
{
|
||||||
|
(void)timeout_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
~ClientImpl()
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void connect()
|
||||||
|
{
|
||||||
|
asio::error_code error_code;
|
||||||
|
|
||||||
|
asio::ip::tcp::resolver resolver{io_context_};
|
||||||
|
auto results = resolver.resolve(host_, port_, error_code);
|
||||||
|
check_asio_error(error_code);
|
||||||
|
|
||||||
|
asio::connect(socket_, results, error_code);
|
||||||
|
check_asio_error(error_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void close()
|
||||||
|
{
|
||||||
|
socket_.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Result command(const std::string& command)
|
||||||
|
{
|
||||||
|
asio::error_code error_code;
|
||||||
|
|
||||||
|
asio::write(socket_, asio::buffer(command), error_code);
|
||||||
|
check_asio_error(error_code);
|
||||||
|
|
||||||
|
Result result;
|
||||||
|
asio::streambuf buffer;
|
||||||
|
size_t remaining{};
|
||||||
|
|
||||||
|
do {
|
||||||
|
asio::read_until(socket_, buffer, "\r\n" , error_code);
|
||||||
|
check_asio_error(error_code);
|
||||||
|
|
||||||
|
parse_response(result, buffer, remaining);
|
||||||
|
} while (remaining);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool check_asio_error(asio::error_code& error_code)
|
||||||
|
{
|
||||||
|
if (error_code) {
|
||||||
|
std::cerr << error_code.message() << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!error_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_response(Result& result, asio::streambuf& streambuf, size_t& remaining)
|
||||||
|
{
|
||||||
|
std::string buffer{asio::buffers_begin(streambuf.data()),
|
||||||
|
asio::buffers_end(streambuf.data())};
|
||||||
|
|
||||||
|
if (!remaining) {
|
||||||
|
// First pass
|
||||||
|
parse_type(result, buffer, remaining);
|
||||||
|
} else {
|
||||||
|
// All other
|
||||||
|
continue_parse_type(result, buffer, remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
streambuf.consume(streambuf.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_type(Result& result, const std::string& buffer, size_t& remaining)
|
||||||
|
{
|
||||||
|
switch (buffer.front()) {
|
||||||
|
case '+':
|
||||||
|
result.type = Result::Type::String;
|
||||||
|
// Exclude the final \r\n bytes
|
||||||
|
result.string += buffer.substr(1, buffer.length() - 3);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case '-':
|
||||||
|
result.type = Result::Type::ProtError;
|
||||||
|
// Exclude the final \r\n bytes
|
||||||
|
result.string += buffer.substr(1, buffer.length() - 3);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ':':
|
||||||
|
result.type = Result::Type::Integer;
|
||||||
|
result.integer = std::stoll(buffer.substr(1));
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$': {
|
||||||
|
size_t size;
|
||||||
|
long length{std::stol(buffer.substr(1), &size)};
|
||||||
|
|
||||||
|
if (length == -1) {
|
||||||
|
result.type = Result::Type::Nil;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.type = Result::Type::String;
|
||||||
|
if (static_cast<size_t>(length) < buffer.length()) {
|
||||||
|
result.string += buffer.substr(size + 3, buffer.length() - size - 3);
|
||||||
|
} else {
|
||||||
|
result.string += buffer.substr(size + 3);
|
||||||
|
remaining = length - result.string.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case '*': {
|
||||||
|
size_t size;
|
||||||
|
long length{std::stol(buffer.substr(1), &size)};
|
||||||
|
|
||||||
|
if (length == -1) {
|
||||||
|
result.type = Result::Type::Nil;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.type = Result::Type::Array;
|
||||||
|
remaining = length;
|
||||||
|
|
||||||
|
while (remaining) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Error
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void continue_parse_type(Result& result, const std::string& buffer, size_t& remaining)
|
||||||
|
{
|
||||||
|
switch (result.type) {
|
||||||
|
case Result::Type::String:
|
||||||
|
case Result::Type::ProtError:
|
||||||
|
case Result::Type::IOError:
|
||||||
|
if (remaining == buffer.length() - 2) {
|
||||||
|
result.string += buffer.substr(0, remaining);
|
||||||
|
remaining = 0;
|
||||||
|
} else {
|
||||||
|
result.string += buffer;
|
||||||
|
remaining -= buffer.length();
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string host_;
|
||||||
|
const std::string port_;
|
||||||
|
const size_t timeout_;
|
||||||
|
|
||||||
|
asio::io_context io_context_;
|
||||||
|
asio::ip::tcp::socket socket_;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Client::Client() : Client{"localhost", "6379"} { }
|
||||||
|
|
||||||
|
|
||||||
|
Client::Client(const std::string& address, size_t timeout)
|
||||||
|
{
|
||||||
|
std::string host, port;
|
||||||
|
std::stringstream sstream{address};
|
||||||
|
|
||||||
|
std::getline(sstream, host, ':');
|
||||||
|
std::getline(sstream, port);
|
||||||
|
|
||||||
|
impl_ = std::make_unique<ClientImpl>(host, port, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Client::Client(const std::string& host, const std::string& port, size_t timeout)
|
||||||
|
: impl_{std::make_unique<ClientImpl>(host, port, timeout)}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Client::~Client() { }
|
||||||
|
|
||||||
|
|
||||||
|
void Client::connect() { impl_->connect(); }
|
||||||
|
void Client::close() { impl_->close(); }
|
||||||
|
|
||||||
|
Result Client::command(const std::vector<std::string>& str)
|
||||||
|
{
|
||||||
|
if (str.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream builder;
|
||||||
|
builder << '*' << str.size() << "\r\n";
|
||||||
|
|
||||||
|
for (const std::string& part: str) {
|
||||||
|
if (is_number(part)) {
|
||||||
|
builder << ':' << part << "\r\n";
|
||||||
|
} else {
|
||||||
|
builder << '$' << part.length() << "\r\n" << part << "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return impl_->command(builder.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result Client::command(const std::stringstream& builder)
|
||||||
|
{
|
||||||
|
return impl_->command(builder.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
22
src/proto-cli.cc
Normal file
22
src/proto-cli.cc
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "cli-common.h"
|
||||||
|
#include "resp.pb.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
||||||
|
|
||||||
|
auto options{common::parse_commandline(argc, argv)};
|
||||||
|
|
||||||
|
google::protobuf::ShutdownProtobufLibrary();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
31
src/proxy.cc
Normal file
31
src/proxy.cc
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "cli-common.h"
|
||||||
|
#include "resply.h"
|
||||||
|
#include "resp.pb.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
||||||
|
|
||||||
|
auto options{common::parse_commandline(argc, argv)};
|
||||||
|
|
||||||
|
if (options.show_version) {
|
||||||
|
std::cout
|
||||||
|
<< argv[0] << '\n'
|
||||||
|
<< "Using resply version " << resply::version() << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
google::protobuf::ShutdownProtobufLibrary();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
56
src/resply-cli.cc
Normal file
56
src/resply-cli.cc
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "cli-common.h"
|
||||||
|
#include "resply.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
auto options{common::parse_commandline(argc, argv)};
|
||||||
|
|
||||||
|
if (options.show_version) {
|
||||||
|
std::cout
|
||||||
|
<< argv[0] << '\n'
|
||||||
|
<< "Using resply version " << resply::version() << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resply::Client client{options.host, options.port};
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
while (std::cin) {
|
||||||
|
std::cout << options.host << ':' << options.port << "> ";
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::getline(std::cin, line);
|
||||||
|
std::stringstream linestream{line};
|
||||||
|
|
||||||
|
std::vector<std::string> command;
|
||||||
|
while (linestream >> line) {
|
||||||
|
command.push_back(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stringstream sstream;
|
||||||
|
sstream << client.command(command);
|
||||||
|
|
||||||
|
std::cout << sstream.str();
|
||||||
|
if (sstream.str().back() != '\n') {
|
||||||
|
std::cout << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
|
||||||
|
std::cout << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
21
tests/ping-pong.cc
Normal file
21
tests/ping-pong.cc
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "resply.h"
|
||||||
|
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
resply::Client client;
|
||||||
|
client.connect();
|
||||||
|
|
||||||
|
std::stringstream stream;
|
||||||
|
stream << client.command("ping");
|
||||||
|
|
||||||
|
return stream.str() != "PONG";
|
||||||
|
}
|
Reference in a new issue