Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitattributes

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/cmake-single-platform.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build and Test
name: CI

on:
push:
Expand Down
58 changes: 45 additions & 13 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,50 @@
cmake_minimum_required(VERSION 3.20)
project(CacheDB VERSION 1.0.0 LANGUAGES CXX)

cmake_minimum_required(VERSION 3.10)
project(CacheDB)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
enable_testing()

add_executable(server server.cpp)
add_executable(client client.cpp)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) # For Windows: Prevent overriding the parent project's compiler/linker settings
FetchContent_MakeAvailable(googletest)

# enable_testing()
# add_executable(server_test server_test.cpp)
# target_link_libraries(server_test PRIVATE <dependencies-if-any>)
# add_test(NAME ServerTest COMMAND server_test)
file(GLOB_RECURSE CLIENT_FILES "client/*.cc" "client/*.cpp")
file(GLOB_RECURSE SERVER_FILES "server/*.cc" "server/*.cpp")
file(GLOB_RECURSE TEST_FILES "tests/*.cc" "tests/*.cpp")

# add_executable(client_test client_test.cpp)
# target_link_libraries(client_test PRIVATE <dependencies-if-any>)
# add_test(NAME ClientTest COMMAND client_test)
add_executable(cachedb_client ${CLIENT_FILES} ${CMAKE_SOURCE_DIR}/client.cpp)
target_include_directories(cachedb_client
PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/client
)

add_executable(cachedb_server ${SERVER_FILES} ${CMAKE_SOURCE_DIR}/cachedb.cpp)
target_include_directories(cachedb_server
PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/server
)

add_executable(all_tests ${CLIENT_FILES} ${SERVER_FILES} ${TEST_FILES})
target_link_libraries(all_tests
PRIVATE
GTest::gtest
GTest::gtest_main
)
target_include_directories(all_tests
PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/client
${CMAKE_SOURCE_DIR}/server
)

include(GoogleTest)
gtest_discover_tests(all_tests)
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ CacheDB is an in-memory key-value store designed for high performance and low la
- TTL Management: Set expiration times for keys to automatically manage data lifecycle.
- Non-Blocking I/O: Handles multiple client connections efficiently.

## Getting Started

1. **Clone the repository:**
```bash
git clone https://github.com/etbala/CacheDB.git
cd CacheDB
```

2. **Build the project:**
```bash
mkdir build
cd build
cmake ..
make -j$(nproc)
```

3. **Run tests:**
```bash
./all_tests
```

## Commands

### GET
Expand Down
27 changes: 27 additions & 0 deletions cachedb.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "server/server.h"

int main() {
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); return 1; }

