In-Memory Key-Value Store in C++
Build a mini Redis with LRU eviction and a TCP server in C++.
Introduction
30 Second Summary
Every time you visit a fast website, there's a good chance the data you see was pulled from an in-memory cache rather than a slow database. These caches decide what to keep and what to throw away so they never run out of space.
In this project, you will build a mini Redis clone from scratch in C++. You'll create an in-memory key-value store with automatic eviction that you can connect to over the network and interact with using typed commands.
What You'll Build
A networked key-value store that accepts TCP connections and processes SET, GET, and DEL commands against an LRU cache with O(1) performance.
By the end of this project, you'll have:
- An LRU cache class that performs constant-time get, set, and delete with automatic eviction when the cache reaches its capacity limit.
- A command parser that translates raw text input like SET name Alice into structured cache operations with clear response messages.
- A TCP server using Winsock2 that accepts client connections on localhost and processes key-value commands in real time.
- Secret Mission: Handle multiple simultaneous client connections using threading and mutex protection.
Are there any prerequisites?
You need basic C++ syntax knowledge (variables, functions, if/else, loops). No prior experience with STL containers or socket programming is required.
Before We Start
Before We Start...
Set Up and Verify Your Toolchain
Before writing any project code, you need to confirm that your compiler supports the C++17 features this project relies on.
In this step, you will verify your g++ version, confirm that the Winsock2 networking library links correctly, and create the project file structure.
In this step, get ready to:
- Verify g++ supports C++17 and compile a test program.
- Confirm Winsock2 links correctly with a minimal network program.
- Create the five project files for your key-value store.
Open VS Code and verify your compiler
- Press the Windows key to open your search bar.
- Type VS Code and press Enter to open it.
- Open a new terminal by clicking Terminal in the top menu bar, then New Terminal.
A PowerShell panel opens at the bottom of VS Code. This is where you will run all your commands.
- Check your compiler version by running this command:
g++ --version
You should see output like g++ (GCC) 13.2.0 or similar. The important number is the major version before the first dot.
Version lower than 7 or command not found?
If you see a version lower than 7.x, you need to update your MinGW installation to a newer release that supports C++17.
If you see an error about the command not being recognized, your MinGW bin folder is not in your system PATH. Revisit your MinGW setup and add the bin directory to your PATH environment variable.
Help me fix my g++ setup so I can compile C++17 code
Now create your project folder on the Desktop so you can find it easily.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir kv-store
cd kv-store
Time to confirm C++17 compilation works end-to-end. You will create a small test program that uses a C++17 feature.
- Create a test file called test_compile.cpp by running:
New-Item -ItemType File -Name "test_compile.cpp"
- Open test_compile.cpp in the VS Code editor by clicking it in the Explorer sidebar on the left.
- Paste the following C++17 test program into the file:
#include <iostream>
#include <optional>
int main() {
// std::optional is a C++17 feature
std::optional<int> value = 42;
if (value.has_value()) {
std::cout << "C++17 works! Value: " << value.value() << std::endl;
}
return 0;
}
What does this code do?
This program uses std::optional, a container introduced in C++17 that may or may not hold a value. If your compiler can build this, it fully supports C++17.
- Save the file with Ctrl+S.
- Compile and run the test program by running these commands in your terminal:
g++ -std=c++17 test_compile.cpp -o test_compile.exe
.\test_compile.exe
You should see C++17 works! Value: 42 printed in the terminal.
Seeing a compilation error?
If you see an error about optional not being found, your g++ version does not support C++17. Check the version again with g++ --version and ensure it is 7.x or higher.
If the file is not found, make sure you are running the command from inside the kv-store folder where you saved the file.
Help me troubleshoot my C++17 compilation error
Verify Winsock2 is available
Your key-value store needs Winsock2 to accept network connections over TCP. Winsock2 is the Windows socket API for network programming. You need to confirm it links correctly before building the real server.
- Create a test file called test_winsock.cpp by running:
New-Item -ItemType File -Name "test_winsock.cpp"
- Open test_winsock.cpp in the editor and paste the following code:
#include <winsock2.h>
#include <iostream>
int main() {
// Initialize the Winsock2 library
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result == 0) {
std::cout << "Winsock2 initialized successfully!" << std::endl;
WSACleanup();
} else {
std::cerr << "WSAStartup failed: " << result << std::endl;
}
return 0;
}
What does this code do?
WSAStartup(MAKEWORD(2, 2), &wsaData) initializes the Winsock2 DLL and requests version 2.2. Every Windows program that uses sockets must call this before making any network calls.
WSACleanup() releases the Winsock2 resources when you are done. The -lws2_32 flag you will use during compilation tells the linker to include the Winsock2 library.
- Save the file with Ctrl+S.
- Compile and run the Winsock2 test by running these commands:
g++ -std=c++17 test_winsock.cpp -o test_winsock.exe -lws2_32
.\test_winsock.exe
You should see Winsock2 initialized successfully! printed in the terminal.
Seeing a linker error?
If you see undefined reference errors for WSAStartup or WSACleanup, the -lws2_32 flag is missing from your compile command. Make sure it appears at the end of the command, after the source file name.
If you see an error about winsock2.h not being found, your MinGW installation may be incomplete. Reinstall MinGW and ensure the mingw32-pthreads-w32 and Windows API headers packages are included.
Help me fix my Winsock2 linker error
Create your project file structure
Your project will have five source files. Two pairs (header + implementation) for the cache and parser modules, plus one main server file.
- Create all five project files by running this command:
New-Item -ItemType File -Name "lru_cache.h", "lru_cache.cpp", "command_parser.h", "command_parser.cpp", "server.cpp"
What are these files for?
- lru_cache.h and lru_cache.cpp will hold your LRU cache class definition and implementation.
- command_parser.h and command_parser.cpp will handle parsing raw text commands like SET name Alice into structured operations.
- server.cpp will contain the TCP server that ties everything together.
- Confirm the files exist by running:
ls
You should see command_parser.cpp, command_parser.h, lru_cache.cpp, lru_cache.h, and server.cpp listed alongside your test files.
- Open the kv-store folder in VS Code by clicking File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the kv-store folder, and click Select Folder.
VS Code reloads with your project folder as the workspace. You should see all five files in the Explorer sidebar. IntelliSense now recognizes the header files for auto-complete in later steps.
Don't see all five files in the sidebar?
Make sure you selected the kv-store folder itself, not a parent folder. The Explorer sidebar should show the files at the top level, not nested inside another directory.
If a file is missing, create it manually by right-clicking in the Explorer sidebar and selecting New File.
Help me get my project files showing in VS Code
Your toolchain is ready to go. Next up, you will build the LRU cache, the data structure at the heart of your key-value store.
Build the LRU Cache
Your toolchain is verified and your project files are ready. Now it's time to build the core data structure that powers your key-value store: the LRU cache.
An LRU (Least Recently Used) cache stores a fixed number of entries and automatically removes the one that hasn't been accessed for the longest time when it runs out of space. You'll implement this using two data structures that work together to give you O(1) performance on every operation.
In this step, get ready to:
- Define the LRU cache class with its data structure members.
- Implement get, set, and delete operations with O(1) eviction.
- Test the cache to verify correct behavior.
Define the LRU cache class interface
The cache needs two data structures working in tandem. A doubly-linked list tracks usage order, with the most recently accessed item at the front and the least recently used at the back. A hash map maps each key directly to its position in the list, giving you O(1) lookups.
- Open lru_cache.h in VS Code and add the class declaration by pasting this code:
#ifndef LRU_CACHE_H
#define LRU_CACHE_H
#include <string>
#include <list>
#include <unordered_map>
class LRUCache {
public:
explicit LRUCache(size_t capacity);
std::string get(const std::string& key);
void set(const std::string& key, const std::string& value);
bool del(const std::string& key);
private:
size_t capacity_;
std::list<std::pair<std::string, std::string>> items_;
std::unordered_map<std::string, std::list<std::pair<std::string, std::string>>::iterator> map_;
};
#endif
What does this code do?
The header guard (#ifndef / #define / #endif) prevents the file from being included multiple times during compilation.
items_ is a doubly-linked list of key-value pairs. The front of the list is the most recently used item. The back is the eviction candidate.
map_ maps each key to an iterator pointing into the list. This lets you jump straight to any entry without scanning the entire list.
- Save lru_cache.h.
Why two data structures?
A linked list alone gives you O(1) removal and reordering, but finding a specific key requires scanning every node (O(n)). A hash map alone gives you O(1) lookup, but has no concept of ordering.
Combining them gives you the best of both. The map jumps to the node instantly, and the list maintains usage order. This is the same pattern used in the classic LeetCode 146 interview question.
Implement the cache methods with eviction
Now you'll write the actual logic. The key insight is that std::list::splice can move a node to the front of the list in O(1) without invalidating any iterators stored in the map.
- Open lru_cache.cpp and add the constructor and get method by pasting this code:
#include "lru_cache.h"
LRUCache::LRUCache(size_t capacity) : capacity_(capacity) {}
std::string LRUCache::get(const std::string& key) {
auto it = map_.find(key);
if (it == map_.end()) {
return "";
}
// Move accessed item to front (most recently used)
items_.splice(items_.begin(), items_, it->second);
return it->second->second;
}
What does this code do?
The constructor stores the maximum capacity. No entries are created yet.
get() first checks if the key exists in the map. If not, it returns an empty string. If found, it uses splice to move that node to the front of the list (promoting it to "most recently used"), then returns the value.
- Add the set method below the get method by pasting this code:
void LRUCache::set(const std::string& key, const std::string& value) {
auto it = map_.find(key);
if (it != map_.end()) {
// Key already exists: update value and move to front
it->second->second = value;
items_.splice(items_.begin(), items_, it->second);
return;
}
// Key is new: check if we need to evict
if (items_.size() == capacity_) {
// Evict the least recently used entry (back of the list)
std::string lru_key = items_.back().first;
items_.pop_back();
map_.erase(lru_key);
}
// Insert new entry at the front
items_.emplace_front(key, value);
map_[key] = items_.begin();
}
How does eviction work?
When inserting a new key, set() checks if the cache is full. If it is, the back of the list (the least recently used entry) gets evicted. Its key is removed from the map, and the node is popped off the list.
If the key already exists, the value is updated in place and the node is spliced to the front. No eviction needed.
- Add the del method below the set method by pasting this code:
bool LRUCache::del(const std::string& key) {
auto it = map_.find(key);
if (it == map_.end()) {
return false;
}
// Remove from both data structures
items_.erase(it->second);
map_.erase(it);
return true;
}
What does this code do?
del() looks up the key in the map. If found, it erases the node from the list (using the stored iterator) and removes the key from the map. Both operations are O(1).
- Save lru_cache.cpp.
Seeing red squiggles in VS Code?
If IntelliSense shows errors on the #include "lru_cache.h" line, make sure both files are in the same folder. IntelliSense may take a few seconds to catch up after you save.
If you see errors on the iterator type, double check that your lru_cache.h includes both <list> and <unordered_map>.
Help me fix IntelliSense errors in my LRU cache files.
✔️ Awesome, I've got everything!
Great. Double check you've saved both lru_cache.h and lru_cache.cpp.
ⓧ I'd like to double check the full code
Here is the complete lru_cache.h:
#ifndef LRU_CACHE_H
#define LRU_CACHE_H
#include <string>
#include <list>
#include <unordered_map>
class LRUCache {
public:
explicit LRUCache(size_t capacity);
std::string get(const std::string& key);
void set(const std::string& key, const std::string& value);
bool del(const std::string& key);
private:
size_t capacity_;
std::list<std::pair<std::string, std::string>> items_;
std::unordered_map<std::string, std::list<std::pair<std::string, std::string>>::iterator> map_;
};
#endif
Here is the complete lru_cache.cpp:
#include "lru_cache.h"
LRUCache::LRUCache(size_t capacity) : capacity_(capacity) {}
std::string LRUCache::get(const std::string& key) {
auto it = map_.find(key);
if (it == map_.end()) {
return "";
}
items_.splice(items_.begin(), items_, it->second);
return it->second->second;
}
void LRUCache::set(const std::string& key, const std::string& value) {
auto it = map_.find(key);
if (it != map_.end()) {
it->second->second = value;
items_.splice(items_.begin(), items_, it->second);
return;
}
if (items_.size() == capacity_) {
std::string lru_key = items_.back().first;
items_.pop_back();
map_.erase(lru_key);
}
items_.emplace_front(key, value);
map_[key] = items_.begin();
}
bool LRUCache::del(const std::string& key) {
auto it = map_.find(key);
if (it == map_.end()) {
return false;
}
items_.erase(it->second);
map_.erase(it);
return true;
}
Test the cache with a driver program
Before wiring the cache into a server, you need to confirm it actually works. You'll write a small test program that exercises set, get, delete, and eviction.
- Create a new file called test_cache.cpp in the same project folder and paste this code:
#include <iostream>
#include "lru_cache.h"
int main() {
// Create a cache with capacity of 3
LRUCache cache(3);
// Test basic set and get
cache.set("a", "apple");
cache.set("b", "banana");
cache.set("c", "cherry");
std::cout << "GET a: " << cache.get("a") << std::endl;
std::cout << "GET b: " << cache.get("b") << std::endl;
std::cout << "GET c: " << cache.get("c") << std::endl;
// After the GETs above, usage order is: c (front), b, a (back)
// Adding a 4th entry should evict "a" (least recently used)
cache.set("d", "date");
std::cout << "GET a (evicted): " << cache.get("a") << std::endl;
std::cout << "GET d: " << cache.get("d") << std::endl;
// Test delete
bool deleted = cache.del("c");
std::cout << "DEL c: " << (deleted ? "true" : "false") << std::endl;
std::cout << "GET c (deleted): " << cache.get("c") << std::endl;
return 0;
}
What does this code do?
This creates a cache with capacity 3, fills it with three entries, then adds a fourth. The fourth entry forces eviction of the least recently used key.
After getting a, b, and c in that order, the usage order becomes c (most recent), b, a (least recent). So adding "d" evicts "a". The test also verifies that del() removes an entry correctly.
- Compile the test program by running this command in your terminal:
g++ -std=c++17 test_cache.cpp lru_cache.cpp -o test_cache.exe
What does this command do?
This compiles both test_cache.cpp and lru_cache.cpp together, linking them into one executable called test_cache.exe. The -std=c++17 flag enables C++17 features.
- Run the test by executing:
.\test_cache.exe
- You should see this output in your terminal:
GET a: apple
GET b: banana
GET c: cherry
GET a (evicted):
GET d: date
DEL c: true
GET c (deleted):
The empty values for evicted and deleted keys confirm your cache is working correctly. The entry for "a" was automatically removed when the cache exceeded its capacity of 3.
Compilation errors?
If you see errors about undefined references, make sure you included lru_cache.cpp in the compile command (not just test_cache.cpp alone).
If you see errors about missing members or unknown types, compare your lru_cache.h against the full code in the tab above. A missing semicolon after the class closing brace is a common culprit.
Help me fix compilation errors in my LRU cache.
Wrong output values?
If evicted keys still return values, check that your set() method compares items_.size() == capacity_ (not > or >=) before inserting the new entry.
Help me debug incorrect eviction in my LRU cache.
Your LRU cache is fully operational with O(1) get, set, and delete. Next up, you'll build the command parser that translates raw text commands like SET name Alice into cache operations.
Build the Command Parser
Your LRU cache can store, retrieve, and evict data. But right now the only way to use it is by calling C++ methods directly in code.
A real key-value store needs a text-based protocol so that external clients can send commands like SET name Alice over the network. In this step, you will build a command parser that translates raw strings into cache operations and returns human-readable responses.
In this step, get ready to:
- Define command types and a parsed command structure.
- Implement a parser that splits raw input into commands and arguments.
- Implement an executor that runs parsed commands against the cache and returns responses.
Define the command types and parser header
Before you can parse commands, you need to define what a command looks like in your program. An enum represents the supported command types, and a struct holds the parsed pieces (type, key, and optionally a value).
- Open command_parser.h in VS Code and add the following code:
#ifndef COMMAND_PARSER_H
#define COMMAND_PARSER_H
#include <string>
#include "lru_cache.h"
// Supported command types
enum class CommandType {
SET,
GET,
DEL,
UNKNOWN
};
// Holds the parsed pieces of a command
struct ParsedCommand {
CommandType type;
std::string key;
std::string value;
};
// Parses a raw input string into a structured command
ParsedCommand parseCommand(const std::string& input);
// Executes a parsed command against the cache and returns a response
std::string executeCommand(const ParsedCommand& cmd, LRUCache& cache);
#endif
What does this code do?
The enum class CommandType defines four possible command types: SET, GET, DEL, and UNKNOWN. Using enum class instead of a plain enum gives you type safety so you cannot accidentally mix command types with integers.
The ParsedCommand struct groups the command type together with its key and value arguments. This makes it easy to pass around a complete command as a single object.
- Save command_parser.h.
Implement the parseCommand function
The parser needs to split a raw string like "SET name Alice" into its command word, key, and value. You will use std::istringstream from the <sstream> header, which lets you treat a string like an input stream and extract words one at a time.
- Open command_parser.cpp and add the includes and the parseCommand function:
#include "command_parser.h"
#include <sstream>
ParsedCommand parseCommand(const std::string& input) {
ParsedCommand cmd;
cmd.type = CommandType::UNKNOWN;
// Wrap the input in a stream for easy word extraction
std::istringstream stream(input);
std::string command_word;
stream >> command_word;
if (command_word == "SET") {
cmd.type = CommandType::SET;
stream >> cmd.key;
// Use getline to capture multi-word values
std::getline(stream >> std::ws, cmd.value);
} else if (command_word == "GET") {
cmd.type = CommandType::GET;
stream >> cmd.key;
} else if (command_word == "DEL") {
cmd.type = CommandType::DEL;
stream >> cmd.key;
}
return cmd;
}
What does this code do?
The function wraps the raw input string in a std::istringstream so you can use the >> operator to extract one word at a time. The first extraction pulls the command word (SET, GET, or DEL).
For SET commands, std::getline(stream >> std::ws, cmd.value) captures the entire rest of the line as the value. The >> std::ws part skips leading whitespace first. This means values can contain spaces (e.g. SET greeting Hello World).
- Save command_parser.cpp.
Seeing red squiggles in VS Code?
If IntelliSense shows errors on #include "lru_cache.h", make sure all your files are in the same folder. IntelliSense resolves relative includes from the file's own directory.
help me fix IntelliSense include errors in my C++ project
Implement the executeCommand function
Now you need a function that takes a parsed command, runs it against the LRU cache, and returns a response string the client will see.
- In command_parser.cpp, add the executeCommand function below parseCommand:
std::string executeCommand(const ParsedCommand& cmd, LRUCache& cache) {
switch (cmd.type) {
case CommandType::SET:
cache.set(cmd.key, cmd.value);
return "OK";
case CommandType::GET: {
std::string val = cache.get(cmd.key);
if (val.empty()) {
return "(nil)";
}
return val;
}
case CommandType::DEL:
if (cache.del(cmd.key)) {
return "(deleted)";
}
return "(nil)";
default:
return "ERROR: unknown command";
}
}
What does this code do?
The switch statement routes each command type to the matching cache method. Responses follow a simple protocol:
- OK for a successful SET.
- The stored value for a successful GET.
- (nil) when a key does not exist.
- (deleted) for a successful DEL.
- ERROR: unknown command for anything else.
- Save command_parser.cpp.
✔️ Awesome, I've got everything!
Great. Double check you have saved both command_parser.h and command_parser.cpp.
ⓧ I'd like to double check the full code
Here is the complete command_parser.h:
#ifndef COMMAND_PARSER_H
#define COMMAND_PARSER_H
#include <string>
#include "lru_cache.h"
enum class CommandType {
SET,
GET,
DEL,
UNKNOWN
};
struct ParsedCommand {
CommandType type;
std::string key;
std::string value;
};
ParsedCommand parseCommand(const std::string& input);
std::string executeCommand(const ParsedCommand& cmd, LRUCache& cache);
#endif
Here is the complete command_parser.cpp:
#include "command_parser.h"
#include <sstream>
ParsedCommand parseCommand(const std::string& input) {
ParsedCommand cmd;
cmd.type = CommandType::UNKNOWN;
std::istringstream stream(input);
std::string command_word;
stream >> command_word;
if (command_word == "SET") {
cmd.type = CommandType::SET;
stream >> cmd.key;
std::getline(stream >> std::ws, cmd.value);
} else if (command_word == "GET") {
cmd.type = CommandType::GET;
stream >> cmd.key;
} else if (command_word == "DEL") {
cmd.type = CommandType::DEL;
stream >> cmd.key;
}
return cmd;
}
std::string executeCommand(const ParsedCommand& cmd, LRUCache& cache) {
switch (cmd.type) {
case CommandType::SET:
cache.set(cmd.key, cmd.value);
return "OK";
case CommandType::GET: {
std::string val = cache.get(cmd.key);
if (val.empty()) {
return "(nil)";
}
return val;
}
case CommandType::DEL:
if (cache.del(cmd.key)) {
return "(deleted)";
}
return "(nil)";
default:
return "ERROR: unknown command";
}
}
Test the command parser
Time to wire the parser and executor together in a test to confirm everything works end-to-end.
- Replace the contents of test_cache.cpp with this test program:
#include <iostream>
#include "lru_cache.h"
#include "command_parser.h"
int main() {
LRUCache cache(10);
// Test command strings matching the protocol
std::string commands[] = {
"SET foo bar",
"GET foo",
"DEL foo",
"GET foo"
};
for (const auto& input : commands) {
ParsedCommand cmd = parseCommand(input);
std::string response = executeCommand(cmd, cache);
std::cout << input << " -> " << response << std::endl;
}
return 0;
}
What does this test do?
The test sends four commands through the full pipeline: parse the string, execute it against the cache, and print the response. This confirms that SET stores a value, GET retrieves it, DEL removes it, and a second GET on the deleted key returns (nil).
- Save test_cache.cpp.
- Compile and run the test by running these commands in your terminal:
g++ -std=c++17 test_cache.cpp lru_cache.cpp command_parser.cpp -o test_cache.exe
.\test_cache.exe
You should see this output:
SET foo bar -> OK
GET foo -> bar
DEL foo -> (deleted)
GET foo -> (nil)
Seeing a compile error?
Make sure you included command_parser.cpp in the compile command. Without it, the linker cannot find the parseCommand and executeCommand definitions.
Also check that your command_parser.h includes "lru_cache.h" and that all files are in the same folder.
help me debug a linker error when compiling my C++ command parser
Your command parser is complete. It can translate raw text into structured commands and execute them against the cache. Next up, you will wrap this in a TCP server so clients can send these commands over the network in real time.
Build the TCP Server
Your LRU cache works perfectly and your command parser turns raw text into structured operations. Now it's time to expose all of that logic over the network so a client can connect and interact with your key-value store in real time.
In this step, you will build a TCP server using Winsock2 that listens for connections, receives commands from a client, passes them through your parser, and sends back responses.
In this step, get ready to:
- Initialize Winsock2 and bind a TCP socket to port 7070.
- Write an accept-and-serve loop that processes client commands.
- Compile and test the full server with a live client connection.
Initialize Winsock2 and create the listening socket
Before your server can accept connections, it needs to initialize the Windows socket library, create a socket, bind it to an address and port, and start listening. This sequence is the standard setup for any Winsock2-based TCP server.
- Open server.cpp in VS Code and add the following initialization code:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>
#include "lru_cache.h"
#include "command_parser.h"
#define DEFAULT_PORT 7070
#define DEFAULT_BUFLEN 512
#define CACHE_CAPACITY 256
int main() {
// Initialize Winsock2
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
std::cerr << "WSAStartup failed: " << iResult << std::endl;
return 1;
}
// Create a TCP socket
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET) {
std::cerr << "socket failed: " << WSAGetLastError() << std::endl;
WSACleanup();
return 1;
}
// Bind socket to localhost on port 7070
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
serverAddr.sin_port = htons(DEFAULT_PORT);
iResult = bind(listenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
if (iResult == SOCKET_ERROR) {
std::cerr << "bind failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
// Start listening for incoming connections
iResult = listen(listenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
std::cerr << "listen failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
std::cout << "Listening on port " << DEFAULT_PORT << std::endl;
// Create the LRU cache with capacity 256
LRUCache cache(CACHE_CAPACITY);
What does this code do?
This sets up the server's networking foundation in four stages:
- WSAStartup(MAKEWORD(2, 2), &wsaData) initializes the Winsock2 library. You must call this before any socket functions.
- socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) creates a TCP socket. AF_INET means IPv4, SOCK_STREAM means reliable byte-stream (TCP).
- bind() attaches the socket to 127.0.0.1:7070 so it only accepts connections from your own machine.
- listen() with SOMAXCONN puts the socket in a state where it waits for incoming connection requests.
- Save server.cpp.
You won't see a change yet. The next chunk adds the accept loop that completes the server.
Write the accept-and-serve loop
The server needs to wait for a client to connect, then enter a loop where it receives commands, processes them through your parser, and sends back responses. When the client disconnects, the server loops back to accept the next connection.
- In server.cpp, add the following code directly after the LRUCache cache(CACHE_CAPACITY); line:
// Accept loop: wait for client connections
while (true) {
SOCKET clientSocket = accept(listenSocket, NULL, NULL);
if (clientSocket == INVALID_SOCKET) {
std::cerr << "accept failed: " << WSAGetLastError() << std::endl;
continue;
}
std::cout << "Client connected." << std::endl;
char recvbuf[DEFAULT_BUFLEN];
int recvResult;
// Recv/send loop: process commands until client disconnects
while (true) {
recvResult = recv(clientSocket, recvbuf, DEFAULT_BUFLEN - 1, 0);
if (recvResult > 0) {
recvbuf[recvResult] = '\0';
std::string input(recvbuf);
// Remove trailing newline/carriage return
while (!input.empty() && (input.back() == '\n' || input.back() == '\r')) {
input.pop_back();
}
if (input.empty()) continue;
ParsedCommand cmd = parseCommand(input);
std::string response = executeCommand(cmd, cache) + "\n";
send(clientSocket, response.c_str(), (int)response.size(), 0);
} else {
break;
}
}
std::cout << "Client disconnected." << std::endl;
closesocket(clientSocket);
}
// Cleanup (unreachable in infinite loop, but good practice)
closesocket(listenSocket);
WSACleanup();
return 0;
}
What does this code do?
This is the server's main runtime logic:
- accept() blocks until a client connects, then returns a new socket for that client.
- recv() reads raw bytes from the client into a buffer. If the client disconnects, it returns 0 and we break out.
- The newline-stripping loop removes \n and \r that clients append when sending lines of text.
- parseCommand() and executeCommand() from your parser module turn the raw text into a cache operation and produce a response string.
- send() sends the response back to the client with a trailing \n so the client sees output on its own line.
- closesocket(clientSocket) releases the client connection when they disconnect, then the outer loop waits for the next client.
The final closesocket(listenSocket) and WSACleanup() are unreachable in an infinite loop but represent proper resource cleanup if you ever add a shutdown signal.
- Save server.cpp.
You won't see a visible change yet. In the next substep, you will compile and test the full server.
Seeing red squiggles in VS Code?
IntelliSense may flag Winsock2 types because it does not know about the -lws2_32 linker flag. These warnings will disappear once you successfully compile.
If the squiggles bother you, check that winsock2.h is included before ws2tcpip.h. The order matters on Windows.
Help me fix IntelliSense errors with Winsock2 in my server.cpp file.
✔️ Awesome, I've got everything!
Great. Double check you have saved server.cpp before moving on to compile.
ⓧ I'd like to double check the full code
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <string>
#include "lru_cache.h"
#include "command_parser.h"
#define DEFAULT_PORT 7070
#define DEFAULT_BUFLEN 512
#define CACHE_CAPACITY 256
int main() {
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
std::cerr << "WSAStartup failed: " << iResult << std::endl;
return 1;
}
SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET) {
std::cerr << "socket failed: " << WSAGetLastError() << std::endl;
WSACleanup();
return 1;
}
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
serverAddr.sin_port = htons(DEFAULT_PORT);
iResult = bind(listenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr));
if (iResult == SOCKET_ERROR) {
std::cerr << "bind failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
iResult = listen(listenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
std::cerr << "listen failed: " << WSAGetLastError() << std::endl;
closesocket(listenSocket);
WSACleanup();
return 1;
}
std::cout << "Listening on port " << DEFAULT_PORT << std::endl;
LRUCache cache(CACHE_CAPACITY);
while (true) {
SOCKET clientSocket = accept(listenSocket, NULL, NULL);
if (clientSocket == INVALID_SOCKET) {
std::cerr << "accept failed: " << WSAGetLastError() << std::endl;
continue;
}
std::cout << "Client connected." << std::endl;
char recvbuf[DEFAULT_BUFLEN];
int recvResult;
while (true) {
recvResult = recv(clientSocket, recvbuf, DEFAULT_BUFLEN - 1, 0);
if (recvResult > 0) {
recvbuf[recvResult] = '\0';
std::string input(recvbuf);
while (!input.empty() && (input.back() == '\n' || input.back() == '\r')) {
input.pop_back();
}
if (input.empty()) continue;
ParsedCommand cmd = parseCommand(input);
std::string response = executeCommand(cmd, cache) + "\n";
send(clientSocket, response.c_str(), (int)response.size(), 0);
} else {
break;
}
}
std::cout << "Client disconnected." << std::endl;
closesocket(clientSocket);
}
closesocket(listenSocket);
WSACleanup();
return 0;
}
Compile and test the server
With the full server code in place, you can now compile all three source files together and run your key-value store.
- Compile the full project by running this command:
g++ -std=c++17 server.cpp lru_cache.cpp command_parser.cpp -o server.exe -lws2_32
What does this compile command do?
- -std=c++17 enables C++17 features.
- The three .cpp files are your server, cache implementation, and command parser.
- -o server.exe names the output executable.
- -lws2_32 links the Winsock2 library, which provides all the socket functions.
- Start the server by running:
.\server.exe
You should see Listening on port 7070 printed in the terminal. The server is now waiting for a client to connect.
- Open a second terminal in VS Code by clicking **Terminal** in the top menu bar, then clicking **New Terminal**.
- Verify the port is reachable by running:
Test-NetConnection -ComputerName localhost -Port 7070
You should see TcpTestSucceeded : True in the output, confirming your server is listening.
- Now send commands to your server by running this PowerShell test script:
# Connect to the server
$client = New-Object System.Net.Sockets.TcpClient("localhost", 7070)
$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$reader = New-Object System.IO.StreamReader($stream)
$writer.AutoFlush = $true
# Test SET command
$writer.WriteLine("SET hello world")
Write-Host "SET hello world ->" $reader.ReadLine()
# Test GET command
$writer.WriteLine("GET hello")
Write-Host "GET hello ->" $reader.ReadLine()
# Test DEL command
$writer.WriteLine("DEL hello")
Write-Host "DEL hello ->" $reader.ReadLine()
# Test GET after DEL
$writer.WriteLine("GET hello")
Write-Host "GET hello ->" $reader.ReadLine()
# Close connection
$client.Close()
You should see the following output in your second terminal:
SET hello world -> OK
GET hello -> world
DEL hello -> (deleted)
GET hello -> (nil)
In your first terminal (where the server is running), you should see Client connected. followed by Client disconnected. after the script finishes.
Server won't compile?
Make sure you are running the compile command from inside your project folder where all five files live. If g++ cannot find lru_cache.h, your terminal is in the wrong directory.
If you see linker errors about undefined references to Winsock functions, confirm that -lws2_32 is at the end of the compile command, not the beginning. Linker flags must come after source files.
If you see "bind failed" with an error code, another process may already be using port 7070. Close any other instances of server.exe running in other terminals.
Help me debug my server.exe compilation or runtime errors.
Why use a PowerShell script instead of telnet?
Windows does not enable telnet by default, and installing ncat requires a separate download. PowerShell's TcpClient class is built into every Windows machine, making it the most accessible way to test a TCP server without installing anything extra.
Your key-value store is fully operational over TCP. In the Secret Mission, you will upgrade it to handle multiple simultaneous clients using threads.
Secret mission
Handle Multiple Concurrent Clients
Your server currently handles one client at a time. A second client must wait until the first disconnects. Can you add multi-threading so multiple clients can connect and interact with the cache simultaneously?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your files around for future tinkering, pause the server process to free up your terminal, or delete everything entirely.
Resources you used:
- The running server.exe process (if still active in your terminal).
- Your project folder on the Desktop (containing server.cpp, lru_cache.cpp, command_parser.cpp, header files, and compiled .exe files).
Keep everything running
No action needed. Choose this if you plan to keep experimenting with your key-value store, adding new commands, or extending the server.
- Your project files stay on your Desktop, ready to compile and run anytime.
- If the server is still running, you can keep sending commands to it or stop it later with Ctrl+C in the terminal.
Pause - I'll come back to this later
Stop the server process to free up your terminal. Your source files stay intact so you can recompile and run again later.
- Switch to the terminal where server.exe is running.
- Press Ctrl+C to stop the server.
- Your source files and compiled executables remain on your Desktop. To restart later, open the project folder in VS Code and run server.exe from the terminal.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your source code and compiled executables permanently.
- If the server is still running, press Ctrl+C in the terminal to stop it.
- Close VS Code.
- Open File Explorer and navigate to your Desktop.
- Right-click your project folder (the one containing server.cpp and lru_cache.h) and select Delete.
- Empty your Recycle Bin if you want to reclaim the disk space immediately.
Nice Work!
Nice Work!
You just built a mini Redis from scratch. Your in-memory key-value store handles real-time commands over TCP, evicts stale entries automatically, and runs entirely from code you wrote yourself.
You've learned how to:
- Build an LRU cache from scratch using std::unordered_map and std::list with O(1) get, set, and delete operations.
- Implement a text-based command protocol (SET/GET/DEL) with a clean parser that translates raw strings into structured cache operations.
- Create a TCP server using Winsock2 that accepts client connections and processes key-value commands over the network.
- Secret Mission: Added multi-threading to handle concurrent clients safely with std::mutex protection.
Ready to quiz yourself?