├── CMakeLists.txt ├── README.md ├── RESPParser.cpp ├── RESPParser.h ├── cmds.cpp ├── cmds.h ├── common.cpp ├── common.h ├── config.cpp ├── config.h ├── config.json ├── redisstore.cpp ├── redisstore.h ├── server.cpp ├── state.json ├── type.cpp └── type.h /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(redis) 3 | 4 | # Add executable 5 | add_executable(redis server.cpp 6 | type.cpp 7 | RESPParser.cpp 8 | redisstore.cpp 9 | common.cpp 10 | cmds.cpp 11 | config.cpp) 12 | 13 | # for debug info 14 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g") 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | myRedis 2 | ------- 3 | 4 | This is my implementation of popular in-memory key-value database [Redis](https://github.com/redis/redis) from scratch in C++. (referred to as myRedis in rest of this doc) 5 | 6 | myRedis supports the same operations as the actual version of Redis at launch in 2009. The list of commands supported are: 7 | 8 | - [SET](https://redis.io/docs/latest/commands/set/) 9 | - [GET](https://redis.io/docs/latest/commands/get/) 10 | - [EXISTS](https://redis.io/docs/latest/commands/exists/) 11 | - [DEL](https://redis.io/docs/latest/commands/del/) 12 | - [INCR](https://redis.io/docs/latest/commands/incr/) 13 | - [DECR](https://redis.io/docs/latest/commands/decr/) 14 | - [LPUSH](https://redis.io/docs/latest/commands/lpush/) 15 | - [RPUSH](https://redis.io/docs/latest/commands/rpush/) 16 | - [LRANGE](https://redis.io/docs/latest/commands/lrange/) 17 | - [SAVE](https://redis.io/docs/latest/commands/save/) 18 | 19 | The objective of this project is just to build a functional key-value database application from scratch. myRedis is not written with an aim to overperform official redis in some metric, but rather to deeply understand how Redis and server applications in C++ works. 20 | 21 | Performance 22 | ----------- 23 | 24 | Here is a benchmark of how myRedis performs: 25 | 26 | ``` 27 | redis-benchmark -p 2000 -t set,get, -n 100000 -q 28 | SET: 207468.88 requests per second 29 | GET: 213675.22 requests per second 30 | ``` 31 | 32 | For comparison here is how current official version of redis performs: 33 | ``` 34 | redis-benchmark -t set,get, -n 100000 -q 35 | SET: 222222.23 requests per second 36 | GET: 222717.16 requests per second 37 | ``` 38 | 39 | Building myRedis 40 | ---------------- 41 | 42 | Let's say the path you cloned this repo is `$REDIS_HOME`. Steps to build myRedis: 43 | ``` 44 | $export REDIS_HOME=/path/to/your/clone 45 | $cd $REDIS_HOME 46 | $mkdir ${REDIS_HOME}/bin 47 | $cd bin 48 | $cmake $REDIS_HOME 49 | $make 50 | ``` 51 | 52 | This will create the executable named `redis` in `${REDIS_HOME}\bin` directory. 53 | 54 | Running myRedis 55 | ---------------- 56 | 57 | To run myRedis, just do: 58 | ``` 59 | $cd $REDIS_HOME 60 | $bin/redis 61 | ``` 62 | 63 | This will start a myRedis server at port 2000 (You can change that in `config.json` file). 64 | 65 | You should see an output like below if you are running this for the first time: 66 | ``` 67 | $bin/redis 68 | State restoral failed! Continuing with empty state... 69 | Server listening on port: 2000 70 | ``` 71 | 72 | (More on state restoral below) 73 | 74 | Note that you must run this executable from your `$REDIS_HOME` directory, as myRedis assumes that a 75 | `config.json` file is present in the directory the executable is being run in. 76 | 77 | Talking to myRedis 78 | ------------------ 79 | 80 | You can talk to myRedis using official [redis-cli](https://redis.io/docs/latest/develop/connect/cli/)! In a different terminal you can try the following: 81 | ``` 82 | $redis-cli -p 2000 83 | 127.0.0.1:2000> ping 84 | PONG 85 | 127.0.0.1:2000> echo "this is myRedis" 86 | this is myRedis 87 | 127.0.0.1:2000> set name1 ram 88 | OK 89 | 127.0.0.1:2000> get name1 90 | ram 91 | 127.0.0.1:2000> rpush statement myRedis looks interesting! 92 | (integer) 3 93 | 127.0.0.1:2000> lrange statement 0 -1 94 | 1) "myRedis" 95 | 2) "looks" 96 | 3) "interesting!" 97 | 127.0.0.1:2000> save 98 | OK 99 | ``` 100 | 101 | Key choices of myRedis 102 | ---------------------- 103 | There are few ways myRedis is different from official implementation. 104 | 105 | - myRedis uses one thread per client connection, as opposed to single threaded event loop architecture of official redis 106 | - main reason to choose this was ease of implementation 107 | - working with async io code in c++ seems to be quite messy 108 | - myRedis being multi-threaded implements necessary locking to protect data race 109 | - myRedis supports persistence to disk by dumping snapshot of myRedis data into a `state.json` file in `$REDIS_HOME`. This is unlike official redis which uses binary `rdb` file. If there exists a `state.json` file in `$REDIS_HOME` dir from its last run, myRedis will load this file at startup, otherwise will run with an empty state. 110 | - chose `json` just because it is human readable, and I wanted to see changes to data in action on running `save` redis cmd 111 | - at some point in future, I may consider moving this to a binary format like `protobuf` or `rdb` itself 112 | - myRedis supports to two configs via `config.json` file in `$REDIS_HOME` dir: 113 | - `port`: the port at which you want your myRedis server to run 114 | - `snapshot_period`: the time period (in minutes) of periodic snapshot of myRedis' in-memory state 115 | 116 | 117 | Source code layout 118 | === 119 | 120 | Following are the important files: 121 | 122 | CMakeLists.txt 123 | -------------- 124 | Contains information on how to build myRedis executable. 125 | 126 | server.cpp 127 | ---------- 128 | This is the main file, which starts up the server and launches a new thread for every client connection. This also creates the snapshot thread which periodically wakes up after a fixed time interval to dump myRedis' state. 129 | 130 | RESPParser.cpp (.h) 131 | ------------------- 132 | Redis uses [RESP](https://redis.io/docs/latest/develop/reference/protocol-spec/) protocol to exchange messages between server and client. 133 | 134 | Each client request is an [array](https://redis.io/docs/latest/develop/reference/protocol-spec/#arrays) of [bulk strings](https://redis.io/docs/latest/develop/reference/protocol-spec/#bulk-strings). This file implements the deserialization logic of the client request including the logic to read from client `fd`. 135 | 136 | Since RESP uses `\r\n` (CRLF) to separate two meaningful items, a RESP parser would have to process the serialized message item by item. Instead of reading item by item from socket, myRedis has a read cache of `8192` bytes which prevents parser from making large number of expensive `read` syscalls. 137 | 138 | Each client thread have a RESPParser object which exposes `read_new_request()` method. 139 | 140 | redisstore.cpp (.h) 141 | ------------------- 142 | 143 | This file implements the data structures which house all the data stored in myRedis. RedisStore is a singleton class which exposes relevant methods required by redis cmds. 144 | 145 | cmds.cpp (.h) 146 | ------------- 147 | 148 | This file houses the implementation of redis cmds. These are essentially a thin wrapper to do validation of redis cmds before calling actual methods of RedisStore 149 | 150 | type.cpp (.h) 151 | ------------- 152 | 153 | To send a reply to redis-client, redis-server also needs to serialize output as per RESP. There are multiple [data types](https://redis.io/docs/latest/develop/reference/protocol-spec/#resp-protocol-description) supported in RESP each of which have there own serialization logic. 154 | 155 | This file defines a base class `RObject` from which all fancy types (string, error, integers, bulk string, array) inherit. Each sub-class needs to define it's own `serialize()` method as per RESP. 156 | 157 | Arguably some part of RESPParser should have utilized object definitions here, and it might have been nicer to have serialization and deserialization logic in one place. However RESPParser had much more nuances because of validations required before successful deserialization. This is why myRedis has opted to keep them separate. 158 | 159 | config.cpp (.h) 160 | --------------- 161 | 162 | Enables reading up config from the `config.json` file. 163 | 164 | Improvements 165 | ------------ 166 | 167 | - Limit number of client connections or explore thread pool architecture 168 | - Use binary protocol to dump in-memory state 169 | - support passing config as an argument 170 | - explore unique data structures optimization in official redis 171 | 172 | 173 | -------------------------------------------------------------------------------- /RESPParser.cpp: -------------------------------------------------------------------------------- 1 | #include "RESPParser.h" 2 | 3 | 4 | std::string RESPParser::read_from_fd(int n_bytes) { 5 | char buf[n_bytes]; 6 | ssize_t bytesRead = recv(_read_fd, buf, n_bytes, 0); 7 | if (bytesRead <= 0) { 8 | throw SysCallFailure("recv failed!"); //error 9 | } 10 | return std::string(buf, bytesRead); 11 | } 12 | 13 | /** 14 | * If there is a valid (CRLF terminated) item in cache, 15 | * populate 'item' with it and return true. 16 | * 17 | * Returns false otherwise. 18 | */ 19 | bool RESPParser::_cache_has_valid_item(std::string& item) { 20 | 21 | for (int i = 0; i < _read_cache.length(); i++) { 22 | 23 | item += _read_cache[i]; // this is probably bad 24 | // as appending char by char 25 | // must be expensive due to re-allocations 26 | 27 | if(item.length() >= 2 && 28 | item[item.length()-2] == '\r' && 29 | item[item.length()-1] == '\n') { 30 | 31 | // Remove the part read 32 | _read_cache = _read_cache.substr(i+1); 33 | return true; 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | void RESPParser::_update_cache() { 40 | _read_cache = read_from_fd(READ_CACHE_MAX); 41 | } 42 | 43 | std::string RESPParser::read_next_item() { 44 | 45 | std::string item = ""; 46 | 47 | while(!_cache_has_valid_item(item)) { 48 | if (item.length() > ITEM_LEN_MAX) { 49 | throw IncorrectProtocol("item length too big!"); 50 | } 51 | _update_cache(); 52 | } 53 | 54 | return item; 55 | } 56 | 57 | /** 58 | * size_item must look like: 59 | * "*\r\n" 60 | */ 61 | bool RESPParser::_validate_array_size(const std::string& size_item) { 62 | 63 | int len = size_item.length(); 64 | // Must be atleast 4 characters 65 | if (len < 4) { 66 | return false; 67 | } 68 | // Must begin with * 69 | if (size_item[0] != '*') { 70 | return false; 71 | } 72 | // Must end with \r\n 73 | if (size_item[len-1] != '\n' || size_item[len-2] != '\r') { 74 | return false; 75 | } 76 | // Rest should be a number 77 | for (int i = 1; i <= len-3; i++) { 78 | if (size_item[i] < '0' || size_item[i] > '9') { 79 | return false; 80 | } 81 | } 82 | // valid 83 | return true; 84 | } 85 | 86 | /** 87 | * size_item must look like: 88 | * $\r\n 89 | */ 90 | bool RESPParser::_validate_bstr_size(const std::string& size_item) { 91 | 92 | int len = size_item.length(); 93 | // Must be atleast 4 characters 94 | if (len < 4) { 95 | return false; 96 | } 97 | // Must begin with $ 98 | if (size_item[0] != '$') { 99 | return false; 100 | } 101 | // Must end with \r\n 102 | if (size_item[len-1] != '\n' || size_item[len-2] != '\r') { 103 | return false; 104 | } 105 | // Rest should be a number 106 | for (int i = 1; i <= len-3; i++) { 107 | if (size_item[i] < '0' || size_item[i] > '9') { 108 | return false; 109 | } 110 | } 111 | // valid 112 | return true; 113 | } 114 | 115 | 116 | /** 117 | * Returns true is bstr is terminated with CRLF 118 | */ 119 | bool RESPParser::_validate_crlf(const std::string& bstr) { 120 | 121 | int len = bstr.length(); 122 | if (len < 2) { 123 | return false; 124 | } 125 | return bstr[len-2] == '\r' && bstr[len-1] == '\n'; 126 | 127 | } 128 | 129 | std::vector RESPParser::read_new_request(){ 130 | 131 | // Assumes each RESP request is an array of bulk strings 132 | 133 | // Read size of array in the request 134 | std::string arr_size_item = read_next_item(); 135 | if (!_validate_array_size(arr_size_item)){ 136 | throw IncorrectProtocol("Bad array size"); 137 | } 138 | int size = std::stoi(arr_size_item.substr(1, arr_size_item.length()-3)); 139 | 140 | std::vector req(size); 141 | 142 | // Read bulk strings 143 | for (int i = 0; i < size; i++) { 144 | 145 | // Read size of the bulk string 146 | std::string bstr_size_item = read_next_item(); 147 | if (!_validate_bstr_size(bstr_size_item)){ 148 | throw IncorrectProtocol("Bad bulk string size"); 149 | } 150 | int bstr_size = std::stoi(bstr_size_item.substr(1, bstr_size_item.length()-3)); 151 | 152 | if (bstr_size == -1) { 153 | // null bulk string 154 | req[i] = NULL_BULK_STRING; 155 | continue; 156 | } 157 | if (bstr_size < -1) { 158 | throw IncorrectProtocol("Bulk string size < -1"); 159 | } 160 | 161 | // Read the bulk string 162 | std::string bstr_item = read_next_item(); 163 | if (!_validate_crlf(bstr_item)) { 164 | throw IncorrectProtocol("Bulk string not terminated by CRLF"); 165 | } 166 | 167 | std::string bstr = bstr_item.substr(0, bstr_item.length()-2); 168 | if (bstr.length() != bstr_size) { 169 | throw IncorrectProtocol("Bulk string size does not match"); 170 | } 171 | 172 | req[i] = bstr; 173 | } 174 | 175 | return req; 176 | } 177 | -------------------------------------------------------------------------------- /RESPParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "common.h" 3 | 4 | #define NULL_BULK_STRING "NULL" 5 | #define READ_CACHE_MAX 8192 6 | #define ITEM_LEN_MAX 536870912 // 512 MB 7 | 8 | class RESPParser { 9 | 10 | private: 11 | int _read_fd = -1; 12 | std::string _read_cache = ""; 13 | 14 | 15 | protected: 16 | bool _validate_array_size(const std::string& size_item); 17 | bool _validate_bstr_size(const std::string& size_item); 18 | bool _validate_crlf(const std::string& bstr); 19 | bool _cache_has_valid_item(std::string& item); 20 | void _update_cache(); 21 | 22 | /** 23 | * Read upto n_bytes from fd 24 | */ 25 | std::string read_from_fd(int n_bytes); 26 | 27 | /** 28 | * Returns next item on _read_fd 29 | * An item terminates in /r/n 30 | */ 31 | std::string read_next_item(); 32 | 33 | 34 | public: 35 | 36 | RESPParser(int fd) { 37 | _read_fd = fd; 38 | _read_cache = ""; 39 | } 40 | 41 | /** 42 | * Read a new request from a _read_fd 43 | * Returns the request as a vector of string 44 | * with first element being command name 45 | */ 46 | std::vector read_new_request(); 47 | }; 48 | -------------------------------------------------------------------------------- /cmds.cpp: -------------------------------------------------------------------------------- 1 | #include "cmds.h" 2 | #include "redisstore.h" 3 | 4 | std::unordered_map cmd_map = { 5 | {"ping", redis_ping}, 6 | {"echo", redis_echo}, 7 | {"set", redis_set}, 8 | {"get", redis_get}, 9 | {"exists", redis_exists}, 10 | {"del", redis_del}, 11 | {"incr", redis_incr}, 12 | {"decr", redis_decr}, 13 | {"lpush", redis_lpush}, 14 | {"rpush", redis_rpush}, 15 | {"lrange", redis_lrange}, 16 | {"save", redis_save}, 17 | {"config", redis_config},}; 18 | 19 | 20 | redis_func_type get_redis_func(const std::string& cmd_name) { 21 | 22 | auto it = cmd_map.find(cmd_name); 23 | if (it != nullptr) { 24 | return it->second; 25 | } 26 | // Should not reach here 27 | throw RedisServerError(cmd_name + " not found!"); 28 | } 29 | 30 | 31 | cmd_ret_type redis_ping(const std::vector& req) { 32 | 33 | if (req.size() == 0 || req[0] != "ping") { 34 | throw RedisServerError("Bad input"); 35 | } 36 | 37 | if (req.size() > 2) { 38 | return std::make_unique 39 | ("ERR wrong number of arguments for 'ping' command"); 40 | } 41 | if (req.size() == 1) { 42 | // no arguments provided, return simple string 43 | return std::make_unique("PONG"); 44 | } 45 | 46 | // return a bulk string of the argument if any 47 | return std::make_unique(req[1]); 48 | } 49 | 50 | cmd_ret_type redis_echo(const std::vector& req) { 51 | 52 | if (req.size() == 0 || req[0] != "echo") { 53 | throw RedisServerError("Bad input"); 54 | } 55 | 56 | if (req.size() != 2) { 57 | return std::make_unique 58 | ("ERR wrong number of arguments for 'echo' command"); 59 | } 60 | return std::make_unique(req[1]); 61 | } 62 | 63 | 64 | cmd_ret_type redis_set(const std::vector& req) { 65 | 66 | if (req.size() == 0 || req[0] != "set") { 67 | throw RedisServerError("Bad input"); 68 | } 69 | 70 | std::time_t expiry_epoch = LONG_MAX; 71 | int i = 3; 72 | if (req.size() < 3) { 73 | goto syntax_error; 74 | } 75 | 76 | while(i < req.size()) { 77 | if (to_lower(req[i]) == "ex") { 78 | i++; 79 | if (i >= req.size()) { 80 | goto syntax_error; 81 | } 82 | std::time_t expiry_from_now; 83 | try { 84 | expiry_from_now = std::stol(req[i]); 85 | } catch (const std::exception& e) { 86 | goto out_of_range; 87 | } 88 | auto now = std::chrono::system_clock::now(); 89 | auto expiry = now + std::chrono::seconds(expiry_from_now); 90 | expiry_epoch = std::chrono::system_clock::to_time_t(expiry); 91 | 92 | } else if (to_lower(req[i]) == "px") { 93 | i++; 94 | if (i >= req.size()) { 95 | goto syntax_error; 96 | } 97 | std::time_t expiry_from_now; 98 | try { 99 | expiry_from_now = std::stol(req[i])/1000; 100 | } catch (const std::exception& e) { 101 | goto out_of_range; 102 | } 103 | auto now = std::chrono::system_clock::now(); 104 | auto expiry = now + std::chrono::seconds(expiry_from_now); 105 | expiry_epoch = std::chrono::system_clock::to_time_t(expiry); 106 | 107 | } else if (to_lower(req[i]) == "exat") { 108 | i++; 109 | if (i >= req.size()) { 110 | goto syntax_error; 111 | } 112 | try { 113 | expiry_epoch = std::stol(req[i]); 114 | } catch (const std::exception& e) { 115 | goto out_of_range; 116 | } 117 | } else if (to_lower(req[i]) == "pxat") { 118 | i++; 119 | if (i >= req.size()) { 120 | goto syntax_error; 121 | } 122 | try { 123 | expiry_epoch = std::stol(req[i])/1000; 124 | } catch (const std::exception& e) { 125 | goto out_of_range; 126 | } 127 | } else { 128 | goto syntax_error; 129 | } 130 | ++i; 131 | } 132 | 133 | RedisStore::getInstance().set(req[1], req[2], expiry_epoch); 134 | return std::make_unique("OK"); 135 | 136 | syntax_error: 137 | return std::make_unique("ERR syntax error"); 138 | 139 | out_of_range: 140 | return std::make_unique("ERR value is not an integer or out of range"); 141 | 142 | } 143 | 144 | cmd_ret_type redis_get(const std::vector& req) { 145 | 146 | if (req.size() == 0 || req[0] != "get") { 147 | throw RedisServerError("Bad input"); 148 | } 149 | 150 | if (req.size() != 2) { 151 | return std::make_unique 152 | ("ERR wrong number of arguments for 'get' command"); 153 | } 154 | std::string output; 155 | bool found = RedisStore::getInstance().get(req[1], output); 156 | 157 | if (!found) { 158 | return std::make_unique(); 159 | } 160 | return std::make_unique(output); 161 | } 162 | 163 | cmd_ret_type redis_exists(const std::vector& req) { 164 | 165 | if (req.size() == 0 || req[0] != "exists") { 166 | throw RedisServerError("Bad input"); 167 | } 168 | 169 | int count = 0; 170 | int i = 1; 171 | 172 | if (req.size() < 2) { 173 | return std::make_unique("ERR syntax error"); 174 | } 175 | 176 | while(i < req.size()) { 177 | if (RedisStore::getInstance().exists(req[i])) { 178 | count++; 179 | } 180 | i++; 181 | } 182 | return std::make_unique(count); 183 | } 184 | 185 | 186 | cmd_ret_type redis_del(const std::vector& req) { 187 | 188 | if (req.size() == 0 || req[0] != "del") { 189 | throw RedisServerError("Bad input"); 190 | } 191 | 192 | int count = 0; 193 | int i = 1; 194 | 195 | if (req.size() < 2) { 196 | return std::make_unique("ERR syntax error"); 197 | } 198 | 199 | while(i < req.size()) { 200 | count += RedisStore::getInstance().erase(req[i]); 201 | i++; 202 | } 203 | return std::make_unique(count); 204 | 205 | } 206 | 207 | cmd_ret_type redis_incr(const std::vector& req) { 208 | 209 | if (req.size() == 0 || req[0] != "incr") { 210 | throw RedisServerError("Bad input"); 211 | } 212 | 213 | if (req.size() != 2) { 214 | return std::make_unique("ERR syntax error"); 215 | } 216 | 217 | try{ 218 | int64_t res = RedisStore::getInstance().incr(req[1]); 219 | return std::make_unique(res); 220 | } catch (const std::exception& e) { 221 | goto out_of_range; 222 | } 223 | 224 | out_of_range: 225 | return std::make_unique("ERR value is not an integer or out of range"); 226 | } 227 | 228 | cmd_ret_type redis_decr(const std::vector& req) { 229 | 230 | if (req.size() == 0 || req[0] != "decr") { 231 | throw RedisServerError("Bad input"); 232 | } 233 | 234 | if (req.size() != 2) { 235 | return std::make_unique("ERR syntax error"); 236 | } 237 | 238 | try{ 239 | int64_t res = RedisStore::getInstance().incr(req[1], true); 240 | return std::make_unique(res); 241 | } catch (const std::exception& e) { 242 | goto out_of_range; 243 | } 244 | 245 | out_of_range: 246 | return std::make_unique("ERR value is not an integer or out of range"); 247 | } 248 | 249 | cmd_ret_type redis_lpush(const std::vector& req) { 250 | 251 | if (req.size() == 0 || req[0] != "lpush") { 252 | throw RedisServerError("Bad input"); 253 | } 254 | 255 | if (req.size() < 3) { 256 | return std::make_unique 257 | ("ERR wrong number of arguments for 'lpush' command"); 258 | } 259 | 260 | int i = 2; 261 | std::vector vals; 262 | while (i < req.size()) { 263 | vals.push_back(req[i]); 264 | i++; 265 | } 266 | int res = RedisStore::getInstance().lpush(req[1], vals); 267 | return std::make_unique(res); 268 | } 269 | 270 | cmd_ret_type redis_rpush(const std::vector& req) { 271 | 272 | if (req.size() == 0 || req[0] != "rpush") { 273 | throw RedisServerError("Bad input"); 274 | } 275 | 276 | if (req.size() < 3) { 277 | return std::make_unique 278 | ("ERR wrong number of arguments for 'rpush' command"); 279 | } 280 | 281 | int i = 2; 282 | std::vector vals; 283 | while (i < req.size()) { 284 | vals.push_back(req[i]); 285 | i++; 286 | } 287 | int res = RedisStore::getInstance().lpush(req[1], vals, true); 288 | return std::make_unique(res); 289 | } 290 | 291 | cmd_ret_type redis_lrange(const std::vector& req) { 292 | 293 | if (req.size() == 0 || req[0] != "lrange") { 294 | throw RedisServerError("Bad input"); 295 | } 296 | 297 | if (req.size() != 4) { 298 | return std::make_unique 299 | ("ERR wrong number of arguments for 'lrange' command"); 300 | } 301 | 302 | int64_t start; 303 | int64_t end; 304 | std::vector res; 305 | std::unique_ptr arr = std::make_unique(); 306 | try{ 307 | start = std::stoll(req[2]); 308 | end = std::stoll(req[3]); 309 | } catch (const std::exception& e) { 310 | goto out_of_range; 311 | } 312 | 313 | res = RedisStore::getInstance().lrange(req[1], start, end); 314 | 315 | for (auto str : res) { 316 | arr->add_element(std::make_unique(str)); 317 | } 318 | 319 | return std::move(arr); 320 | 321 | out_of_range: 322 | return std::make_unique("ERR value is not an integer or out of range"); 323 | 324 | } 325 | 326 | 327 | cmd_ret_type redis_save(const std::vector& req) { 328 | 329 | if (req.size() == 0 || req[0] != "save") { 330 | throw RedisServerError("Bad input"); 331 | } 332 | 333 | if (req.size() > 1) { 334 | return std::make_unique 335 | ("ERR wrong number of arguments for 'save' command"); 336 | } 337 | 338 | if (RedisStore::getInstance().dump()) { 339 | return std::make_unique("OK"); 340 | } 341 | return std::make_unique("Could not save!, make sure statefile path exists!"); 342 | } 343 | 344 | 345 | cmd_ret_type redis_config_get(const std::vector& req) { 346 | 347 | // this is just hardcoded at the moment 348 | // to make redis-benchmark usable 349 | 350 | std::unique_ptr arr = std::make_unique(); 351 | arr->add_element(std::make_unique("900")); 352 | arr->add_element(std::make_unique("1")); 353 | return arr; // move not required because of RVO 354 | } 355 | 356 | cmd_ret_type redis_config(const std::vector& req) { 357 | 358 | if (req.size() == 0 || req[0] != "config") { 359 | throw RedisServerError("Bad input"); 360 | } 361 | return redis_config_get(req); 362 | } 363 | -------------------------------------------------------------------------------- /cmds.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "common.h" 4 | #include "type.h" 5 | 6 | using cmd_ret_type = std::unique_ptr; 7 | 8 | #define REDIS_CMD(NAME) cmd_ret_type redis_##NAME(const std::vector& req); 9 | 10 | using redis_func_type = std::function&)>; 11 | 12 | REDIS_CMD(ping) 13 | REDIS_CMD(echo) 14 | REDIS_CMD(set) 15 | REDIS_CMD(get) 16 | REDIS_CMD(exists) 17 | REDIS_CMD(del) 18 | REDIS_CMD(incr) 19 | REDIS_CMD(decr) 20 | REDIS_CMD(lpush) 21 | REDIS_CMD(rpush) 22 | REDIS_CMD(lrange) 23 | REDIS_CMD(save) 24 | REDIS_CMD(config) 25 | 26 | 27 | redis_func_type get_redis_func(const std::string& cmd_name); 28 | -------------------------------------------------------------------------------- /common.cpp: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | 4 | void die(const char* msg) { 5 | perror(msg); 6 | exit(EXIT_FAILURE); 7 | } 8 | 9 | std::string to_lower(const std::string& str) { 10 | std::string result = str; 11 | std::transform(result.begin(), result.end(), result.begin(), [](unsigned char c){ return std::tolower(c); }); 12 | return result; 13 | } 14 | 15 | int recv_exactly(int fd, char* buf, size_t n_bytes) { 16 | while (n_bytes > 0) { 17 | ssize_t bytesRead = recv(fd, buf, n_bytes, 0); 18 | if (bytesRead <= 0) { 19 | perror("recv"); 20 | return -1; //error 21 | } 22 | assert((size_t)bytesRead <= n_bytes); 23 | n_bytes -= (size_t)bytesRead; 24 | buf += (size_t)bytesRead; 25 | } 26 | return 0; // success 27 | } 28 | 29 | int write_exactly(int fd, const char* buf, size_t n_bytes) { 30 | while (n_bytes > 0) { 31 | ssize_t bytesSent = send(fd, buf, n_bytes, 0); 32 | if (bytesSent <= 0) { 33 | perror("send"); 34 | return -1; // error 35 | } 36 | assert((size_t)bytesSent <= n_bytes); 37 | n_bytes -= (size_t)bytesSent; 38 | buf += (size_t)bytesSent; 39 | } 40 | return 0; // success 41 | } 42 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | void die(const char* msg); 16 | 17 | int recv_exactly(int fd, char* buf, size_t n_bytes); 18 | 19 | int write_exactly(int fd, const char* buf, size_t n_bytes); 20 | 21 | std::string to_lower(const std::string& str); 22 | 23 | class IncorrectProtocol : public std::runtime_error { 24 | public: 25 | // Constructor with a message 26 | explicit IncorrectProtocol(const std::string& message) : std::runtime_error(message) {} 27 | }; 28 | 29 | class SysCallFailure : public std::runtime_error { 30 | public: 31 | // Constructor with a message 32 | explicit SysCallFailure(const std::string& message) : std::runtime_error(message) {} 33 | }; 34 | 35 | class RedisServerError : public std::runtime_error { 36 | public: 37 | // Constructor with a message 38 | explicit RedisServerError(const std::string& message) : std::runtime_error(message) {} 39 | }; 40 | -------------------------------------------------------------------------------- /config.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "config.h" 5 | 6 | 7 | namespace fs = std::filesystem; 8 | 9 | redis::config redis::GlobalConfig; 10 | 11 | bool redis::read_config() { 12 | 13 | fs::path current_path = fs::current_path(); 14 | 15 | for (const auto& entry : fs::directory_iterator(current_path)) { 16 | 17 | if (entry.path().filename() == CONFIG_FILE) { 18 | std::ifstream config(entry.path()); 19 | if (config.is_open()) { 20 | // found the config file! 21 | nlohmann::json json = nlohmann::json::parse(config); 22 | 23 | if (json.find("snapshot_period") == json.end()) { 24 | std::cout << "Unable to read snapshot config! " << std::endl; 25 | return false; 26 | } else { 27 | redis::GlobalConfig.snapshot_period = json["snapshot_period"]; 28 | } 29 | 30 | if (json.find("port") == json.end()) { 31 | std::cout << "Unable to read port config! " << std::endl; 32 | return false; 33 | } else { 34 | redis::GlobalConfig.port = json["port"]; 35 | } 36 | 37 | // all config read 38 | return true; 39 | } else { 40 | // error opening file 41 | break; 42 | } 43 | } 44 | } 45 | return false; 46 | } 47 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.h" 4 | 5 | #define CONFIG_FILE "config.json" 6 | 7 | namespace redis { 8 | struct config{ 9 | int port; 10 | std::string statefile; 11 | int snapshot_period; 12 | }; 13 | extern config GlobalConfig; 14 | bool read_config(); 15 | } 16 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port" : 2000, 3 | "snapshot_period" : 5 4 | } -------------------------------------------------------------------------------- /redisstore.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "redisstore.h" 4 | 5 | RedisStore* RedisStore::_Instance = nullptr; 6 | std::mutex RedisStore::_Instancemutex; 7 | RedisStore::data_type RedisStore::data; 8 | RedisStore::list_type RedisStore::list_data; 9 | 10 | RedisStore& RedisStore::getInstance() { 11 | 12 | if (_Instance == nullptr) { 13 | std::lock_guard lock(_Instancemutex); 14 | if (_Instance == nullptr) { 15 | _Instance = new RedisStore(); 16 | } 17 | } 18 | return *_Instance; 19 | } 20 | 21 | void RedisStore::delInstance() { 22 | 23 | if (_Instance != nullptr) { 24 | std::lock_guard lock(_Instancemutex); 25 | if (_Instance != nullptr) { 26 | _Instance->clear(); 27 | delete _Instance; 28 | _Instance = nullptr; 29 | } 30 | } 31 | } 32 | 33 | void RedisStore::clear() { 34 | 35 | { 36 | std::unique_lock lock(_datamutex); 37 | data.clear(); 38 | } 39 | { 40 | std::unique_lock lock(_listmutex); 41 | list_data.clear(); 42 | } 43 | 44 | } 45 | 46 | void RedisStore::set(const std::string& key, const std::string& value, const std::time_t expiry_epoch) { 47 | std::unique_lock lock(_datamutex); 48 | data[key] = {value, expiry_epoch}; // never expires 49 | } 50 | 51 | static std::time_t now_epoch() { 52 | auto now = std::chrono::system_clock::now(); 53 | return std::chrono::system_clock::to_time_t(now); 54 | } 55 | 56 | bool RedisStore::get(const std::string& key, std::string& value) const { 57 | 58 | bool remove_key = false; 59 | { 60 | std::shared_lock lock(_datamutex); 61 | auto it = data.find(key); 62 | if (it != data.end()) { 63 | 64 | if (it->second.expiry_epoch > now_epoch()) { 65 | value = it->second.val; 66 | return true; 67 | } else { 68 | // key expired, remove it 69 | remove_key = true; 70 | // can not upgrade to unique lock here as it 71 | // can lead to deadlock with another get call 72 | } 73 | } 74 | } 75 | if (remove_key) { 76 | // lazy removal 77 | std::unique_lock lock(_datamutex); 78 | data.erase(key); 79 | } 80 | return false; 81 | } 82 | 83 | bool RedisStore::exists(const std::string& key) const { 84 | 85 | static std::string temp; 86 | bool in_data = get(key, temp); 87 | bool in_list_data = false; 88 | std::shared_lock lock(_listmutex); 89 | auto it = list_data.find(key); 90 | if (it != list_data.end()) { 91 | in_list_data = true; 92 | } 93 | return in_data || in_list_data; 94 | } 95 | 96 | int RedisStore::erase(const std::string& key) { 97 | std::unique_lock lockdata(_datamutex); 98 | int in_data = data.erase(key); 99 | if (in_data) { 100 | return in_data; 101 | } 102 | std::unique_lock locklist(_listmutex); 103 | int in_list_data = list_data.erase(key); 104 | if (in_list_data) { 105 | return in_list_data; 106 | } 107 | return 0; 108 | } 109 | 110 | /** 111 | * reverse set to true means decr 112 | */ 113 | int RedisStore::incr(const std::string& key, bool reverse) { 114 | std::unique_lock lock(_datamutex); 115 | std::string str_val; 116 | auto it = data.find(key); 117 | if (it != data.end() && it->second.expiry_epoch > now_epoch()) { 118 | int64_t int_val; 119 | try{ 120 | int_val = std::stoll(it->second.val); 121 | } catch (const std::exception& e) { 122 | throw e; 123 | } 124 | int delta = reverse ? -1 : 1; 125 | str_val = std::to_string(int_val+delta); 126 | it->second.val = str_val; 127 | return int_val+delta; 128 | } else { 129 | data[key] = {reverse ? "-1" : "1", LONG_MAX}; 130 | return reverse ? -1 : 1; 131 | } 132 | 133 | // should not reach here 134 | return -1; 135 | } 136 | 137 | int RedisStore::lpush(const std::string& key, const std::vector& vals, bool reverse) { 138 | 139 | std::unique_lock lock(_listmutex); 140 | 141 | auto it = list_data.find(key); 142 | if (it == list_data.end()) { 143 | // maybe this is not required 144 | list_data[key] = {}; 145 | } 146 | 147 | for (auto val : vals) { 148 | if (reverse) { 149 | list_data[key].push_back(val); 150 | } else { 151 | list_data[key].push_front(val); 152 | } 153 | } 154 | 155 | return list_data[key].size(); 156 | 157 | } 158 | 159 | std::vector RedisStore::lrange(const std::string& key, int start, int end) { 160 | std::shared_lock lock(_listmutex); 161 | auto it = list_data.find(key); 162 | if (it == list_data.end()) { 163 | return {}; 164 | } 165 | 166 | if (start < 0) { 167 | start = it->second.size() + start; 168 | if (start < 0) { 169 | // I think this should return empty list 170 | // unless we are wrapping around 171 | return {}; 172 | } 173 | } 174 | if (end < 0) { 175 | end = it->second.size() + end; 176 | if (end < 0) { 177 | // I think this should return empty list 178 | // unless we are wrapping around 179 | return {}; 180 | } 181 | } 182 | if (end >= it->second.size()) { 183 | end = it->second.size()-1; 184 | } 185 | if (start > end) { 186 | return {}; 187 | } 188 | std::vector res; 189 | for (int i=start; i<=end && i < it->second.size(); i++) { 190 | res.push_back(it->second[i]); 191 | } 192 | return res; 193 | } 194 | 195 | void to_json(nlohmann::json& j, const ValueEntry& v) { 196 | j = nlohmann::json{{"val", v.val}, {"expiry_epoch", v.expiry_epoch}}; 197 | } 198 | 199 | void from_json(const nlohmann::json& j, ValueEntry& v) { 200 | j.at("val").get_to(v.val); 201 | j.at("expiry_epoch").get_to(v.expiry_epoch); 202 | } 203 | 204 | bool RedisStore::dump() { 205 | 206 | try { 207 | nlohmann::json json; // will contain data to be dumped 208 | { 209 | // Acquire read locks 210 | std::shared_lock lockdata(_datamutex); 211 | std::shared_lock locklist(_listmutex); 212 | 213 | // json library needs to_json() 214 | // defined for every type involved 215 | 216 | json["data"] = data; 217 | json["list_data"] = list_data; 218 | } 219 | std::filesystem::path current_path = std::filesystem::current_path(); 220 | std::filesystem::path state = current_path / STATEFILE; 221 | // Dump into STATEFILE (state.json) in current directory 222 | std::ofstream outputFile(state); 223 | outputFile << json.dump(4) << std::endl; 224 | } catch (const std::exception& e) { 225 | // something went wrong 226 | // maybe log 227 | return false; 228 | } 229 | return true; 230 | } 231 | 232 | bool RedisStore::restore() { 233 | 234 | try { 235 | 236 | // Acquire write locks 237 | std::unique_lock lockdata(_datamutex); 238 | std::unique_lock locklist(_listmutex); 239 | 240 | std::filesystem::path current_path = std::filesystem::current_path(); 241 | std::filesystem::path state = current_path / STATEFILE; 242 | 243 | // Expect STATEFILE (state.json) in current directory 244 | 245 | std::ifstream inputFile(state); 246 | nlohmann::json json = nlohmann::json::parse(inputFile); 247 | auto it_data = json.find("data"); 248 | if (it_data != json.end()) { 249 | data = it_data.value(); 250 | } else { 251 | throw RedisServerError("Could not find data map"); 252 | } 253 | auto it_list_data = json.find("list_data"); 254 | if (it_list_data != json.end()) { 255 | list_data = it_list_data.value(); 256 | } else { 257 | throw RedisServerError("Could not find list_data map"); 258 | } 259 | 260 | } catch (const std::exception& e) { 261 | // something went wrong 262 | // maybe log 263 | return false; 264 | } 265 | return true; 266 | } 267 | -------------------------------------------------------------------------------- /redisstore.h: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | #include 3 | #include 4 | #include 5 | 6 | #define STATEFILE "state.json" 7 | 8 | /** 9 | * To expiry of values 10 | */ 11 | struct ValueEntry { 12 | std::string val; 13 | std::time_t expiry_epoch; 14 | }; 15 | 16 | /** 17 | * This is a singleton class. 18 | * RedisStore represents all the data stored in redis 19 | */ 20 | class RedisStore { 21 | 22 | typedef std::unordered_map data_type; 23 | typedef std::unordered_map> list_type; 24 | 25 | public: 26 | 27 | /** 28 | * Returns the singleton instance of this class. 29 | */ 30 | static RedisStore& getInstance(); 31 | 32 | /** 33 | * Deletes the singleton instance of this class. 34 | */ 35 | static void delInstance(); 36 | 37 | /** 38 | * set a {key, {value, expiry}} 39 | * stores in data map. 40 | */ 41 | void set(const std::string& key, const std::string& value, const std::time_t expiry_epoch = LONG_MAX); 42 | 43 | /** 44 | * get a value of key from data map, return false if 45 | * key does not exists or is expired. 46 | * If key is expired, it is also removed. 47 | */ 48 | bool get(const std::string& key, std::string& value) const; 49 | 50 | /** 51 | * Checkd if there is an entry for a key in 52 | * RedisStore (check both data and list_data maps). 53 | */ 54 | bool exists(const std::string& key) const; 55 | 56 | /** 57 | * Remove a key from RedisStore 58 | */ 59 | int erase(const std::string& key); 60 | 61 | /** 62 | * Increment the value if it can be converted to integer 63 | * Decrements if reverse = true 64 | */ 65 | int incr(const std::string& key, bool reverse = false); 66 | 67 | /** 68 | * Push the vals from left, 69 | * if reverse = true, then push from right 70 | */ 71 | int lpush(const std::string& key, const std::vector& vals, bool reverse = false); 72 | 73 | /** 74 | * get the value of key in a given range 75 | * return empty array if key isn't present in list_data 76 | */ 77 | std::vector lrange(const std::string& key, int start, int end); 78 | 79 | /** 80 | * Dump the in-memory state of redis to disk 81 | */ 82 | bool dump(); 83 | 84 | /** 85 | * load the in-memory state of redis from disk 86 | */ 87 | bool restore(); 88 | 89 | /** 90 | * clear out all the data from RedisStore 91 | */ 92 | void clear(); 93 | 94 | RedisStore(const RedisStore&) = delete; 95 | RedisStore& operator=(const RedisStore&) = delete; 96 | 97 | private: 98 | 99 | static data_type data; 100 | static list_type list_data; 101 | 102 | /** 103 | * Note: There are some rules to follow between set and lpush 104 | * if the key is same between both. Duhh. I haven't implemented that. 105 | * better use different keys. 106 | */ 107 | 108 | RedisStore() {} 109 | ~RedisStore() {} 110 | 111 | static RedisStore* _Instance; 112 | /** 113 | * mutex to synchronize access to _Instance 114 | */ 115 | static std::mutex _Instancemutex; 116 | 117 | /** 118 | * mutexes to synchronize data 119 | */ 120 | mutable std::shared_mutex _datamutex; 121 | mutable std::shared_mutex _listmutex; 122 | }; 123 | -------------------------------------------------------------------------------- /server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "common.h" 3 | #include "RESPParser.h" 4 | #include "cmds.h" 5 | #include "redisstore.h" 6 | #include "config.h" 7 | 8 | class RObject; 9 | 10 | void periodic_snapshot() { 11 | 12 | while (true) { 13 | redis_save({"save"}); 14 | std::this_thread::sleep_for( 15 | std::chrono::minutes(redis::GlobalConfig.snapshot_period)); 16 | } 17 | } 18 | 19 | void process_request(const std::vector& req, int client_fd) { 20 | 21 | if (req.size() == 0) { 22 | return; 23 | } 24 | 25 | if (req[0] == "COMMAND") { 26 | // apparently this is required for redis-cli 27 | // during handshake, but I don't think it actually 28 | // respects it. Sending a minimal response. 29 | const char* response = "*1\r\n$4\r\nPING\r\n"; 30 | write_exactly(client_fd, response, strlen(response)); 31 | } else { 32 | redis_func_type redis_func = get_redis_func(req[0]); 33 | std::unique_ptr output = redis_func(req); 34 | if (output == nullptr) { 35 | throw RedisServerError("Redis cmd failed to return a valid RObject!"); 36 | } 37 | std::string response = output->serialize(); 38 | write_exactly(client_fd, response.c_str(), response.size()); 39 | } 40 | } 41 | 42 | 43 | void handle_client(int client_fd) { 44 | 45 | RESPParser parser(client_fd); 46 | while(true) { 47 | try { 48 | std::vector req = parser.read_new_request(); 49 | if (req.size() == 0) { 50 | throw RedisServerError("Read empty request!"); 51 | } 52 | req[0] = to_lower(req[0]); 53 | process_request(req, client_fd); 54 | } 55 | catch (std::runtime_error& e) { 56 | break; 57 | } 58 | } 59 | if (close(client_fd)) { 60 | die("close"); 61 | } 62 | } 63 | 64 | 65 | /** 66 | * Returns fd on which server is setup. 67 | */ 68 | int setup_server() { 69 | int server_fd; 70 | if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { 71 | // perror appends errno to your msg 72 | // errno: a thread-local error value written to by POSIX syscalls 73 | die("socket"); 74 | } 75 | int reuse = 1; 76 | if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) { 77 | die("setsockopt"); 78 | } 79 | struct sockaddr_in serverAddr; 80 | serverAddr.sin_family = AF_INET; 81 | // htons: host to network short (sin_port is only 16 bit) 82 | // host (x86) byte order is little-endian (LSB at lowest mem addr) 83 | // network (TCP) byte order is big-endian (MSB at lowest mem addr) 84 | // XXX Should htons be mandatory? Maybe not if both server and client 85 | // stay consistent. (Since your don't know endian-ness of the client 86 | // better to always use htons for portability) 87 | serverAddr.sin_port = htons(redis::GlobalConfig.port); 88 | // Bind to any available interface 89 | // XXX Did not understand this fully 90 | serverAddr.sin_addr.s_addr = INADDR_ANY; 91 | if (bind(server_fd, (const sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) { 92 | die("bind"); 93 | } 94 | if (listen(server_fd, SOMAXCONN) == -1) { 95 | die("listen"); 96 | } 97 | 98 | std::cout << "Server listening on port: " << redis::GlobalConfig.port << std::endl; 99 | return server_fd; 100 | } 101 | 102 | void handle_clients(int server_fd) { 103 | 104 | std::vector client_threads; 105 | 106 | while (true) { 107 | struct sockaddr_in clientAddr; 108 | socklen_t clientAddrLen = sizeof(clientAddr); 109 | int client_fd; 110 | if ((client_fd = accept(server_fd, (struct sockaddr *)&clientAddr, &clientAddrLen)) < 0) { 111 | die("accept"); 112 | } 113 | client_threads.emplace_back(handle_client, client_fd); 114 | } 115 | for (auto& thread : client_threads) { 116 | thread.join(); 117 | } 118 | } 119 | 120 | int main() { 121 | 122 | if (!redis::read_config()) { 123 | std::cout << "Unable to read config\n" 124 | << "Please ensure config.json exist and is correctly setup" 125 | << std::endl; 126 | return 0; 127 | } 128 | 129 | if (!RedisStore::getInstance().restore()) { 130 | std::cout << "State restoral failed! Continuing with empty state..." << std::endl; 131 | } else { 132 | std::cout << "Previous state restored!" << std::endl; 133 | } 134 | 135 | std::thread snapshot_thread(periodic_snapshot); 136 | 137 | int server_fd = setup_server(); 138 | handle_clients(server_fd); 139 | if (close(server_fd)) { 140 | die("close"); 141 | } 142 | 143 | snapshot_thread.join(); 144 | RedisStore::delInstance(); 145 | } 146 | -------------------------------------------------------------------------------- /state.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": {}, 3 | "list_data": {} 4 | } 5 | -------------------------------------------------------------------------------- /type.cpp: -------------------------------------------------------------------------------- 1 | #include "type.h" 2 | 3 | std::string redis::RObject::CRLF = "\r\n"; 4 | -------------------------------------------------------------------------------- /type.h: -------------------------------------------------------------------------------- 1 | #include "common.h" 2 | 3 | namespace redis { 4 | 5 | /** 6 | * RObject models all the types 7 | * of responses redis can give 8 | */ 9 | class RObject { 10 | 11 | public: 12 | virtual ~RObject() {}; 13 | virtual std::string prefix() = 0; 14 | virtual std::string serialize() = 0; 15 | static std::string CRLF; 16 | 17 | }; 18 | 19 | class SimpleString : public RObject { 20 | 21 | public: 22 | SimpleString(const std::string& input) { 23 | str = input; 24 | } 25 | std::string prefix() override { 26 | return "+"; 27 | } 28 | std::string serialize() override { 29 | return prefix() + str + CRLF; 30 | } 31 | private: 32 | std::string str; 33 | 34 | }; 35 | 36 | class Error : public RObject { 37 | 38 | public: 39 | Error(const std::string& input) { 40 | str = input; 41 | } 42 | std::string prefix() override { 43 | return "-"; 44 | } 45 | std::string serialize() override { 46 | return prefix() + str + CRLF; 47 | } 48 | private: 49 | std::string str; 50 | 51 | }; 52 | 53 | class Integer : public RObject { 54 | 55 | public: 56 | Integer(int input) { 57 | val = input; 58 | } 59 | std::string prefix() override { 60 | return ":"; 61 | } 62 | std::string serialize() override { 63 | return prefix() + std::to_string(val) + CRLF; 64 | } 65 | private: 66 | int val; 67 | 68 | }; 69 | 70 | class BulkString : public RObject { 71 | 72 | public: 73 | BulkString(const std::string& input) { 74 | str = input; 75 | } 76 | std::string prefix() override { 77 | return "$"; 78 | } 79 | std::string serialize() override { 80 | return prefix() + std::to_string(str.size()) + CRLF 81 | + str + CRLF; 82 | } 83 | private: 84 | std::string str; 85 | }; 86 | 87 | class NullString : public RObject { 88 | 89 | public: 90 | NullString() { 91 | str = "-1"; 92 | } 93 | std::string prefix() override { 94 | return "$"; 95 | } 96 | std::string serialize() override { 97 | return prefix() + str + CRLF; 98 | } 99 | private: 100 | std::string str; 101 | }; 102 | 103 | class Array : public RObject { 104 | 105 | public: 106 | Array() {} 107 | std::string prefix() override { 108 | return "*"; 109 | } 110 | std::string serialize() override { 111 | std::string res = prefix() + std::to_string(array.size()) + CRLF; 112 | for (const auto& it : array) { 113 | res += it->serialize(); 114 | } 115 | return res; 116 | } 117 | void add_element(std::unique_ptr robj) { 118 | array.emplace_back(std::move(robj)); 119 | } 120 | Array(const Array&) = delete; 121 | Array& operator=(const Array&) = delete; 122 | private: 123 | std::vector> array; 124 | }; 125 | 126 | } 127 | --------------------------------------------------------------------------------