int val = 1;
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = htonl(0);
if (::bind(listen_fd, (sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }

if (::listen(listen_fd, SOMAXCONN) < 0) { perror("listen"); return 1; }

Server server;
server.run(listen_fd);
return 0;
}
221 changes: 12 additions & 209 deletions client.cpp
Original file line number Diff line number Diff line change
@@ -1,215 +1,18 @@

#include "client/client_loop.h"
#include "client/transport.h"
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <vector>
#include <sstream>

void die(const std::string& message) {
int err = errno;
std::cerr << "[" << err << "] " << message << std::endl;
std::exit(EXIT_FAILURE);
}

// Serialization codes
enum {
SER_STR = 0,
SER_NIL = 1,
SER_INT = 2,
SER_ERR = 3,
SER_ARR = 4,
SER_DBL = 5,
};

// Serialize the request according to the server's protocol
void serialize_request(const std::vector<std::string>& cmd, std::vector<uint8_t>& out) {
uint32_t argc = cmd.size();
uint32_t data_len = 4; // length of argc
for (const std::string& arg : cmd) {
data_len += 4; // length of arg_len
data_len += arg.size(); // length of argument data
}
uint32_t total_len = data_len; // total length of data after the initial len field

out.resize(4 + data_len); // 4 bytes for len, plus data_len bytes for data
uint32_t offset = 0;

// Write total_len into the first 4 bytes
memcpy(&out[offset], &total_len, 4);
offset += 4;

// Write argc
memcpy(&out[offset], &argc, 4);
offset += 4;

for (const std::string& arg : cmd) {
uint32_t arg_len = arg.size();
memcpy(&out[offset], &arg_len, 4);
offset += 4;
memcpy(&out[offset], arg.data(), arg_len);
offset += arg_len;
}
}

void deserialize_response(const std::vector<uint8_t>& in, size_t& offset) {
if (offset >= in.size()) {
die("Response parsing error: offset out of bounds");
}
uint8_t type = in[offset++];
switch (type) {
case SER_STR: {
if (offset + 4 > in.size()) {
die("Response parsing error: string length");
}
uint32_t len = 0;
memcpy(&len, &in[offset], 4);
offset += 4;
if (offset + len > in.size()) {
die("Response parsing error: string data");
}
std::string str((char*)&in[offset], len);
offset += len;
std::cout << str << std::endl;
break;
}
case SER_INT: {
if (offset + 8 > in.size()) {
die("Response parsing error: int data");
}
int64_t val = 0;
memcpy(&val, &in[offset], 8);
offset += 8;
std::cout << val << std::endl;
break;
}
case SER_DBL: {
if (offset + 8 > in.size()) {
die("Response parsing error: double data");
}
double val = 0;
memcpy(&val, &in[offset], 8);
offset += 8;
std::cout << val << std::endl;
break;
}
case SER_NIL: {
std::cout << "(nil)" << std::endl;
break;
}
case SER_ERR: {
if (offset + 4 > in.size()) {
die("Response parsing error: error length");
}
uint32_t len = 0;
memcpy(&len, &in[offset], 4);
offset += 4;
if (offset + len > in.size()) {
die("Response parsing error: error data");
}
std::string str((char*)&in[offset], len);
offset += len;
std::cerr << "(error) " << str << std::endl;
break;
}
case SER_ARR: {
if (offset + 4 > in.size()) {
die("Response parsing error: array length");
}
uint32_t len = 0;
memcpy(&len, &in[offset], 4);
offset += 4;
for (uint32_t i = 0; i < len; ++i) {
deserialize_response(in, offset);
}
break;
}
default:
die("Response parsing error: unknown type");
}
}

int main() {
// Create a socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
die("socket failed");
}

// Server address
sockaddr_in serv_addr = {};
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(1234);

// Convert IPv4 and IPv6 addresses from text to binary form
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
die("Invalid address / Address not supported");
}

// Connect to the server
if (connect(sockfd, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
die("Connection failed");
}

std::string line;
while (std::cout << "> ", std::getline(std::cin, line)) {
// Split line into arguments
std::istringstream iss(line);
std::vector<std::string> cmd;
std::string arg;
while (iss >> arg) {
cmd.push_back(arg);
}
if (cmd.empty()) {
continue;
}

// Serialize the request
std::vector<uint8_t> request;
serialize_request(cmd, request);

// Send the request
size_t total_sent = 0;
while (total_sent < request.size()) {
ssize_t n = send(sockfd, &request[total_sent], request.size() - total_sent, 0);
if (n < 0) {
die("send failed");
}
total_sent += n;
}

// Receive the response header (4 bytes indicating the length)
std::vector<uint8_t> response_header(4);
ssize_t n = recv(sockfd, &response_header[0], 4, MSG_WAITALL);
if (n <= 0) {
die("recv failed");
}
uint32_t resp_len = 0;
memcpy(&resp_len, &response_header[0], 4);
if (resp_len > 10 * 1024 * 1024) {
die("Response too large");
}
int main(int argc, char** argv) {
const char* host = "127.0.0.1";
uint16_t port = 1234;

// Receive the response body
std::vector<uint8_t> response_body(resp_len);
size_t total_received = 0;
while (total_received < resp_len) {
n = recv(sockfd, &response_body[total_received], resp_len - total_received, 0);
if (n <= 0) {
die("recv failed");
}
total_received += n;
}
// (Optional) allow overrides: client 127.0.0.1 1234
if (argc >= 2) host = argv[1];
if (argc >= 3) port = static_cast<uint16_t>(std::stoi(argv[2]));

// Parse and print the response
size_t offset = 0;
deserialize_response(response_body, offset);
}
TcpTransport transport;
transport.connect(host, port);

close(sockfd);
return 0;
// Use std::cin/std::cout/std::cerr in production; in tests you'll pass string streams.
return run_client_repl(transport, std::cin, std::cout, std::cerr);
}
Loading