├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── asio_redis_client.h ├── asio_redis_client_test.cc ├── error_code.h ├── main.cpp └── parser ├── config.h ├── redisparser.cpp ├── redisparser.h └── redisvalue.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "future"] 2 | path = future 3 | url = https://github.com/topcpporg/future.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(asio_redis_client) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | set(CMAKE_CXX_FLAGS "-fprofile-instr-generate -fcoverage-mapping") 7 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall -Wextra") 8 | 9 | #ADD_DEFINITIONS(-DUSE_FUTURE) 10 | ADD_DEFINITIONS(-DDEBUG_INFO) 11 | 12 | include_directories( 13 | /usr/local/include 14 | future 15 | ) 16 | 17 | link_directories( 18 | /usr/local/lib 19 | ) 20 | 21 | add_executable(asio_redis_client 22 | main.cpp 23 | ) 24 | 25 | target_link_libraries(asio_redis_client -labsl_bad_variant_access) 26 | 27 | add_executable(asio_redis_client_test 28 | asio_redis_client_test.cc 29 | ) 30 | 31 | target_link_libraries(asio_redis_client_test gtest -labsl_bad_variant_access) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 qicosmos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asio_redis_client 2 | an easy to use, thread-safe, cross-platform async redis client implemented in c++11. 3 | 4 | The best c++ redis client! 5 | 6 | # dependcy 7 | asio 8 | 9 | future extention(submodule) 10 | 11 | # how to use 12 | 13 | ``` 14 | git clone https://github.com/topcpporg/asio_redis_client.git 15 | 16 | git submodule update --init --recursive 17 | ``` 18 | 19 | # quick example 20 | 21 | ## async interface 22 | 23 | ``` 24 | //create redis client 25 | std::shared_ptr create_client(){ 26 | auto client = std::make_shared(ios); 27 | client->enable_auto_reconnect(true); 28 | 29 | client->set_error_callback([](RedisValue value){ 30 | std::cout<connect(host_name, 6379); 34 | assert(r); 35 | return client; 36 | } 37 | 38 | void get_set() { 39 | auto client = create_client(); 40 | 41 | client->auth("123456", [](RedisValue value) { 42 | if(value.isError()){ 43 | std::cout<<"redis error:"<set("hello", "world", [](RedisValue value) { 50 | std::cout << "set: " << value.toString() << '\n'; 51 | }); 52 | 53 | client->get("hello", [](RedisValue value) { 54 | std::cout << "get: " << value.toString() << '\n'; 55 | }); 56 | 57 | client->command("info", {"stats"}, [](RedisValue value) { 58 | std::cout << "info stats: " << value.toString() << '\n'; 59 | }); 60 | 61 | client->command("get", {"hello"}, [](RedisValue value) { 62 | std::cout << "get result: " << value.toString() << '\n'; 63 | }); 64 | 65 | client->del("hello", [](RedisValue value) { 66 | std::cout << "del: " << value.inspect() << '\n'; 67 | }); 68 | 69 | client->publish("mychannel", "hello world", [](RedisValue value) { 70 | std::cout << "publish mychannel ok, number:" << value.inspect() << '\n'; 71 | }); 72 | } 73 | ``` 74 | 75 | ## sync interface 76 | 77 | fun with future! 78 | ``` 79 | void future(){ 80 | auto client = create_client(); 81 | 82 | auto auth_future = client->auth("123456"); 83 | auto set_future = client->set("hello", "world"); 84 | auto get_future = client->get("hello"); 85 | auto del_future = client->del("hello"); 86 | 87 | std::cout<<"future------------\n"; 88 | std::cout<<"auth result:"<close(); 93 | } 94 | 95 | void future_then(){ 96 | auto client = create_client(); 97 | auto future = client->auth("123456").Then([&](RedisValue value){ 98 | std::cout<<"auth result:"<set("hello", "world").Get(); 100 | }).Then([&](RedisValue value){ 101 | std::cout<<"set result:"<get("hello").Get(); 103 | }).Then([&](RedisValue value){ 104 | std::cout<<"get result:"<del("hello").Get(); 106 | }).Then([](RedisValue value){ 107 | std::cout<auth("123456").Then([=](RedisValue value){ 119 | std::cout<<"auth result:"<set("hello", "world").Get(); 121 | }).Then([=](RedisValue value){ 122 | std::cout<<"set result:"<get("hello").Get(); 124 | }).Then([=](RedisValue value){ 125 | std::cout<<"get result:"<del("hello").Get(); 127 | }).Then([](RedisValue value){ 128 | std::cout<(ios); 141 | client->enable_auto_reconnect(true); 142 | 143 | //network error occurred 144 | client->set_error_callback([](RedisValue value){ 145 | std::cout<auth("123456", [](RedisValue value) { 150 | if(value.isError()){ 151 | std::cout<<"redis error:"< 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "parser/redisparser.h" 13 | #include "error_code.h" 14 | 15 | #ifdef USE_FUTURE 16 | #include 17 | #endif 18 | 19 | namespace purecpp { 20 | constexpr const char *CRCF = "\r\n"; 21 | constexpr const size_t CRCF_SIZE = 2; 22 | using RedisCallback = std::function; 23 | 24 | template inline void print(Args &&... args) { 25 | #ifdef DEBUG_INFO 26 | (void)std::initializer_list{ 27 | (std::cout << std::forward(args) << ' ', 0)...}; 28 | std::cout << "\n"; 29 | #endif 30 | (void)sizeof...(args); 31 | } 32 | 33 | template 34 | inline std::string make_command(const Container &c) { 35 | std::string result; 36 | result.append("*").append(std::to_string(c.size())).append(CRCF); 37 | 38 | for (const auto &item : c) { 39 | result.append("$").append(std::to_string(item.size())).append(CRCF); 40 | result.append(item).append(CRCF); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | class asio_redis_client 47 | : public std::enable_shared_from_this { 48 | public: 49 | asio_redis_client(boost::asio::io_service &ios) 50 | : ios_(ios), resolver_(ios), socket_(ios), work_(timer_ios_) { 51 | std::thread thd([this] { timer_ios_.run(); }); 52 | thd.detach(); 53 | } 54 | 55 | ~asio_redis_client() { 56 | timer_ios_.stop(); 57 | close(); 58 | } 59 | 60 | size_t connect_with_trytimes(const std::string &host, unsigned short port, 61 | size_t try_times) { 62 | bool auto_reconnect = enbale_auto_reconnect_; 63 | enbale_auto_reconnect_ = false; 64 | size_t has_try = 0; 65 | for (; has_try < try_times; has_try++) { 66 | if (connect(host, port)) { 67 | enbale_auto_reconnect_ = auto_reconnect; 68 | return has_try; 69 | } 70 | 71 | reset_socket(); 72 | 73 | print("retry times: ", has_try); 74 | std::this_thread::sleep_for(std::chrono::seconds(1)); 75 | } 76 | 77 | enbale_auto_reconnect_ = auto_reconnect; 78 | return has_try; 79 | } 80 | 81 | bool connect(const std::string &host, unsigned short port, 82 | size_t timeout_seconds = 3) { 83 | host_ = host; 84 | port_ = port; 85 | auto promise = std::make_shared>(); 86 | std::weak_ptr> weak(promise); 87 | 88 | async_connect_inner(host, port, weak); 89 | 90 | auto future = promise->get_future(); 91 | auto status = future.wait_for(std::chrono::seconds(timeout_seconds)); 92 | if (status == std::future_status::timeout) { 93 | print("connect timeout"); 94 | promise = nullptr; 95 | close_inner(); 96 | return false; 97 | } 98 | 99 | bool r = future.get(); 100 | promise = nullptr; 101 | return r; 102 | } 103 | 104 | #ifdef USE_FUTURE 105 | Future auth(const std::string &password) { 106 | password_ = password; 107 | std::vector v{"AUTH", password}; 108 | return command(make_command(v)); 109 | } 110 | 111 | Future get(const std::string &key) { 112 | std::vector v{"GET", key}; 113 | return command(make_command(v)); 114 | } 115 | 116 | Future set(const std::string &key, const std::string &value) { 117 | std::vector v{"SET", key, value}; 118 | return command(make_command(v)); 119 | } 120 | 121 | Future del(const std::string &key) { 122 | std::vector v{"DEL", key}; 123 | return command(make_command(v)); 124 | } 125 | 126 | Future ping() { 127 | std::vector v{"PING"}; 128 | return command(make_command(v)); 129 | } 130 | 131 | Future command(const std::string &cmd, 132 | std::deque args) { 133 | args.push_front(cmd); 134 | return command(make_command(args)); 135 | } 136 | 137 | Future unsubscribe(const std::string &key) { 138 | std::vector v{"UNSUBSCRIBE", key}; 139 | return command(make_command(v)); 140 | } 141 | 142 | Future punsubscribe(const std::string &key) { 143 | std::vector v{"PUNSUBSCRIBE", key}; 144 | return command(make_command(v)); 145 | } 146 | #endif 147 | 148 | void command(const std::string &cmd, std::deque args, 149 | RedisCallback callback) { 150 | args.push_front(cmd); 151 | return command(make_command(args), std::move(callback)); 152 | } 153 | 154 | template ::value>::type> 156 | void set(const std::string &key, const T &value, RedisCallback callback) { 157 | std::vector v{"SET", key, std::to_string(value)}; 158 | command(make_command(v), std::move(callback)); 159 | } 160 | 161 | void set(const std::string &key, const std::string &value, 162 | RedisCallback callback) { 163 | std::vector v{"SET", key, value}; 164 | command(make_command(v), std::move(callback)); 165 | } 166 | 167 | void del(const std::string &key, RedisCallback callback) { 168 | std::vector v{"DEL", key}; 169 | command(make_command(v), std::move(callback)); 170 | } 171 | 172 | void ping(RedisCallback callback) { 173 | std::vector v{"PING"}; 174 | command(make_command(v), std::move(callback)); 175 | } 176 | 177 | void auth(const std::string &password, RedisCallback callback) { 178 | password_ = password; 179 | auth_callback_ = callback; 180 | std::vector v{"AUTH", password}; 181 | command(make_command(v), std::move(callback)); 182 | } 183 | 184 | void get(const std::string &key, RedisCallback callback) { 185 | std::vector v{"GET", key}; 186 | command(make_command(v), std::move(callback)); 187 | } 188 | 189 | void publish(const std::string &channel, const std::string &msg, 190 | RedisCallback callback) { 191 | std::vector v{"PUBLISH", channel, msg}; 192 | command(make_command(v), std::move(callback)); 193 | } 194 | 195 | void subscribe(const std::string &key, RedisCallback callback) { 196 | std::vector v{"SUBSCRIBE", key}; 197 | command(make_command(v), std::move(callback), key); 198 | } 199 | 200 | void psubscribe(const std::string &key, RedisCallback callback) { 201 | std::vector v{"PSUBSCRIBE", key}; 202 | command(make_command(v), std::move(callback), key); 203 | } 204 | 205 | void unsubscribe(const std::string &key, RedisCallback callback) { 206 | std::vector v{"UNSUBSCRIBE", key}; 207 | command(make_command(v), std::move(callback)); 208 | } 209 | 210 | void punsubscribe(const std::string &key, RedisCallback callback) { 211 | std::vector v{"PUNSUBSCRIBE", key}; 212 | command(make_command(v), std::move(callback)); 213 | } 214 | 215 | void command(const std::string &key, std::deque args, 216 | size_t retry_times, RedisCallback callback) { 217 | args.push_front(key); 218 | auto cmd = make_command(args); 219 | auto init_time = std::chrono::steady_clock::now(); 220 | command_inner(init_time, cmd, retry_times, std::move(callback)); 221 | } 222 | 223 | void enable_auto_reconnect(bool enable) { enbale_auto_reconnect_ = enable; } 224 | 225 | void close() { 226 | enbale_auto_reconnect_ = false; 227 | close_inner(); 228 | } 229 | 230 | bool has_connected() const { return has_connected_; } 231 | 232 | void set_retry_timeout_ms(int64_t retry_timeout_ms) { 233 | retry_timeout_ms_ = retry_timeout_ms; 234 | } 235 | 236 | void async_connect(const std::string &host, unsigned short port, 237 | RedisCallback callback) { 238 | host_ = host; 239 | port_ = port; 240 | 241 | boost::asio::ip::tcp::resolver::query query(host, std::to_string(port)); 242 | auto self = this->shared_from_this(); 243 | resolver_.async_resolve( 244 | query, [this, self, 245 | callback](boost::system::error_code ec, 246 | const boost::asio::ip::tcp::resolver::iterator &it) { 247 | if (ec) { 248 | callback(RedisValue(ErrorCode::io_error, ec.message())); 249 | return; 250 | } 251 | 252 | auto self = shared_from_this(); 253 | boost::asio::async_connect( 254 | socket_, it, 255 | [this, self, 256 | callback](boost::system::error_code ec, 257 | const boost::asio::ip::tcp::resolver::iterator &) { 258 | if (!ec) { 259 | if (has_connected_) { 260 | return; 261 | } 262 | 263 | has_connected_ = true; 264 | print("connect ok"); 265 | resubscribe(); 266 | do_read(); 267 | callback(RedisValue(ErrorCode::no_error, "connect ok")); 268 | } else { 269 | print(ec.message()); 270 | close_inner(); 271 | if (enbale_auto_reconnect_) { 272 | print("auto reconnect"); 273 | async_connect(host_, port_, std::move(callback)); 274 | } else { 275 | callback(RedisValue(ErrorCode::io_error, "connect failed")); 276 | } 277 | } 278 | }); 279 | }); 280 | } 281 | 282 | private: 283 | void async_connect_inner(const std::string &host, unsigned short port, 284 | std::weak_ptr> weak) { 285 | boost::asio::ip::tcp::resolver::query query(host, std::to_string(port)); 286 | auto self = this->shared_from_this(); 287 | resolver_.async_resolve( 288 | query, 289 | [this, self, weak](boost::system::error_code ec, 290 | const boost::asio::ip::tcp::resolver::iterator &it) { 291 | if (ec) { 292 | auto sp = weak.lock(); 293 | if (sp) { 294 | sp->set_value(false); 295 | } 296 | 297 | return; 298 | } 299 | 300 | auto self = shared_from_this(); 301 | boost::asio::async_connect( 302 | socket_, it, 303 | [this, self, 304 | weak](boost::system::error_code ec, 305 | const boost::asio::ip::tcp::resolver::iterator &) { 306 | if (!ec) { 307 | if (has_connected_) { 308 | return; 309 | } 310 | 311 | has_connected_ = true; 312 | print("connect ok"); 313 | resubscribe(); 314 | do_read(); 315 | } else { 316 | print(ec.message()); 317 | close_inner(); 318 | if (enbale_auto_reconnect_) { 319 | print("auto reconnect"); 320 | async_reconnect(); 321 | } 322 | } 323 | 324 | auto sp = weak.lock(); 325 | if (sp) 326 | sp->set_value(has_connected_); 327 | }); 328 | }); 329 | } 330 | 331 | void async_reconnect() { 332 | reset_socket(); 333 | async_connect_inner(host_, port_, {}); 334 | std::this_thread::sleep_for(std::chrono::milliseconds(1000)); 335 | } 336 | 337 | void resubscribe() { 338 | if (!password_.empty()) { 339 | auto self = shared_from_this(); 340 | assert(auth_callback_); 341 | auth(password_, std::move(auth_callback_)); 342 | } 343 | 344 | if (sub_handlers_.empty()) { 345 | return; 346 | } 347 | 348 | for (auto &pair : sub_handlers_) { 349 | if (pair.first.find("*") != std::string::npos) { // improve later 350 | psubscribe(pair.first, pair.second); 351 | } else { 352 | subscribe(pair.first, pair.second); 353 | } 354 | } 355 | } 356 | 357 | void do_read() { 358 | auto self = shared_from_this(); 359 | async_read_some([this, self](boost::system::error_code ec, size_t size) { 360 | if (ec) { 361 | handle_io_error(ec); 362 | 363 | close_inner(); 364 | if (enbale_auto_reconnect_) { 365 | async_reconnect(); 366 | } 367 | return; 368 | } 369 | 370 | for (size_t pos = 0; pos < size;) { 371 | std::pair result = 372 | parser_.parse(read_buf_.data() + pos, size - pos); 373 | 374 | if (result.second == RedisParser::Completed) { 375 | handle_message(parser_.result()); 376 | } else if (result.second == RedisParser::Incompleted) { 377 | do_read(); 378 | return; 379 | } else { 380 | handle_message( 381 | RedisValue(ErrorCode::redis_parse_error, "redis parse error")); 382 | return; 383 | } 384 | 385 | pos += result.first; 386 | } 387 | 388 | do_read(); 389 | }); 390 | } 391 | 392 | void close_inner() { 393 | if (!has_connected_) 394 | return; 395 | 396 | has_connected_ = false; 397 | 398 | clear_outbox_and_handlers(); 399 | 400 | boost::system::error_code ec; 401 | // timer_.cancel(ec); 402 | socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); 403 | socket_.close(ec); 404 | } 405 | 406 | void reset_socket() { 407 | socket_ = decltype(socket_)(ios_); 408 | if (!socket_.is_open()) { 409 | socket_.open(boost::asio::ip::tcp::v4()); 410 | } 411 | } 412 | 413 | bool is_subscribe(const std::string &cmd) { 414 | if (cmd == "subscribe" || cmd == "psubscribe" || cmd == "message" || 415 | cmd == "pmessage") { 416 | return true; 417 | } 418 | 419 | return false; 420 | } 421 | 422 | void handle_array_msg(RedisValue v) { 423 | std::vector array = v.toArray(); 424 | auto &value = array[0]; 425 | std::string cmd = value.toString(); 426 | if (is_subscribe(cmd)) { 427 | if (array.size() < 3) { 428 | // error, not redis protocol 429 | return; 430 | } 431 | 432 | handle_subscribe_msg(std::move(cmd), std::move(array)); 433 | } else { 434 | handle_non_subscribe_msg(std::move(v)); 435 | } 436 | } 437 | 438 | void handle_subscribe_msg(std::string cmd, std::vector array) { 439 | std::string subscribe_key = array[1].toString(); 440 | RedisValue value; 441 | if (cmd == "subscribe" || cmd == "psubscribe") { 442 | // reply subscribe 443 | print(cmd); 444 | return; 445 | } else if (cmd == "message") { 446 | value = std::move(array[2]); 447 | } else { // pmessage 448 | value = std::move(array[3]); 449 | } 450 | 451 | std::function *callback = nullptr; 452 | { 453 | assert(!sub_handlers_.empty()); 454 | auto it = sub_handlers_.find(subscribe_key); 455 | if (it != sub_handlers_.end()) { 456 | callback = &it->second; 457 | } else { 458 | print("subscibe key: ", subscribe_key, " not found"); 459 | } 460 | } 461 | 462 | if (callback) { 463 | try { 464 | auto &cb = *callback; 465 | if (cb) { 466 | cb(std::move(value)); 467 | } 468 | } catch (std::exception &e) { 469 | print(e.what()); 470 | } catch (...) { 471 | print("unknown exception"); 472 | } 473 | } 474 | } 475 | 476 | void handle_non_subscribe_msg(RedisValue value) { 477 | std::function front = nullptr; 478 | { 479 | std::unique_lock lock(write_mtx_); 480 | if (handlers_.empty()) { 481 | print("warning! no handler deal with this value : ", value.inspect()); 482 | return; 483 | } 484 | 485 | front = std::move(handlers_.front()); 486 | handlers_.pop_front(); 487 | } 488 | 489 | try { 490 | if (front) { 491 | front(std::move(value)); 492 | } 493 | } catch (std::exception &e) { 494 | print(e.what()); 495 | } catch (...) { 496 | print("unknown exception"); 497 | } 498 | } 499 | 500 | void handle_message(RedisValue v) { 501 | if (v.isArray()) { 502 | handle_array_msg(std::move(v)); 503 | } else { 504 | handle_non_subscribe_msg(std::move(v)); 505 | } 506 | } 507 | 508 | template 509 | void command_inner(T init_time, const std::string &cmd, size_t retry_times, 510 | RedisCallback callback) { 511 | if (retry_times > 0) { 512 | command(cmd, [this, init_time, retry_times, cmd, 513 | callback](RedisValue value) mutable { 514 | if (value.IsIOError()) { 515 | retry(init_time, std::move(cmd), retry_times, std::move(callback)); 516 | } else { 517 | callback(value); 518 | } 519 | }); 520 | } else { 521 | //finished retry now, don't retry anymore. 522 | command(cmd, std::move(callback)); 523 | } 524 | } 525 | 526 | template 527 | void retry(T init_time, std::string cmd, size_t retry_times, 528 | RedisCallback callback) { 529 | assert(retry_times > 0); 530 | 531 | auto timer = std::make_shared( 532 | timer_ios_, std::chrono::milliseconds(200)); 533 | timer->async_wait([timer, this, init_time, retry_times, cmd, 534 | callback](boost::system::error_code ec) mutable { 535 | if (ec) { 536 | // cancel 537 | return; 538 | } 539 | 540 | auto now = std::chrono::steady_clock::now(); 541 | int64_t elapsed = 542 | std::chrono::duration_cast(now - init_time) 543 | .count(); 544 | if (elapsed >= retry_timeout_ms_) { 545 | callback(RedisValue(ErrorCode::timeout, "retry timeout")); 546 | return; 547 | } 548 | 549 | if (has_connected_) { 550 | retry_times--; 551 | print("retry times: ", retry_times); 552 | command_inner(init_time, cmd, retry_times, std::move(callback)); 553 | } else { 554 | retry(init_time, std::move(cmd), retry_times, std::move(callback)); 555 | } 556 | }); 557 | } 558 | 559 | void write() { 560 | auto &msg = outbox_[0]; 561 | auto self = shared_from_this(); 562 | async_write(msg, [this, self](const boost::system::error_code &ec, size_t) { 563 | if (ec) { 564 | print(ec.message()); 565 | handle_io_error(ec); 566 | close_inner(); 567 | return; 568 | } 569 | 570 | std::unique_lock lock(write_mtx_); 571 | if (outbox_.empty()) { 572 | return; 573 | } 574 | 575 | outbox_.pop_front(); 576 | 577 | if (!outbox_.empty()) { 578 | // more messages to send 579 | write(); 580 | } 581 | }); 582 | } 583 | 584 | void clear_outbox_and_handlers() { 585 | std::unique_lock lock(write_mtx_); 586 | if (!handlers_.empty()) { 587 | handlers_.clear(); 588 | } 589 | if (!outbox_.empty()) { 590 | outbox_.clear(); 591 | } 592 | } 593 | 594 | void handle_io_error(const boost::system::error_code &ec) { 595 | has_connected_ = false; 596 | std::unique_lock lock(write_mtx_); 597 | 598 | for (auto &handler : handlers_) { 599 | handler(RedisValue(ErrorCode::io_error, ec.message())); 600 | } 601 | handlers_.clear(); 602 | outbox_.clear(); 603 | } 604 | 605 | boost::asio::steady_timer create_timer() { 606 | boost::asio::steady_timer timer( 607 | ios_, std::chrono::milliseconds(retry_timeout_ms_)); 608 | 609 | timer.async_wait([](const boost::system::error_code &ec) { 610 | if (ec) { 611 | return; 612 | } 613 | 614 | // handle_timeout(); 615 | }); 616 | 617 | return timer; 618 | } 619 | 620 | template void async_read_some(Handler handler) { 621 | socket_.async_read_some(boost::asio::buffer(read_buf_), std::move(handler)); 622 | } 623 | 624 | template 625 | void async_write(const std::string &msg, Handler handler) { 626 | boost::asio::async_write(socket_, boost::asio::buffer(msg), 627 | std::move(handler)); 628 | } 629 | 630 | void command(const std::string &cmd, RedisCallback callback, 631 | std::string sub_key = "") { 632 | std::unique_lock lock(write_mtx_); 633 | outbox_.emplace_back(cmd); 634 | 635 | if (callback != nullptr) { 636 | if (sub_key.empty()) { 637 | handlers_.emplace_back(std::move(callback)); 638 | } else { 639 | sub_handlers_.emplace(std::move(sub_key), std::move(callback)); 640 | } 641 | } 642 | 643 | if (outbox_.size() > 1) { 644 | return; 645 | } 646 | 647 | write(); 648 | } 649 | 650 | #ifdef USE_FUTURE 651 | Future command(const std::string &cmd) { 652 | if (!has_connected_) { 653 | return {}; 654 | } 655 | 656 | std::shared_ptr> promise = 657 | std::make_shared>(); 658 | auto callback = [promise](RedisValue value) { 659 | promise->SetValue(std::move(value)); 660 | }; 661 | 662 | { 663 | std::unique_lock lock(write_mtx_); 664 | outbox_.emplace_back(cmd); 665 | handlers_.emplace_back(std::move(callback)); 666 | 667 | if (outbox_.size() <= 1) { 668 | write(); 669 | } 670 | } 671 | 672 | return promise->GetFuture(); 673 | } 674 | #endif 675 | 676 | boost::asio::io_service &ios_; 677 | boost::asio::ip::tcp::resolver resolver_; 678 | boost::asio::ip::tcp::socket socket_; 679 | std::atomic_bool has_connected_ = {false}; 680 | 681 | std::deque outbox_; 682 | std::mutex write_mtx_; 683 | std::array read_buf_; 684 | RedisParser parser_; 685 | std::deque> handlers_; 686 | std::map> sub_handlers_; 687 | bool enbale_auto_reconnect_ = false; 688 | std::string host_; 689 | unsigned short port_; 690 | std::string password_; 691 | RedisCallback auth_callback_ = nullptr; 692 | 693 | std::atomic retry_timeout_ms_ = {15000}; 694 | boost::asio::io_context timer_ios_; 695 | boost::asio::io_context::work work_; 696 | }; 697 | } 698 | #endif // ASIO_REDIS_CLIENT_ASIO_REDIS_CLIENT_H 699 | -------------------------------------------------------------------------------- /asio_redis_client_test.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Created by qicosmos on 2020/9/28. 3 | // 4 | #include 5 | #include 6 | #include "asio_redis_client.h" 7 | 8 | using namespace purecpp; 9 | 10 | boost::asio::io_service ios; 11 | boost::asio::io_service::work work(ios); 12 | std::thread thd([]{ 13 | ios.run(); 14 | }); 15 | static const std::string host_name = "11.166.214.161"; 16 | static const std::string password = "123456"; 17 | 18 | std::shared_ptr create_client(){ 19 | auto client = std::make_shared(ios); 20 | client->enable_auto_reconnect(true); 21 | 22 | bool r = client->connect(host_name, 6379); 23 | assert(r); 24 | return client; 25 | } 26 | 27 | TEST(RedisClient, connect_failed){ 28 | auto client = std::make_shared(ios); 29 | client->enable_auto_reconnect(true); 30 | 31 | unsigned short invalid_port = 0; 32 | bool r = client->connect(host_name, invalid_port); 33 | EXPECT_FALSE(r); 34 | 35 | size_t has_try_times = client->connect_with_trytimes(host_name, invalid_port, 2); 36 | EXPECT_EQ(has_try_times, size_t(2)); 37 | } 38 | 39 | //make sure the redis server has been started! 40 | TEST(RedisClient, connect){ 41 | auto client = std::make_shared(ios); 42 | client->enable_auto_reconnect(true); 43 | 44 | bool r = client->connect(host_name, 6379); 45 | if(!r){ 46 | return; 47 | } 48 | 49 | auto client1 = std::make_shared(ios); 50 | size_t has_try_times = client1->connect_with_trytimes(host_name, 6379, 2); 51 | EXPECT_EQ(has_try_times, size_t(0)); 52 | } 53 | 54 | TEST(RedisClient, auth_get_set_del) { 55 | auto client = create_client(); 56 | client->auth(password, [=](RedisValue value) { 57 | EXPECT_TRUE(value.isError()); //if no password, will return error 58 | std::cout << "auth: " << value.toString() << '\n'; 59 | client->set("hello", "world", [=](RedisValue value) { 60 | EXPECT_FALSE(value.isError()); 61 | std::cout << "set: " << value.toString() << '\n'; 62 | client->get("hello", [=](RedisValue value) { 63 | EXPECT_FALSE(value.isError()); 64 | std::cout << "get: " << value.toString() << '\n'; 65 | client->del("hello", [=](RedisValue value) { 66 | EXPECT_FALSE(value.isError()); 67 | std::cout << "del: " << value.inspect() << '\n'; 68 | }); 69 | }); 70 | }); 71 | }); 72 | } 73 | 74 | TEST(RedisClient, ping){ 75 | auto client = create_client(); 76 | client->ping([](RedisValue value) { 77 | EXPECT_FALSE(value.isError()); 78 | }); 79 | 80 | #ifdef USE_FUTURE 81 | auto future = client->ping(); 82 | EXPECT_FALSE(future.Get().isError()); 83 | #endif 84 | } 85 | 86 | TEST(RedisClient, publish){ 87 | auto client = create_client(); 88 | client->publish("mychannel", "hello world", [](RedisValue value) { 89 | EXPECT_FALSE(value.isError()); 90 | std::cout << "publish mychannel ok, number:" << value.inspect() << '\n'; 91 | }); 92 | } 93 | 94 | bool stop_publish = false; 95 | void create_publisher() { 96 | std::thread pub_thd([] { 97 | auto publisher = create_client(); 98 | 99 | for (int i = 0; i < 3000; i++) { 100 | if (stop_publish) { 101 | break; 102 | } 103 | std::this_thread::sleep_for(std::chrono::seconds(1)); 104 | 105 | publisher->publish("mychannel", "hello world", [](RedisValue value) { 106 | EXPECT_FALSE(value.isError()); 107 | std::cout << "publish mychannel ok, number:" << value.inspect() << '\n'; 108 | }); 109 | } 110 | }); 111 | pub_thd.detach(); 112 | } 113 | 114 | TEST(RedisClient, pub_sub){ 115 | create_publisher(); 116 | 117 | auto client = create_client(); 118 | client->subscribe("mychannel", [=](RedisValue value) { 119 | EXPECT_FALSE(value.isError()); 120 | std::cout << "subscribe mychannel: " << value.toString() << '\n'; 121 | client->unsubscribe("mychannel", [=](RedisValue value) { 122 | EXPECT_FALSE(value.isError()); 123 | std::cout << "unsubscribe mychannel: " << value.toString() << '\n'; 124 | }); 125 | }); 126 | } 127 | 128 | TEST(RedisClient, unsub){ 129 | auto client = create_client(); 130 | client->unsubscribe("mychannel", [=](RedisValue value) { 131 | EXPECT_FALSE(value.isError()); 132 | std::cout << "unsubscribe mychannel: " << value.toString() << '\n'; 133 | }); 134 | 135 | client->punsubscribe("mychanne*", [=](RedisValue value) { 136 | EXPECT_FALSE(value.isError()); 137 | std::cout << "punsubscribe mychannel: " << value.toString() << '\n'; 138 | stop_publish = true; 139 | }); 140 | 141 | #ifdef USE_FUTURE 142 | auto unsub_future = client->unsubscribe("mychannel"); 143 | auto punsub_future = client->punsubscribe("mychanne*"); 144 | EXPECT_FALSE(unsub_future.Get().isError()); 145 | EXPECT_FALSE(punsub_future.Get().isError()); 146 | #endif 147 | } 148 | 149 | TEST(RedisClient, pub_psub){ 150 | auto client = create_client(); 151 | client->psubscribe("mychanne*", [=](RedisValue value) { 152 | EXPECT_FALSE(value.isError()); 153 | std::cout << "psubscribe mychannel: " << value.toString() << '\n'; 154 | client->punsubscribe("mychanne*", [=](RedisValue value) { 155 | EXPECT_FALSE(value.isError()); 156 | std::cout << "punsubscribe mychannel: " << value.toString() << '\n'; 157 | stop_publish = true; 158 | }); 159 | }); 160 | } 161 | 162 | 163 | 164 | TEST(RedisClient, command){ 165 | auto client = create_client(); 166 | client->command("info", {"stats"}, [](RedisValue value) { 167 | EXPECT_FALSE(value.isError()); 168 | std::cout << "info stats: " << value.toString() << '\n'; 169 | }); 170 | 171 | client->command("get", {"hello"}, [](RedisValue value) { 172 | EXPECT_FALSE(value.isError()); 173 | std::cout << "get result: " << value.toString() << '\n'; 174 | }); 175 | 176 | #ifdef USE_FUTURE 177 | auto future = client->command("info", {"stats"}); 178 | EXPECT_FALSE(future.Get().isError()); 179 | #endif 180 | } 181 | 182 | std::thread finally_thread; 183 | #ifdef USE_FUTURE 184 | TEST(RedisClient, future) { 185 | auto client = create_client(); 186 | 187 | auto auth_future = client->auth(password); 188 | auto set_future = client->set("hello", "world"); 189 | auto get_future = client->get("hello"); 190 | auto del_future = client->del("hello"); 191 | 192 | EXPECT_TRUE(auth_future.Get().isError()); 193 | EXPECT_FALSE(set_future.Get().isError()); 194 | EXPECT_FALSE(get_future.Get().isError()); 195 | EXPECT_FALSE(del_future.Get().isError()); 196 | client->close(); 197 | } 198 | 199 | TEST(RedisClient, future_then){ 200 | auto client = create_client(); 201 | auto future = client->auth(password).Then([&](RedisValue value){ 202 | EXPECT_TRUE(value.isError()); 203 | std::cout<<"auth result:"<set("hello", "world").Get(); 205 | }).Then([&](RedisValue value){ 206 | EXPECT_FALSE(value.isError()); 207 | std::cout<<"set result:"<get("hello").Get(); 209 | }).Then([&](RedisValue value){ 210 | EXPECT_FALSE(value.isError()); 211 | std::cout<<"get result:"<del("hello").Get(); 213 | }).Then([](RedisValue value){ 214 | EXPECT_FALSE(value.isError()); 215 | std::cout<auth(password).Then([=](RedisValue value){ 228 | EXPECT_TRUE(value.isError()); 229 | std::cout<<"auth result:"<set("hello", "world").Get(); 231 | }).Then([=](RedisValue value){ 232 | EXPECT_FALSE(value.isError()); 233 | std::cout<<"set result:"<get("hello").Get(); 235 | }).Then([=](RedisValue value){ 236 | EXPECT_FALSE(value.isError()); 237 | std::cout<<"get result:"<del("hello").Get(); 239 | }).Then([](RedisValue value){ 240 | EXPECT_FALSE(value.isError()); 241 | std::cout<has_connected()); 257 | client->close(); 258 | EXPECT_FALSE(client->has_connected()); 259 | ios.stop();//the last case stop the io_service to quit all the test case. 260 | thd.join(); 261 | } 262 | 263 | int main(int argc, char **argv) { 264 | ::testing::InitGoogleTest(&argc, argv); 265 | auto result = RUN_ALL_TESTS(); 266 | return result; 267 | } -------------------------------------------------------------------------------- /error_code.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace purecpp{ 4 | enum ErrorCode{ 5 | no_error, 6 | io_error, 7 | timeout, 8 | redis_parse_error, //redis protocal parse error 9 | redis_reject_error, //rejected by redis server 10 | unknown_error 11 | }; 12 | } -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by qicosmos on 2020/9/9. 3 | // 4 | #include 5 | #include 6 | #include 7 | #include "asio_redis_client.h" 8 | 9 | using namespace purecpp; 10 | boost::asio::io_service ios; 11 | boost::asio::io_service::work work(ios); 12 | std::thread thd([]{ 13 | ios.run(); 14 | }); 15 | static const std::string host_name = "127.0.0.1"; 16 | 17 | std::shared_ptr create_client(){ 18 | auto client = std::make_shared(ios); 19 | client->enable_auto_reconnect(true); 20 | 21 | bool r = client->connect(host_name, 6379); 22 | assert(r); 23 | return client; 24 | } 25 | 26 | void async_connect(){ 27 | auto client = std::make_shared(ios); 28 | client->enable_auto_reconnect(true); 29 | client->async_connect(host_name, 6379, [client](RedisValue value){ 30 | if(value.isOk()){ 31 | client->set("hello", "world", [](RedisValue value) { 32 | std::cout << "set: " << value.toString() << '\n'; 33 | }); 34 | 35 | client->get("hello", [](RedisValue value) { 36 | std::cout << "get: " << value.toString() << '\n'; 37 | }); 38 | } 39 | }); 40 | 41 | while(true){ 42 | std::string str; 43 | std::cin >> str; 44 | } 45 | 46 | } 47 | 48 | void test_retry(){ 49 | auto client = create_client(); 50 | 51 | client->command("get", {"hello"}, 2/*retry_times*/,[](RedisValue value) { 52 | std::cout << "get result: " << value.toString() << '\n'; 53 | }); 54 | 55 | client->command("set", {"hello", "world"}, 2/*retry_times*/,[](RedisValue value) { 56 | std::cout << "set: " << value.toString() << '\n'; 57 | }); 58 | 59 | 60 | std::string str; 61 | std::cin >> str; 62 | 63 | } 64 | 65 | void get_set() { 66 | auto client = create_client(); 67 | 68 | client->auth("123456", [](RedisValue value) { 69 | if(value.isError()){ 70 | std::cout<<"redis error:"<set("hello", "world", [](RedisValue value) { 77 | std::cout << "set: " << value.toString() << '\n'; 78 | }); 79 | 80 | client->get("hello", [](RedisValue value) { 81 | std::cout << "get: " << value.toString() << '\n'; 82 | }); 83 | 84 | client->command("info", {"stats"}, [](RedisValue value) { 85 | std::cout << "info stats: " << value.toString() << '\n'; 86 | }); 87 | 88 | client->command("get", {"hello"}, [](RedisValue value) { 89 | std::cout << "get result: " << value.toString() << '\n'; 90 | }); 91 | 92 | client->command( "get", {"hello"}, 2,[](RedisValue value) { 93 | std::cout << "get: " << value.toString() << '\n'; 94 | }); 95 | 96 | client->del("hello", [](RedisValue value) { 97 | std::cout << "del: " << value.inspect() << '\n'; 98 | }); 99 | 100 | std::string str; 101 | std::cin >> str; 102 | } 103 | 104 | bool stop = false; 105 | void create_publisher() { 106 | std::thread pub_thd([] { 107 | auto publisher = create_client(); 108 | 109 | for (int i = 0; i < 3000; i++) { 110 | if (stop) { 111 | break; 112 | } 113 | std::this_thread::sleep_for(std::chrono::seconds(1)); 114 | 115 | publisher->publish("mychannel", "hello world", [](RedisValue value) { 116 | std::cout << "publish mychannel ok, number:" << value.inspect() << '\n'; 117 | }); 118 | 119 | } 120 | }); 121 | pub_thd.detach(); 122 | } 123 | 124 | void pub_sub() { 125 | create_publisher(); 126 | 127 | auto client = create_client(); 128 | client->subscribe("mychannel", [=](RedisValue value) { 129 | std::cout << "subscribe mychannel: " << value.toString() << '\n'; 130 | // client->unsubscribe("mychannel", [](RedisValue value) { 131 | // std::cout << "unsubscribe mychannel: " << value.toString() << '\n'; 132 | // }); 133 | }); 134 | 135 | std::string str; 136 | std::cin >> str; 137 | 138 | client->unsubscribe("mychannel", [](RedisValue value) { 139 | std::cout << "unsubscribe mychannel: " << value.toString() << '\n'; 140 | }); 141 | 142 | stop = true; 143 | } 144 | 145 | void reconnect() { 146 | auto client = std::make_shared(ios); 147 | client->enable_auto_reconnect(true); 148 | client->connect(host_name, 6379); 149 | std::string str; 150 | std::cin >> str; 151 | client->ping([](RedisValue value) { 152 | std::cout << "ping: " << value.toString() << '\n'; 153 | }); 154 | } 155 | 156 | void reconnect_withtimes() { 157 | auto client = std::make_shared(ios); 158 | client->enable_auto_reconnect(true); 159 | client->connect_with_trytimes(host_name, 6379, 30); 160 | std::string str; 161 | std::cin >> str; 162 | client->ping([](RedisValue value) { 163 | std::cout << "ping: " << value.toString() << '\n'; 164 | }); 165 | } 166 | 167 | void callback_hell() { 168 | auto client = create_client(); 169 | client->auth("123456", [=](RedisValue value) { 170 | std::cout << "auth: " << value.toString() << '\n'; 171 | client->set("hello", "world", [=](RedisValue value) { 172 | std::cout << "set: " << value.toString() << '\n'; 173 | client->get("hello", [=](RedisValue value) { 174 | std::cout << "get: " << value.toString() << '\n'; 175 | client->del("hello", [=](RedisValue value) { 176 | std::cout << "del: " << value.inspect() << '\n'; 177 | }); 178 | }); 179 | }); 180 | }); 181 | } 182 | 183 | #ifdef USE_FUTURE 184 | void future(){ 185 | auto client = create_client(); 186 | 187 | auto auth_future = client->auth("123456"); 188 | auto set_future = client->set("hello", "world"); 189 | auto get_future = client->get("hello"); 190 | auto del_future = client->del("hello"); 191 | 192 | std::cout<<"future------------\n"; 193 | std::cout<<"auth result:"<close(); 198 | } 199 | 200 | void future_then(){ 201 | auto client = create_client(); 202 | auto future = client->auth("123456").Then([&](RedisValue value){ 203 | std::cout<<"auth result:"<set("hello", "world").Get(); 205 | }).Then([&](RedisValue value){ 206 | std::cout<<"set result:"<get("hello").Get(); 208 | }).Then([&](RedisValue value){ 209 | std::cout<<"get result:"<del("hello").Get(); 211 | }).Then([](RedisValue value){ 212 | std::cout<auth("123456").Then([=](RedisValue value){ 224 | std::cout<<"auth result:"<set("hello", "world").Get(); 226 | }).Then([=](RedisValue value){ 227 | std::cout<<"set result:"<get("hello").Get(); 229 | }).Then([=](RedisValue value){ 230 | std::cout<<"get result:"<del("hello").Get(); 232 | }).Then([](RedisValue value){ 233 | std::cout<> str; 263 | 264 | ios.stop(); 265 | thd.join(); 266 | 267 | return 0; 268 | } -------------------------------------------------------------------------------- /parser/config.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Alex Nekipelov (alex@nekipelov.net) 3 | * License: MIT 4 | */ 5 | 6 | #ifndef REDISCLIENT_CONFIG_H 7 | #define REDISCLIENT_CONFIG_H 8 | 9 | // Default to a header-only compilation 10 | #ifndef REDIS_CLIENT_HEADER_ONLY 11 | # ifndef REDIS_CLIENT_SEPARATED_COMPILATION 12 | # define REDIS_CLIENT_HEADER_ONLY 13 | # endif 14 | #endif 15 | 16 | #ifdef REDIS_CLIENT_HEADER_ONLY 17 | # define REDIS_CLIENT_DECL inline 18 | #else 19 | # if defined(WIN32) && defined(REDIS_CLIENT_DYNLIB) 20 | # // Build to a Window dynamic library (DLL) 21 | # ifdef REDIS_CLIENT_BUILD 22 | # define REDIS_CLIENT_DECL __declspec(dllexport) 23 | # else 24 | # define REDIS_CLIENT_DECL __declspec(dllimport) 25 | # endif 26 | # endif 27 | #endif 28 | 29 | #ifndef REDIS_CLIENT_DECL 30 | # define REDIS_CLIENT_DECL 31 | #endif 32 | 33 | 34 | #endif // REDISCLIENT_CONFIG_H 35 | -------------------------------------------------------------------------------- /parser/redisparser.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Alex Nekipelov (alex@nekipelov.net) 3 | * License: MIT 4 | */ 5 | 6 | #ifndef REDISCLIENT_REDISPARSER_CPP 7 | #define REDISCLIENT_REDISPARSER_CPP 8 | 9 | #include 10 | #include 11 | 12 | #ifdef DEBUG_REDIS_PARSER 13 | #include 14 | #endif 15 | 16 | #include "redisparser.h" 17 | 18 | namespace purecpp { 19 | 20 | RedisParser::RedisParser() 21 | : bulkSize(0) 22 | { 23 | buf.reserve(64); 24 | } 25 | 26 | std::pair RedisParser::parse(const char *ptr, size_t size) 27 | { 28 | return RedisParser::parseChunk(ptr, size); 29 | } 30 | 31 | std::pair RedisParser::parseChunk(const char *ptr, size_t size) 32 | { 33 | size_t position = 0; 34 | State state = Start; 35 | 36 | if (!states.empty()) 37 | { 38 | state = states.top(); 39 | states.pop(); 40 | } 41 | 42 | while(position < size) 43 | { 44 | char c = ptr[position++]; 45 | #ifdef DEBUG_REDIS_PARSER 46 | std::cerr << "state: " << state << ", c: " << c << "\n"; 47 | #endif 48 | 49 | switch(state) 50 | { 51 | case StartArray: 52 | case Start: 53 | buf.clear(); 54 | switch(c) 55 | { 56 | case stringReply: 57 | state = String; 58 | break; 59 | case errorReply: 60 | state = ErrorString; 61 | break; 62 | case integerReply: 63 | state = Integer; 64 | break; 65 | case bulkReply: 66 | state = BulkSize; 67 | bulkSize = 0; 68 | break; 69 | case arrayReply: 70 | state = ArraySize; 71 | break; 72 | default: 73 | return std::make_pair(position, Error); 74 | } 75 | break; 76 | case String: 77 | if( c == '\r' ) 78 | { 79 | state = StringLF; 80 | } 81 | else if( isChar(c) && !isControl(c) ) 82 | { 83 | buf.push_back(c); 84 | } 85 | else 86 | { 87 | std::stack().swap(states); 88 | return std::make_pair(position, Error); 89 | } 90 | break; 91 | case ErrorString: 92 | if( c == '\r' ) 93 | { 94 | state = ErrorLF; 95 | } 96 | else if( isChar(c) && !isControl(c) ) 97 | { 98 | buf.push_back(c); 99 | } 100 | else 101 | { 102 | std::stack().swap(states); 103 | return std::make_pair(position, Error); 104 | } 105 | break; 106 | case BulkSize: 107 | if( c == '\r' ) 108 | { 109 | if( buf.empty() ) 110 | { 111 | std::stack().swap(states); 112 | return std::make_pair(position, Error); 113 | } 114 | else 115 | { 116 | state = BulkSizeLF; 117 | } 118 | } 119 | else if( isdigit(c) || c == '-' ) 120 | { 121 | buf.push_back(c); 122 | } 123 | else 124 | { 125 | std::stack().swap(states); 126 | return std::make_pair(position, Error); 127 | } 128 | break; 129 | case StringLF: 130 | if( c == '\n') 131 | { 132 | state = Start; 133 | redisValue = RedisValue(buf); 134 | } 135 | else 136 | { 137 | std::stack().swap(states); 138 | return std::make_pair(position, Error); 139 | } 140 | break; 141 | case ErrorLF: 142 | if( c == '\n') 143 | { 144 | state = Start; 145 | RedisValue::ErrorTag tag; 146 | redisValue = RedisValue(buf, tag); 147 | } 148 | else 149 | { 150 | std::stack().swap(states); 151 | return std::make_pair(position, Error); 152 | } 153 | break; 154 | case BulkSizeLF: 155 | if( c == '\n' ) 156 | { 157 | bulkSize = bufToLong(buf.data(), buf.size()); 158 | buf.clear(); 159 | 160 | if( bulkSize == -1 ) 161 | { 162 | state = Start; 163 | redisValue = RedisValue(); // Nil 164 | } 165 | else if( bulkSize == 0 ) 166 | { 167 | state = BulkCR; 168 | } 169 | else if( bulkSize < 0 ) 170 | { 171 | std::stack().swap(states); 172 | return std::make_pair(position, Error); 173 | } 174 | else 175 | { 176 | buf.reserve(bulkSize); 177 | 178 | long int available = size - position; 179 | long int canRead = std::min(bulkSize, available); 180 | 181 | if( canRead > 0 ) 182 | { 183 | buf.assign(ptr + position, ptr + position + canRead); 184 | position += canRead; 185 | bulkSize -= canRead; 186 | } 187 | 188 | 189 | if (bulkSize > 0) 190 | { 191 | state = Bulk; 192 | } 193 | else 194 | { 195 | state = BulkCR; 196 | } 197 | } 198 | } 199 | else 200 | { 201 | std::stack().swap(states); 202 | return std::make_pair(position, Error); 203 | } 204 | break; 205 | case Bulk: { 206 | assert( bulkSize > 0 ); 207 | 208 | long int available = size - position + 1; 209 | long int canRead = std::min(available, bulkSize); 210 | 211 | buf.insert(buf.end(), ptr + position - 1, ptr + position - 1 + canRead); 212 | bulkSize -= canRead; 213 | position += canRead - 1; 214 | 215 | if( bulkSize == 0 ) 216 | { 217 | state = BulkCR; 218 | } 219 | break; 220 | } 221 | case BulkCR: 222 | if( c == '\r') 223 | { 224 | state = BulkLF; 225 | } 226 | else 227 | { 228 | std::stack().swap(states); 229 | return std::make_pair(position, Error); 230 | } 231 | break; 232 | case BulkLF: 233 | if( c == '\n') 234 | { 235 | state = Start; 236 | redisValue = RedisValue(buf); 237 | } 238 | else 239 | { 240 | std::stack().swap(states); 241 | return std::make_pair(position, Error); 242 | } 243 | break; 244 | case ArraySize: 245 | if( c == '\r' ) 246 | { 247 | if( buf.empty() ) 248 | { 249 | std::stack().swap(states); 250 | return std::make_pair(position, Error); 251 | } 252 | else 253 | { 254 | state = ArraySizeLF; 255 | } 256 | } 257 | else if( isdigit(c) || c == '-' ) 258 | { 259 | buf.push_back(c); 260 | } 261 | else 262 | { 263 | std::stack().swap(states); 264 | return std::make_pair(position, Error); 265 | } 266 | break; 267 | case ArraySizeLF: 268 | if( c == '\n' ) 269 | { 270 | int64_t arraySize = bufToLong(buf.data(), buf.size()); 271 | std::vector array; 272 | 273 | if( arraySize == -1 ) 274 | { 275 | state = Start; 276 | redisValue = RedisValue(); // Nil value 277 | } 278 | else if( arraySize == 0 ) 279 | { 280 | state = Start; 281 | redisValue = RedisValue(std::move(array)); // Empty array 282 | } 283 | else if( arraySize < 0 ) 284 | { 285 | std::stack().swap(states); 286 | return std::make_pair(position, Error); 287 | } 288 | else 289 | { 290 | array.reserve(arraySize); 291 | arraySizes.push(arraySize); 292 | arrayValues.push(std::move(array)); 293 | 294 | state = StartArray; 295 | } 296 | } 297 | else 298 | { 299 | std::stack().swap(states); 300 | return std::make_pair(position, Error); 301 | } 302 | break; 303 | case Integer: 304 | if( c == '\r' ) 305 | { 306 | if( buf.empty() ) 307 | { 308 | std::stack().swap(states); 309 | return std::make_pair(position, Error); 310 | } 311 | else 312 | { 313 | state = IntegerLF; 314 | } 315 | } 316 | else if( isdigit(c) || c == '-' ) 317 | { 318 | buf.push_back(c); 319 | } 320 | else 321 | { 322 | std::stack().swap(states); 323 | return std::make_pair(position, Error); 324 | } 325 | break; 326 | case IntegerLF: 327 | if( c == '\n' ) 328 | { 329 | int64_t value = bufToLong(buf.data(), buf.size()); 330 | 331 | buf.clear(); 332 | redisValue = RedisValue(value); 333 | state = Start; 334 | } 335 | else 336 | { 337 | std::stack().swap(states); 338 | return std::make_pair(position, Error); 339 | } 340 | break; 341 | default: 342 | std::stack().swap(states); 343 | return std::make_pair(position, Error); 344 | } 345 | 346 | 347 | if (state == Start) 348 | { 349 | if (!arraySizes.empty()) 350 | { 351 | assert(arraySizes.size() > 0); 352 | arrayValues.top().getArray().push_back(redisValue); 353 | 354 | while(!arraySizes.empty() && --arraySizes.top() == 0) 355 | { 356 | arraySizes.pop(); 357 | redisValue = std::move(arrayValues.top()); 358 | arrayValues.pop(); 359 | 360 | if (!arraySizes.empty()) 361 | arrayValues.top().getArray().push_back(redisValue); 362 | } 363 | } 364 | 365 | 366 | if (arraySizes.empty()) 367 | { 368 | // done 369 | break; 370 | } 371 | } 372 | } 373 | 374 | if (arraySizes.empty() && state == Start) 375 | { 376 | return std::make_pair(position, Completed); 377 | } 378 | else 379 | { 380 | states.push(state); 381 | return std::make_pair(position, Incompleted); 382 | } 383 | } 384 | 385 | RedisValue RedisParser::result() 386 | { 387 | return std::move(redisValue); 388 | } 389 | 390 | /* 391 | * Convert string to long. I can't use atol/strtol because it 392 | * work only with null terminated string. I can use temporary 393 | * std::string object but that is slower then bufToLong. 394 | */ 395 | long int RedisParser::bufToLong(const char *str, size_t size) 396 | { 397 | long int value = 0; 398 | bool sign = false; 399 | 400 | if( str == nullptr || size == 0 ) 401 | { 402 | return 0; 403 | } 404 | 405 | if( *str == '-' ) 406 | { 407 | sign = true; 408 | ++str; 409 | --size; 410 | 411 | if( size == 0 ) { 412 | return 0; 413 | } 414 | } 415 | 416 | for(const char *end = str + size; str != end; ++str) 417 | { 418 | char c = *str; 419 | 420 | // char must be valid, already checked in the parser 421 | assert(c >= '0' && c <= '9'); 422 | 423 | value = value * 10; 424 | value += c - '0'; 425 | } 426 | 427 | return sign ? -value : value; 428 | } 429 | 430 | } 431 | 432 | #endif // REDISCLIENT_REDISPARSER_CPP 433 | -------------------------------------------------------------------------------- /parser/redisparser.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Alex Nekipelov (alex@nekipelov.net) 3 | * License: MIT 4 | */ 5 | 6 | #ifndef REDISCLIENT_REDISPARSER_H 7 | #define REDISCLIENT_REDISPARSER_H 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "redisvalue.h" 14 | #include "config.h" 15 | 16 | namespace purecpp { 17 | 18 | class RedisParser 19 | { 20 | public: 21 | REDIS_CLIENT_DECL RedisParser(); 22 | 23 | enum ParseResult { 24 | Completed, 25 | Incompleted, 26 | Error, 27 | }; 28 | 29 | REDIS_CLIENT_DECL std::pair parse(const char *ptr, size_t size); 30 | 31 | REDIS_CLIENT_DECL RedisValue result(); 32 | 33 | protected: 34 | REDIS_CLIENT_DECL std::pair parseChunk(const char *ptr, size_t size); 35 | 36 | inline bool isChar(int c) 37 | { 38 | return c >= 0 && c <= 127; 39 | } 40 | 41 | inline bool isControl(int c) 42 | { 43 | return (c >= 0 && c <= 31) || (c == 127); 44 | } 45 | 46 | REDIS_CLIENT_DECL long int bufToLong(const char *str, size_t size); 47 | 48 | private: 49 | enum State { 50 | Start = 0, 51 | StartArray = 1, 52 | 53 | String = 2, 54 | StringLF = 3, 55 | 56 | ErrorString = 4, 57 | ErrorLF = 5, 58 | 59 | Integer = 6, 60 | IntegerLF = 7, 61 | 62 | BulkSize = 8, 63 | BulkSizeLF = 9, 64 | Bulk = 10, 65 | BulkCR = 11, 66 | BulkLF = 12, 67 | 68 | ArraySize = 13, 69 | ArraySizeLF = 14, 70 | }; 71 | 72 | std::stack states; 73 | 74 | long int bulkSize; 75 | std::vector buf; 76 | RedisValue redisValue; 77 | 78 | // temporary variables 79 | std::stack arraySizes; 80 | std::stack arrayValues; 81 | 82 | static const char stringReply = '+'; 83 | static const char errorReply = '-'; 84 | static const char integerReply = ':'; 85 | static const char bulkReply = '$'; 86 | static const char arrayReply = '*'; 87 | }; 88 | 89 | } 90 | 91 | #ifdef REDIS_CLIENT_HEADER_ONLY 92 | #include "redisparser.cpp" 93 | #endif 94 | 95 | #endif // REDISCLIENT_REDISPARSER_H 96 | -------------------------------------------------------------------------------- /parser/redisvalue.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Alex Nekipelov (alex@nekipelov.net) 3 | * License: MIT 4 | */ 5 | 6 | #ifndef REDISCLIENT_REDISVALUE_H 7 | #define REDISCLIENT_REDISVALUE_H 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "config.h" 14 | 15 | namespace purecpp { 16 | 17 | class RedisValue { 18 | public: 19 | struct ErrorTag { 20 | }; 21 | 22 | REDIS_CLIENT_DECL RedisValue() 23 | : value_(NullTag()), error_(false) { 24 | } 25 | 26 | REDIS_CLIENT_DECL RedisValue(RedisValue &&other) 27 | : value_(std::move(other.value_)), error_(other.error_), error_code_(other.error_code_) { 28 | } 29 | 30 | REDIS_CLIENT_DECL RedisValue(int64_t i) 31 | : value_(i), error_(false) { 32 | } 33 | 34 | REDIS_CLIENT_DECL RedisValue(const char *s) 35 | : value_(std::vector(s, s + strlen(s))), error_(false) { 36 | } 37 | 38 | REDIS_CLIENT_DECL RedisValue(const std::string &s) 39 | : value_(std::vector(s.begin(), s.end())), error_(false) { 40 | } 41 | 42 | REDIS_CLIENT_DECL RedisValue(std::vector buf) 43 | : value_(std::move(buf)), error_(false) { 44 | } 45 | 46 | REDIS_CLIENT_DECL RedisValue(std::vector buf, struct ErrorTag) 47 | : value_(std::move(buf)), error_(true) { 48 | } 49 | 50 | REDIS_CLIENT_DECL RedisValue(std::vector array) 51 | : value_(std::move(array)), error_(false) { 52 | } 53 | 54 | RedisValue(int error_code, const std::string &error_msg) 55 | : value_(std::vector(error_msg.begin(), error_msg.end())), error_code_(error_code) { 56 | error_ = (error_code_!=0); 57 | } 58 | 59 | RedisValue(const RedisValue &) = default; 60 | 61 | RedisValue &operator=(const RedisValue &) = default; 62 | 63 | RedisValue &operator=(RedisValue &&) = default; 64 | 65 | // Return the value as a std::string if 66 | // type is a byte string; otherwise returns an empty std::string. 67 | REDIS_CLIENT_DECL std::string toString() const { 68 | const std::vector &buf = toByteArray(); 69 | return std::string(buf.begin(), buf.end()); 70 | } 71 | 72 | // Return the value as a std::vector if 73 | // type is a byte string; otherwise returns an empty std::vector. 74 | REDIS_CLIENT_DECL std::vector toByteArray() const { 75 | return castTo >(); 76 | } 77 | 78 | // Return the value as a std::vector if 79 | // type is an int; otherwise returns 0. 80 | REDIS_CLIENT_DECL int64_t toInt() const { 81 | return castTo(); 82 | } 83 | 84 | // Return the value as an array if type is an array; 85 | // otherwise returns an empty array. 86 | REDIS_CLIENT_DECL std::vector toArray() const { 87 | return castTo >(); 88 | } 89 | 90 | // Return the string representation of the value. Use 91 | // for dump content of the value. 92 | REDIS_CLIENT_DECL std::string inspect() const { 93 | if (isError()) { 94 | static std::string err = "error: "; 95 | std::string result; 96 | 97 | result = err; 98 | result += toString(); 99 | 100 | return result; 101 | } else if (isNull()) { 102 | static std::string null = "(null)"; 103 | return null; 104 | } else if (isInt()) { 105 | return std::to_string(toInt()); 106 | } else if (isString()) { 107 | return toString(); 108 | } else { 109 | std::vector values = toArray(); 110 | std::string result = "["; 111 | 112 | if (values.empty() == false) { 113 | for (size_t i = 0; i < values.size(); ++i) { 114 | result += values[i].inspect(); 115 | result += ", "; 116 | } 117 | 118 | result.resize(result.size() - 1); 119 | result[result.size() - 1] = ']'; 120 | } else { 121 | result += ']'; 122 | } 123 | 124 | return result; 125 | } 126 | } 127 | 128 | // Return true if value not a error_ 129 | REDIS_CLIENT_DECL bool isOk() const { 130 | return !isError(); 131 | } 132 | // Return true if value is a error_ 133 | REDIS_CLIENT_DECL bool isError() const { 134 | return error_; 135 | } 136 | 137 | bool IsIOError() const { 138 | return error_code_ == 1; 139 | } 140 | 141 | int ErrorCode() { 142 | return error_code_; 143 | } 144 | 145 | // Return true if this is a null. 146 | REDIS_CLIENT_DECL bool isNull() const { 147 | return typeEq(); 148 | } 149 | // Return true if type is an int 150 | REDIS_CLIENT_DECL bool isInt() const { 151 | return typeEq(); 152 | } 153 | // Return true if type is an array 154 | REDIS_CLIENT_DECL bool isArray() const { 155 | return typeEq >(); 156 | } 157 | // Return true if type is a string/byte array. Alias for isString(); 158 | REDIS_CLIENT_DECL bool isByteArray() const { 159 | return typeEq >(); 160 | } 161 | // Return true if type is a string/byte array. Alias for isByteArray(). 162 | REDIS_CLIENT_DECL bool isString() const { 163 | return typeEq >(); 164 | } 165 | 166 | // Methods for increasing perfomance 167 | // Throws: boost::bad_get if the type does not match 168 | REDIS_CLIENT_DECL std::vector &getByteArray() { 169 | assert(isByteArray()); 170 | return boost::get>(value_); 171 | } 172 | 173 | REDIS_CLIENT_DECL const std::vector &getByteArray() const { 174 | assert(isByteArray()); 175 | return boost::get>(value_); 176 | } 177 | 178 | REDIS_CLIENT_DECL std::vector &getArray() { 179 | assert(isArray()); 180 | return boost::get>(value_); 181 | } 182 | 183 | REDIS_CLIENT_DECL const std::vector &getArray() const { 184 | assert(isArray()); 185 | return boost::get>(value_); 186 | } 187 | 188 | 189 | REDIS_CLIENT_DECL bool operator==(const RedisValue &rhs) const { 190 | return value_ == rhs.value_; 191 | } 192 | 193 | REDIS_CLIENT_DECL bool operator!=(const RedisValue &rhs) const { 194 | return !(value_ == rhs.value_); 195 | } 196 | 197 | protected: 198 | template 199 | T castTo() const; 200 | 201 | template 202 | bool typeEq() const; 203 | 204 | private: 205 | struct NullTag { 206 | inline bool operator==(const NullTag &) const { 207 | return true; 208 | } 209 | }; 210 | 211 | boost::variant, std::vector > value_; 212 | bool error_; 213 | int error_code_ = 0; 214 | }; 215 | 216 | 217 | template 218 | T RedisValue::castTo() const { 219 | if (value_.type() == typeid(T)) 220 | return boost::get(value_); 221 | else 222 | return T(); 223 | } 224 | 225 | template 226 | bool RedisValue::typeEq() const { 227 | if (value_.type() == typeid(T)) 228 | return true; 229 | else 230 | return false; 231 | } 232 | 233 | } 234 | 235 | #endif // REDISCLIENT_REDISVALUE_H 236 | --------------------------------------------------------------------------------