added basic commands
This commit is contained in:
parent
15d92e9f3a
commit
1fc7801e52
61
README.md
61
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.
|
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!
|
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! ☺️
|
||||||
56
client.py
56
client.py
@ -1,3 +1,4 @@
|
|||||||
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
import argparse
|
import argparse
|
||||||
@ -24,32 +25,61 @@ def recv_exact(sock: socket.socket, n: int) -> bytes:
|
|||||||
data += chunk
|
data += chunk
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def send_frame(sock: socket.socket, payload: bytes) -> None:
|
def send_command(sock: socket.socket, text: str) -> None:
|
||||||
header = struct.pack('!I', len(payload))
|
parts = shlex.split(text)
|
||||||
sock.sendall(header + payload)
|
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)
|
header = recv_exact(sock, 4)
|
||||||
(length,) = struct.unpack('!I', header)
|
(length,) = struct.unpack('!I', header)
|
||||||
if length > 10_000_000:
|
if length > 10_000_000:
|
||||||
raise ValueError('Message length is too long')
|
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():
|
def main():
|
||||||
print_logo()
|
print_logo()
|
||||||
parser = argparse.ArgumentParser(description='NOT(Redis) client')
|
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('-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('-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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with socket.create_connection((args.host, args.port)) as sock:
|
with socket.create_connection((args.host, args.port)) as sock:
|
||||||
if args.message:
|
if args.message:
|
||||||
send_frame(sock, args.message.encode())
|
send_command(sock, args.message)
|
||||||
response = recv_frame(sock)
|
response = recv_response(sock)
|
||||||
print('Message sent:', args.message)
|
print(response)
|
||||||
print('Server says:', response.decode("utf-8", errors='replace'))
|
|
||||||
else:
|
else:
|
||||||
print(f"Connected to {args.host}:{args.port}")
|
print(f"Connected to {args.host}:{args.port}")
|
||||||
print("Type 'quit' or 'exit' to leave.")
|
print("Type 'quit' or 'exit' to leave.")
|
||||||
@ -66,9 +96,9 @@ def main():
|
|||||||
if cmd_line.lower() in ('quit', 'exit'):
|
if cmd_line.lower() in ('quit', 'exit'):
|
||||||
break
|
break
|
||||||
|
|
||||||
send_frame(sock, cmd_line.encode())
|
send_command(sock, cmd_line)
|
||||||
response = recv_frame(sock)
|
response = recv_response(sock)
|
||||||
print(response.decode("utf-8", errors='replace'))
|
print(response)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nType 'quit' or 'exit' to leave.")
|
print("\nType 'quit' or 'exit' to leave.")
|
||||||
|
|||||||
3
src/command_parser.hpp
Normal file
3
src/command_parser.hpp
Normal file
@ -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.
|
||||||
|
|
||||||
@ -3,3 +3,9 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
const uint32_t k_max_message_size = 4096;
|
const uint32_t k_max_message_size = 4096;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
RES_OK = 0,
|
||||||
|
RES_ERR = 1,
|
||||||
|
RES_NX = 2,
|
||||||
|
};
|
||||||
|
|||||||
@ -160,22 +160,96 @@ void Server::state_res(Connection* conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int32_t parse_req(
|
||||||
|
const uint8_t* data, size_t len, std::vector<std::string>& 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) {
|
int32_t Server::parse_and_execute(Connection* conn) {
|
||||||
uint32_t len_net;
|
uint32_t len_net;
|
||||||
memcpy(&len_net, conn->incoming.data(), 4);
|
memcpy(&len_net, conn->incoming.data(), 4);
|
||||||
uint32_t len = ntohl(len_net);
|
uint32_t len = ntohl(len_net);
|
||||||
|
|
||||||
std::string message(conn->incoming.begin() + 4, conn->incoming.begin() + 4 + len);
|
std::vector<std::string> cmd;
|
||||||
Logger::log_info("Client (fd=" + std::to_string(conn->connectionfd) + ") says: " + message);
|
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 = (uint32_t)msg.size();
|
||||||
uint32_t reply_len = static_cast<uint32_t>(reply.size());
|
uint32_t total_len = 4 + reply_len;
|
||||||
uint32_t reply_len_net = htonl(reply_len);
|
|
||||||
|
|
||||||
const char* header_ptr = reinterpret_cast<const char*>(&reply_len_net);
|
uint32_t total_len_net = htonl(total_len);
|
||||||
conn->outgoing.insert(conn->outgoing.end(), header_ptr, header_ptr + 4);
|
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;
|
conn->state = STATE_RES;
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@ -35,6 +35,8 @@ private:
|
|||||||
std::vector<struct pollfd> poll_args;
|
std::vector<struct pollfd> poll_args;
|
||||||
std::map<int, Connection*> fd2conn;
|
std::map<int, Connection*> fd2conn;
|
||||||
|
|
||||||
|
std::map<std::string, std::string> g_data;
|
||||||
|
|
||||||
void setup();
|
void setup();
|
||||||
void accept_new_connection();
|
void accept_new_connection();
|
||||||
void connection_io(Connection* conn);
|
void connection_io(Connection* conn);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user