From 1fc7801e52dd8ac93851362508a5290f4346e223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=93=D0=BB=D0=B0=D0=B7=D1=83?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Sun, 11 Jan 2026 03:15:23 +0300 Subject: [PATCH] added basic commands --- README.md | 61 ++++++++++++++++++++++++++++ client.py | 56 ++++++++++++++++++++------ src/command_parser.hpp | 3 ++ src/constants.hpp | 6 +++ src/server.cpp | 90 ++++++++++++++++++++++++++++++++++++++---- src/server.hpp | 2 + 6 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 src/command_parser.hpp diff --git a/README.md b/README.md index 61a2114..4ff285d 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,64 @@ A toy implementation of a Redis-like server in C++. This is a learning project for me to understand how Redis works internally. It is not intended for production use. Thanks to [Build-your-own-X](https://github.com/codecrafters-io/build-your-own-x) repository for helping me get started with this project! + + +# Building + +Requirements: +- A C++17 compatible compiler (e.g., g++, clang++) +- CMake 3.10 or higher +- Python 3.10 or higher (for the client) + +```bash +cmake . && make +``` + +Now you can run the server: + +```bash +./my_own_redis +``` + +# Client + +In ```Build-your-own-X``` client and server were written on C/C++. But I decided to write a simple client in Python for easier testing and just because I like Python. + +You can run the client like this: + +```bash +uv run ./client.py + +# Help +uv run ./client.py --help +``` + +# Server protocol + +The server uses a simple binary protocol over TCP. +Each command is sent as: +- 4 bytes: total length of the command (excluding this length field) +- 4 bytes: number of arguments (N) +- For each argument: + - 4 bytes: length of the argument (L) + - L bytes: argument data + +The server responds with: +- 4 bytes: total length of the response (excluding this length field) +- 4 bytes: status code (0 = OK, 2 = NX, 1 = ERR) +- Remaining bytes: response message (if any) + +## Commands + +Currently supported commands: +- `SET key value`: Sets the value for the given key. +- `GET key`: Gets the value for the given key. +- `DEL key`: Deletes the given key. + + +# Afterwords + +I don't plan to make this project more complex, but I might add some features in the future. And I don't know if you would find this project useful, but feel free to use it as a learning resource or a starting point for your own projects! +Enjoy coding! +Love yourself! +Be happy! ☺️ \ No newline at end of file diff --git a/client.py b/client.py index 6a7a1f1..66cb560 100644 --- a/client.py +++ b/client.py @@ -1,3 +1,4 @@ +import shlex import socket import struct import argparse @@ -24,32 +25,61 @@ def recv_exact(sock: socket.socket, n: int) -> bytes: data += chunk return data -def send_frame(sock: socket.socket, payload: bytes) -> None: - header = struct.pack('!I', len(payload)) - sock.sendall(header + payload) +def send_command(sock: socket.socket, text: str) -> None: + parts = shlex.split(text) + if not parts: + return + + chunks = [] + + n_args = len(parts) + chunks.append(struct.pack('!I', n_args)) + + for part in parts: + part_bytes = part.encode() + chunks.append(struct.pack('!I', len(part_bytes))) + chunks.append(part_bytes) + + payload = b''.join(chunks) + + sock.sendall(struct.pack('!I', len(payload)) + payload) -def recv_frame(sock: socket.socket) -> bytes: +def recv_response(sock: socket.socket) -> str: header = recv_exact(sock, 4) (length,) = struct.unpack('!I', header) if length > 10_000_000: raise ValueError('Message length is too long') - return recv_exact(sock, length) + + body = recv_exact(sock, length) + if length < 4: + raise ValueError("Response too short") + + (status,) = struct.unpack('!I', body[:4]) + msg = body[4:] + + if status == 0: + if not msg: + return "(ok)" + return msg.decode("utf-8", errors='replace') + elif status == 2: + return "(nil)" + else: + return f"(err) {msg.decode('utf-8', errors='replace')}" def main(): print_logo() parser = argparse.ArgumentParser(description='NOT(Redis) client') parser.add_argument('-H', '--host', type=str, required=False, default='127.0.0.1', help='Server host') parser.add_argument('-P', '--port', type=int, required=False, default=6379, help='Server port') - parser.add_argument('-M', '--message', type=str, required=False, default=None, help='Message to send (if not provided, starts interactive shell)') + parser.add_argument('-M', '--message', type=str, required=False, default=None, help='Command to send (e.g. "set k v")') args = parser.parse_args() try: with socket.create_connection((args.host, args.port)) as sock: if args.message: - send_frame(sock, args.message.encode()) - response = recv_frame(sock) - print('Message sent:', args.message) - print('Server says:', response.decode("utf-8", errors='replace')) + send_command(sock, args.message) + response = recv_response(sock) + print(response) else: print(f"Connected to {args.host}:{args.port}") print("Type 'quit' or 'exit' to leave.") @@ -66,9 +96,9 @@ def main(): if cmd_line.lower() in ('quit', 'exit'): break - send_frame(sock, cmd_line.encode()) - response = recv_frame(sock) - print(response.decode("utf-8", errors='replace')) + send_command(sock, cmd_line) + response = recv_response(sock) + print(response) except KeyboardInterrupt: print("\nType 'quit' or 'exit' to leave.") diff --git a/src/command_parser.hpp b/src/command_parser.hpp new file mode 100644 index 0000000..ac9be44 --- /dev/null +++ b/src/command_parser.hpp @@ -0,0 +1,3 @@ +// TODO: Implemented command parsing logic outside of server.cpp +// This file can be expanded in the future for more complex command parsing needs. + diff --git a/src/constants.hpp b/src/constants.hpp index f442ad8..1aae648 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -3,3 +3,9 @@ #include const uint32_t k_max_message_size = 4096; + +enum { + RES_OK = 0, + RES_ERR = 1, + RES_NX = 2, +}; diff --git a/src/server.cpp b/src/server.cpp index 625241a..87d0136 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -160,22 +160,96 @@ void Server::state_res(Connection* conn) { } } +static int32_t parse_req( + const uint8_t* data, size_t len, std::vector& out) +{ + if (len < 4) { + return -1; + } + uint32_t n = 0; + memcpy(&n, data, 4); + n = ntohl(n); + size_t pos = 4; + while (n--) { + if (pos + 4 > len) { + return -1; + } + uint32_t sz = 0; + memcpy(&sz, data + pos, 4); + sz = ntohl(sz); + pos += 4; + if (pos + sz > len) { + return -1; + } + out.push_back(std::string((char*)&data[pos], sz)); + pos += sz; + } + if (pos != len) { + return -1; + } + return 0; +} + int32_t Server::parse_and_execute(Connection* conn) { uint32_t len_net; memcpy(&len_net, conn->incoming.data(), 4); uint32_t len = ntohl(len_net); - std::string message(conn->incoming.begin() + 4, conn->incoming.begin() + 4 + len); - Logger::log_info("Client (fd=" + std::to_string(conn->connectionfd) + ") says: " + message); + std::vector cmd; + if (parse_req(conn->incoming.data() + 4, len, cmd) != 0) { + Logger::log_error("bad request"); + return -1; + } + + uint32_t status = RES_OK; + std::string msg; + + if (cmd.empty()) { + status = RES_ERR; + msg = "Empty command"; + } else { + std::string name = cmd[0]; + if (name == "set") { + if (cmd.size() == 3) { + g_data[cmd[1]] = cmd[2]; + } else { + status = RES_ERR; + msg = "Usage: set key value"; + } + } else if (name == "get") { + if (cmd.size() == 2) { + auto it = g_data.find(cmd[1]); + if (it == g_data.end()) { + status = RES_NX; + } else { + msg = it->second; + } + } else { + status = RES_ERR; + msg = "Usage: get key"; + } + } else if (name == "del") { + if (cmd.size() == 2) { + g_data.erase(cmd[1]); + } else { + status = RES_ERR; + msg = "Usage: del key"; + } + } else { + status = RES_ERR; + msg = "Unknown command: " + name; + } + } - const std::string reply = "world"; - uint32_t reply_len = static_cast(reply.size()); - uint32_t reply_len_net = htonl(reply_len); + uint32_t reply_len = (uint32_t)msg.size(); + uint32_t total_len = 4 + reply_len; - const char* header_ptr = reinterpret_cast(&reply_len_net); - conn->outgoing.insert(conn->outgoing.end(), header_ptr, header_ptr + 4); + uint32_t total_len_net = htonl(total_len); + uint32_t status_net = htonl(status); - conn->outgoing.insert(conn->outgoing.end(), reply.begin(), reply.end()); + conn->outgoing.insert(conn->outgoing.end(), (char*)&total_len_net, (char*)&total_len_net + 4); + conn->outgoing.insert(conn->outgoing.end(), (char*)&status_net, (char*)&status_net + 4); + conn->outgoing.insert(conn->outgoing.end(), msg.begin(), msg.end()); conn->state = STATE_RES; return 0; diff --git a/src/server.hpp b/src/server.hpp index 64535fd..125e311 100644 --- a/src/server.hpp +++ b/src/server.hpp @@ -35,6 +35,8 @@ private: std::vector poll_args; std::map fd2conn; + std::map g_data; + void setup(); void accept_new_connection(); void connection_io(Connection* conn);