rewrite router, add kv and session store
This commit is contained in:
parent
78b2d8f8fb
commit
e3b7bddf05
@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "http_common.hpp"
|
||||
#include "http_request.hpp"
|
||||
#include "router.hpp"
|
||||
#include "cookie.hpp"
|
||||
#include <string_view>
|
||||
@ -10,25 +11,6 @@
|
||||
|
||||
using std::string_view;
|
||||
|
||||
struct HttpRequest {
|
||||
HttpMethod method = HttpMethod::UNKNOWN;
|
||||
string_view path;
|
||||
string_view query;
|
||||
string_view version;
|
||||
string_view body;
|
||||
std::unordered_map<string_view, string_view> headers;
|
||||
std::unordered_map<std::string, std::string> params; // URL parameters
|
||||
std::vector<Cookie> cookies; // Parsed cookies
|
||||
size_t content_length = 0;
|
||||
|
||||
bool valid = false;
|
||||
|
||||
// Cookie helper method
|
||||
string_view get_cookie(string_view name) const {
|
||||
return CookieHelpers::get_cookie(cookies, name);
|
||||
}
|
||||
};
|
||||
|
||||
class HttpParser {
|
||||
public:
|
||||
static HttpRequest parse(string_view data) {
|
||||
|
@ -5,6 +5,9 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
using std::string_view;
|
||||
using std::string;
|
||||
|
||||
struct HttpRequest {
|
||||
HttpMethod method = HttpMethod::UNKNOWN;
|
||||
string_view path;
|
||||
@ -12,9 +15,17 @@ struct HttpRequest {
|
||||
string_view version;
|
||||
string_view body;
|
||||
std::unordered_map<string_view, string_view> headers;
|
||||
std::unordered_map<std::string, std::string> params; // URL parameters
|
||||
std::unordered_map<string, string> params; // URL parameters
|
||||
std::vector<Cookie> cookies; // Parsed cookies
|
||||
size_t content_length = 0;
|
||||
|
||||
bool valid = false;
|
||||
|
||||
string_view get_cookie(string_view name) const {
|
||||
return CookieHelpers::get_cookie(cookies, name);
|
||||
}
|
||||
|
||||
string session_id() const {
|
||||
return string(get_cookie("session_id"));
|
||||
}
|
||||
};
|
||||
|
66
kv_store.hpp
Normal file
66
kv_store.hpp
Normal file
@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
using std::string;
|
||||
|
||||
class KeyValueStore {
|
||||
public:
|
||||
void set(const string& key, const string& value) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
data_[key] = value;
|
||||
}
|
||||
|
||||
std::optional<string> get(const string& key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
return it != data_.end() ? std::optional<string>{it->second} : std::nullopt;
|
||||
}
|
||||
|
||||
bool del(const string& key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return data_.erase(key) > 0;
|
||||
}
|
||||
|
||||
size_t size() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return data_.size();
|
||||
}
|
||||
|
||||
void set_file(const string& filename) {
|
||||
filename_ = filename;
|
||||
}
|
||||
|
||||
void load() {
|
||||
std::ifstream file(filename_);
|
||||
if (!file.is_open()) return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
string line;
|
||||
while (std::getline(file, line)) {
|
||||
size_t eq = line.find('=');
|
||||
if (eq != string::npos) {
|
||||
data_[line.substr(0, eq)] = line.substr(eq + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::ofstream file(filename_);
|
||||
if (!file.is_open()) return;
|
||||
|
||||
for (const auto& [key, value] : data_) {
|
||||
file << key << "=" << value << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<string, string> data_;
|
||||
mutable std::mutex mutex_;
|
||||
string filename_ = "server_store.txt";
|
||||
};
|
16
main.cpp
16
main.cpp
@ -45,7 +45,6 @@ int main() {
|
||||
res.status = 201;
|
||||
});
|
||||
|
||||
// Request info route
|
||||
router.get("/request-info", [](const HttpRequest& req, HttpResponse& res) {
|
||||
std::string info = "Method: " + std::to_string(static_cast<int>(req.method)) + "\n";
|
||||
info += "Path: " + std::string(req.path) + "\n";
|
||||
@ -55,15 +54,16 @@ int main() {
|
||||
res.set_text(info);
|
||||
});
|
||||
|
||||
// Different HTTP methods
|
||||
router.put("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
|
||||
std::string id = req.params.at("id");
|
||||
res.set_json("{\"message\":\"User " + id + " updated\"}");
|
||||
router.get("/foo", [](const HttpRequest& req, HttpResponse& res) {
|
||||
res.set_text("Admin");
|
||||
});
|
||||
|
||||
router.del("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
|
||||
std::string id = req.params.at("id");
|
||||
res.set_json("{\"message\":\"User " + id + " deleted\"}");
|
||||
router.get("/admin/counter", [](const HttpRequest& req, HttpResponse& res) {
|
||||
auto current = server->store.get("visit_count");
|
||||
int count = current ? std::stoi(current.value()) : 0;
|
||||
server->store.set("visit_count", std::to_string(count + 1));
|
||||
|
||||
res.set_text("Counter: " + std::to_string(count + 1));
|
||||
});
|
||||
|
||||
server = new HttpServer(8080, router);
|
||||
|
77
router.hpp
77
router.hpp
@ -15,13 +15,12 @@ using std::string_view;
|
||||
using Handler = std::function<void(const HttpRequest&, HttpResponse&)>;
|
||||
|
||||
struct TrieNode {
|
||||
std::string prefix;
|
||||
std::string param_name; // For parameter nodes
|
||||
std::string param_name;
|
||||
std::unordered_map<HttpMethod, Handler> handlers;
|
||||
std::unordered_map<char, std::unique_ptr<TrieNode>> children;
|
||||
bool is_param = false;
|
||||
std::unordered_map<std::string, std::unique_ptr<TrieNode>> children;
|
||||
std::unique_ptr<TrieNode> param_child;
|
||||
|
||||
TrieNode(string_view p = "") : prefix(p) {}
|
||||
TrieNode() = default;
|
||||
};
|
||||
|
||||
class Router {
|
||||
@ -39,7 +38,7 @@ private:
|
||||
}
|
||||
|
||||
bool is_param = path[i] == ':';
|
||||
if (is_param) i++; // skip ':'
|
||||
if (is_param) i++;
|
||||
|
||||
size_t start = i;
|
||||
while (i < path.length() && path[i] != '/') i++;
|
||||
@ -47,31 +46,17 @@ private:
|
||||
std::string segment(path.substr(start, i - start));
|
||||
|
||||
if (is_param) {
|
||||
// All parameters share the same node
|
||||
auto it = current->children.find(':');
|
||||
if (it == current->children.end()) {
|
||||
auto new_node = std::make_unique<TrieNode>();
|
||||
new_node->is_param = true;
|
||||
new_node->param_name = segment;
|
||||
current->children[':'] = std::move(new_node);
|
||||
if (!current->param_child) {
|
||||
current->param_child = std::make_unique<TrieNode>();
|
||||
}
|
||||
current = current->children[':'].get();
|
||||
current->param_child->param_name = segment;
|
||||
current = current->param_child.get();
|
||||
} else {
|
||||
char first_char = segment[0];
|
||||
auto it = current->children.find(first_char);
|
||||
auto it = current->children.find(segment);
|
||||
if (it == current->children.end()) {
|
||||
auto new_node = std::make_unique<TrieNode>(segment);
|
||||
current->children[first_char] = std::move(new_node);
|
||||
current = current->children[first_char].get();
|
||||
} else {
|
||||
current = it->second.get();
|
||||
if (current->prefix != segment) {
|
||||
// Handle prefix mismatch - split node if needed
|
||||
auto new_node = std::make_unique<TrieNode>(segment);
|
||||
current->children[first_char] = std::move(new_node);
|
||||
current = current->children[first_char].get();
|
||||
}
|
||||
current->children[segment] = std::make_unique<TrieNode>();
|
||||
}
|
||||
current = current->children[segment].get();
|
||||
}
|
||||
}
|
||||
return current;
|
||||
@ -91,19 +76,19 @@ private:
|
||||
while (i < path.length() && path[i] != '/') i++;
|
||||
|
||||
string_view segment = path.substr(start, i - start);
|
||||
std::string segment_str(segment);
|
||||
|
||||
// Try exact match first
|
||||
auto it = current->children.find(segment[0]);
|
||||
if (it != current->children.end() && it->second->prefix == segment) {
|
||||
auto it = current->children.find(segment_str);
|
||||
if (it != current->children.end()) {
|
||||
current = it->second.get();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try parameter match
|
||||
auto param_it = current->children.find(':');
|
||||
if (param_it != current->children.end()) {
|
||||
current = param_it->second.get();
|
||||
params[current->param_name] = std::string(segment);
|
||||
if (current->param_child) {
|
||||
params[current->param_child->param_name] = segment_str;
|
||||
current = current->param_child.get();
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -153,29 +138,3 @@ public:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Usage example:
|
||||
/*
|
||||
Router router;
|
||||
|
||||
router.get("/users", [](const HttpRequest& req, HttpResponse& res) {
|
||||
res.set_json("{\"users\": []}");
|
||||
});
|
||||
|
||||
router.get("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
|
||||
std::string id = req.params.at("id");
|
||||
res.set_json("{\"id\": \"" + id + "\"}");
|
||||
});
|
||||
|
||||
router.post("/users", [](const HttpRequest& req, HttpResponse& res) {
|
||||
// Use req.body to get POST data
|
||||
res.status = 201;
|
||||
res.set_json("{\"created\": true}");
|
||||
});
|
||||
|
||||
router.get("/users/:id/posts/:postId", [](const HttpRequest& req, HttpResponse& res) {
|
||||
std::string userId = req.params.at("id");
|
||||
std::string postId = req.params.at("postId");
|
||||
res.set_json("{\"userId\": \"" + userId + "\", \"postId\": \"" + postId + "\"}");
|
||||
});
|
||||
*/
|
||||
|
45
server.hpp
45
server.hpp
@ -5,6 +5,8 @@
|
||||
#include "http_parser.hpp"
|
||||
#include "http_response.hpp"
|
||||
#include "static_file_handler.hpp"
|
||||
#include "kv_store.hpp"
|
||||
#include "session_store.hpp"
|
||||
#include <iostream>
|
||||
#include <string.h>
|
||||
#include <string_view>
|
||||
@ -16,10 +18,15 @@
|
||||
|
||||
class HttpServer {
|
||||
public:
|
||||
KeyValueStore store;
|
||||
SessionStore sessions;
|
||||
|
||||
explicit HttpServer(uint16_t port, Router& router) : port_(port), router_(router) {}
|
||||
|
||||
~HttpServer() {
|
||||
stop();
|
||||
store.save();
|
||||
sessions.save();
|
||||
for (auto& worker : workers_) {
|
||||
if (worker->thread.joinable()) {
|
||||
worker->thread.join();
|
||||
@ -28,13 +35,16 @@ public:
|
||||
}
|
||||
|
||||
bool start() {
|
||||
store.load();
|
||||
sessions.load();
|
||||
|
||||
unsigned int num_cores = std::thread::hardware_concurrency();
|
||||
if (num_cores == 0) num_cores = 1;
|
||||
|
||||
workers_.reserve(num_cores);
|
||||
|
||||
for (unsigned int i = 0; i < num_cores; ++i) {
|
||||
auto worker = std::make_unique<Worker>(port_, router_, static_handler_);
|
||||
auto worker = std::make_unique<Worker>(port_, router_, static_handler_, sessions);
|
||||
if (!worker->socket.start()) return false;
|
||||
workers_.push_back(std::move(worker));
|
||||
}
|
||||
@ -95,11 +105,12 @@ private:
|
||||
EpollSocket socket;
|
||||
Router& router;
|
||||
std::shared_ptr<StaticFileHandler>& static_handler;
|
||||
SessionStore& sessions;
|
||||
std::array<char, BUFFER_SIZE> buffer;
|
||||
std::thread thread;
|
||||
|
||||
Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh)
|
||||
: socket(port), router(r), static_handler(sh) {
|
||||
Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh, SessionStore& s)
|
||||
: socket(port), router(r), static_handler(sh), sessions(s) {
|
||||
socket.on_connection([this](int fd) { handle_connection(fd); });
|
||||
socket.on_data([this](int fd) { handle_data(fd); });
|
||||
socket.on_disconnect([this](int fd) { handle_disconnect(fd); });
|
||||
@ -139,24 +150,52 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle session
|
||||
std::string session_id = get_session_id(req);
|
||||
if (session_id.empty()) {
|
||||
session_id = sessions.create();
|
||||
}
|
||||
|
||||
HttpResponse response;
|
||||
|
||||
// Try router first
|
||||
if (router.handle(req, response)) {
|
||||
set_session_cookie(response, session_id);
|
||||
send_http_response(client_fd, response, req.version);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then try static files
|
||||
if (static_handler && static_handler->handle(req, response)) {
|
||||
set_session_cookie(response, session_id);
|
||||
send_http_response(client_fd, response, req.version);
|
||||
} else {
|
||||
response.status = 404;
|
||||
response.set_text("Not Found");
|
||||
set_session_cookie(response, session_id);
|
||||
send_http_response(client_fd, response, req.version);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_session_id(const HttpRequest& req) {
|
||||
auto it = req.headers.find("Cookie");
|
||||
if (it == req.headers.end()) return "";
|
||||
|
||||
std::string_view cookies = it->second;
|
||||
size_t pos = cookies.find("session_id=");
|
||||
if (pos == std::string_view::npos) return "";
|
||||
|
||||
pos += 11; // length of "session_id="
|
||||
size_t end = cookies.find(';', pos);
|
||||
if (end == std::string_view::npos) end = cookies.length();
|
||||
|
||||
return std::string(cookies.substr(pos, end - pos));
|
||||
}
|
||||
|
||||
void set_session_cookie(HttpResponse& response, const std::string& session_id) {
|
||||
response.headers["Set-Cookie"] = "session_id=" + session_id + "; HttpOnly; Path=/; SameSite=Strict";
|
||||
}
|
||||
|
||||
void send_http_response(int client_fd, const HttpResponse& response, string_view version) {
|
||||
std::string http_response = HttpResponseBuilder::build_response(response, version);
|
||||
send_raw_response(client_fd, http_response);
|
||||
|
78
session_store.hpp
Normal file
78
session_store.hpp
Normal file
@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "kv_store.hpp"
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
class SessionStore {
|
||||
public:
|
||||
SessionStore() : rng_(std::random_device{}()) {}
|
||||
|
||||
std::string create() {
|
||||
std::string id = generate_id();
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
sessions_[id] = {};
|
||||
return id;
|
||||
}
|
||||
|
||||
std::optional<std::string> get(const std::string& id, const std::string& key) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = sessions_.find(id);
|
||||
if (it == sessions_.end()) return std::nullopt;
|
||||
|
||||
auto data_it = it->second.find(key);
|
||||
return data_it != it->second.end() ? std::optional<std::string>{data_it->second} : std::nullopt;
|
||||
}
|
||||
|
||||
void set(const std::string& id, const std::string& key, const std::string& value) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
sessions_[id][key] = value;
|
||||
}
|
||||
|
||||
bool del(const std::string& id) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return sessions_.erase(id) > 0;
|
||||
}
|
||||
|
||||
size_t size() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return sessions_.size();
|
||||
}
|
||||
|
||||
void set_file(const std::string& filename) {
|
||||
store_.set_file(filename);
|
||||
}
|
||||
|
||||
void load() {
|
||||
store_.load();
|
||||
}
|
||||
|
||||
void save() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
for (const auto& [id, data] : sessions_) {
|
||||
for (const auto& [key, value] : data) {
|
||||
store_.set(id + ":" + key, value);
|
||||
}
|
||||
}
|
||||
store_.save();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> sessions_;
|
||||
mutable std::mutex mutex_;
|
||||
KeyValueStore store_;
|
||||
std::mt19937 rng_;
|
||||
|
||||
std::string generate_id() {
|
||||
static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
std::string id;
|
||||
id.reserve(16);
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
id += chars[rng_() % (sizeof(chars) - 1)];
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user