From 7a7863feea897c648dfd4b765708034badfb181f Mon Sep 17 00:00:00 2001 From: helintongh Date: Mon, 1 Jan 2024 22:29:47 +0800 Subject: [PATCH 1/2] feat: add radix tree router and regex router --- include/cinatra/coro_http_connection.hpp | 54 ++++- include/cinatra/coro_http_request.hpp | 13 ++ include/cinatra/coro_http_router.hpp | 76 +++++++- include/cinatra/coro_radix_tree.hpp | 238 +++++++++++++++++++++++ tests/test_coro_http_server.cpp | 42 ++++ 5 files changed, 411 insertions(+), 12 deletions(-) create mode 100644 include/cinatra/coro_radix_tree.hpp diff --git a/include/cinatra/coro_http_connection.hpp b/include/cinatra/coro_http_connection.hpp index e293ad05..70f73a98 100644 --- a/include/cinatra/coro_http_connection.hpp +++ b/include/cinatra/coro_http_connection.hpp @@ -195,8 +195,58 @@ class coro_http_connection co_await router_.route_coro(coro_handler, request_, response_); } else { - // not found - response_.set_status(status_type::not_found); + bool is_exist = false; + std::function + handler; + params_t params = {}; + std::string method_str; + method_str.assign(parser_.method().data(), parser_.method().length()); + std::string url_path; + url_path.assign(parser_.url().data(), parser_.url().length()); + + std::tie(is_exist, handler, request_.params_) = + router_.get_router_tree()->get(url_path, method_str); + if (is_exist) { + (handler)(request_, response_); + } + else { + bool is_matched_regex_router = false; + // coro regex router + auto coro_regex_handlers = router_.get_coro_regex_handlers(); + if (coro_regex_handlers.size() != 0) { + for (auto &pair : coro_regex_handlers) { + std::string coro_regex_key; + coro_regex_key.assign(key.data(), key.size()); + + if (std::regex_match(coro_regex_key, request_.matches_, + std::get<0>(pair))) { + auto coro_handler = std::get<1>(pair); + co_await (coro_handler)(request_, response_); + is_matched_regex_router = true; + } + } + } + // regex router + if (!is_matched_regex_router) { + auto regex_handlers = router_.get_regex_handlers(); + if (regex_handlers.size() != 0) { + for (auto &pair : regex_handlers) { + std::string regex_key; + regex_key.assign(key.data(), key.size()); + if (std::regex_match(regex_key, request_.matches_, + std::get<0>(pair))) { + auto handler = std::get<1>(pair); + (handler)(request_, response_); + is_matched_regex_router = true; + } + } + } + } + // not found + if (!is_matched_regex_router) + response_.set_status(status_type::not_found); + } } } diff --git a/include/cinatra/coro_http_request.hpp b/include/cinatra/coro_http_request.hpp index 07211d95..8920449d 100644 --- a/include/cinatra/coro_http_request.hpp +++ b/include/cinatra/coro_http_request.hpp @@ -1,10 +1,20 @@ #pragma once +#include + #include "async_simple/coro/Lazy.h" #include "define.h" #include "http_parser.hpp" #include "ws_define.h" namespace cinatra { + +typedef std::pair paramters_t; + +struct params_t { + std::vector parameters; + int size; +}; + class coro_http_connection; class coro_http_request { public: @@ -118,6 +128,9 @@ class coro_http_request { return true; } + params_t params_; + std::smatch matches_; + private: http_parser& parser_; std::string_view body_; diff --git a/include/cinatra/coro_http_router.hpp b/include/cinatra/coro_http_router.hpp index e269a23c..2f832954 100644 --- a/include/cinatra/coro_http_router.hpp +++ b/include/cinatra/coro_http_router.hpp @@ -10,7 +10,9 @@ #include "cinatra/cinatra_log_wrapper.hpp" #include "cinatra/coro_http_request.hpp" +#include "cinatra/coro_radix_tree.hpp" #include "cinatra/response_cv.hpp" +#include "cinatra/utils.hpp" #include "coro_http_response.hpp" #include "ylt/util/type_traits.h" @@ -39,20 +41,56 @@ class coro_http_router { // std::string_view, avoid memcpy when route using return_type = typename util::function_traits::return_type; if constexpr (is_lazy_v) { - auto [it, ok] = coro_keys_.emplace(std::move(whole_str)); - if (!ok) { - CINATRA_LOG_WARNING << key << " has already registered."; - return; + if (whole_str.find("{") != std::string::npos || + whole_str.find(")") != std::string::npos) { + std::string pattern = whole_str; + std::unordered_map params; + params.clear(); + + if (pattern.find("{}") != std::string::npos) { + replace_all(pattern, "{}", "([^/]+)"); + } + + coro_regex_handles_.emplace_back(std::regex(pattern), + std::move(handler)); + } + else { + auto [it, ok] = coro_keys_.emplace(std::move(whole_str)); + if (!ok) { + CINATRA_LOG_WARNING << key << " has already registered."; + return; + } + coro_handles_.emplace(*it, std::move(handler)); } - coro_handles_.emplace(*it, std::move(handler)); } else { - auto [it, ok] = keys_.emplace(std::move(whole_str)); - if (!ok) { - CINATRA_LOG_WARNING << key << " has already registered."; - return; + if (whole_str.find(':') != std::string::npos) { + std::vector method_names = {}; + std::string method_str; + method_str.append(method_name); + method_names.push_back(method_str); + router_tree_->insert(key, std::move(handler), method_names); + } + else if (whole_str.find("{") != std::string::npos || + whole_str.find(")") != std::string::npos) { + std::string pattern = whole_str; + std::unordered_map params; + params.clear(); + + if (pattern.find("{}") != std::string::npos) { + replace_all(pattern, "{}", "([^/]+)"); + } + + regex_handles_.emplace_back(std::regex(pattern), std::move(handler)); + } + else { + auto [it, ok] = keys_.emplace(std::move(whole_str)); + if (!ok) { + CINATRA_LOG_WARNING << key << " has already registered."; + return; + } + map_handles_.emplace(*it, std::move(handler)); } - map_handles_.emplace(*it, std::move(handler)); } } @@ -104,6 +142,12 @@ class coro_http_router { const auto& get_coro_handlers() const { return coro_handles_; } + radix_tree* get_router_tree() { return router_tree_; } + + const auto& get_coro_regex_handlers() { return coro_regex_handles_; } + + const auto& get_regex_handlers() { return regex_handles_; } + private: std::set keys_; std::unordered_map< @@ -116,5 +160,17 @@ class coro_http_router { std::function( coro_http_request& req, coro_http_response& resp)>> coro_handles_; + + radix_tree* router_tree_ = new radix_tree(); + + std::vector>> + regex_handles_; + + std::vector( + coro_http_request& req, coro_http_response& resp)>>> + coro_regex_handles_; }; } // namespace cinatra \ No newline at end of file diff --git a/include/cinatra/coro_radix_tree.hpp b/include/cinatra/coro_radix_tree.hpp new file mode 100644 index 00000000..4580f328 --- /dev/null +++ b/include/cinatra/coro_radix_tree.hpp @@ -0,0 +1,238 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "cinatra/coro_http_request.hpp" +#include "coro_http_response.hpp" +#include "ylt/util/type_traits.h" + +namespace cinatra { +constexpr char type_asterisk = '*'; +constexpr char type_colon = ':'; +constexpr char type_slash = '/'; + +typedef std::tuple< + bool, std::function, + params_t> + parse_result; + +struct handler_t { + std::string method; + std::function handler; +}; + +struct radix_tree_node { + std::string path; + std::vector handlers; + std::string indices; + std::vector children; + int max_params; + + radix_tree_node() = default; + radix_tree_node(const std::string &path) { this->path = path; } + ~radix_tree_node() { + for (auto &c : this->children) delete c; + } + + std::function + get_handler(const std::string &method) { + for (auto &h : this->handlers) { + if (h.method == method) { + return h.handler; + } + } + return nullptr; + } + + int add_handler( + std::function + handler, + const std::vector &methods) { + for (auto &m : methods) { + auto old_handler = this->get_handler(m); + this->handlers.push_back(handler_t{m, handler}); + } + return 0; + } + + radix_tree_node *insert_child(char index, radix_tree_node *child) { + auto i = this->get_index_position(index); + this->indices.insert(this->indices.begin() + i, index); + this->children.insert(this->children.begin() + i, child); + return child; + } + + radix_tree_node *get_child(char index) { + auto i = this->get_index_position(index); + return this->indices[i] != index ? nullptr : this->children[i]; + } + + int get_index_position(char target) { + int low = 0, high = this->indices.size(), mid; + + while (low < high) { + mid = low + ((high - low) >> 1); + if (this->indices[mid] < target) + low = mid + 1; + else + high = mid; + } + return low; + } +}; + +class radix_tree { + public: + radix_tree() { this->root = new radix_tree_node(); } + + ~radix_tree() { delete this->root; } + + int insert( + const std::string &path, + std::function + handler, + const std::vector &methods) { + auto root = this->root; + int i = 0, n = path.size(), param_count = 0, code = 0; + while (i < n) { + if (!root->indices.empty() && + (root->indices[0] == type_asterisk || path[i] == type_asterisk || + (path[i] != type_colon && root->indices[0] == type_colon) || + (path[i] == type_colon && root->indices[0] != type_colon) || + (path[i] == type_colon && root->indices[0] == type_colon && + path.substr(i + 1, find_pos(path, type_slash, i) - i - 1) != + root->children[0]->path))) { + code = -1; + break; + } + + auto child = root->get_child(path[i]); + if (!child) { + auto p = find_pos(path, type_colon, i); + + if (p == n) { + p = find_pos(path, type_asterisk, i); + + root = root->insert_child(path[i], + new radix_tree_node(path.substr(i, p - i))); + + if (p < n) { + root = root->insert_child(type_asterisk, + new radix_tree_node(path.substr(p + 1))); + ++param_count; + } + + code = root->add_handler(handler, methods); + break; + } + + root = root->insert_child(path[i], + new radix_tree_node(path.substr(i, p - i))); + + i = find_pos(path, type_slash, p); + + root = root->insert_child( + type_colon, new radix_tree_node(path.substr(p + 1, i - p - 1))); + ++param_count; + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + else { + root = child; + + if (path[i] == type_colon) { + ++param_count; + i += root->path.size() + 1; + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + else { + auto j = 0UL; + auto m = root->path.size(); + + for (; i < n && j < m && path[i] == root->path[j]; ++i, ++j) { + } + + if (j < m) { + auto child = new radix_tree_node(root->path.substr(j)); + child->handlers = root->handlers; + child->indices = root->indices; + child->children = root->children; + + root->path = root->path.substr(0, j); + root->handlers = {}; + root->indices = child->path[0]; + root->children = {child}; + } + + if (i == n) { + code = root->add_handler(handler, methods); + break; + } + } + } + } + + if (param_count > this->root->max_params) + this->root->max_params = param_count; + + return code; + } + + parse_result get(const std::string &path, const std::string &method) { + params_t params = {{}, 0}; + auto root = this->root; + + int i = 0, n = path.size(), p; + + while (i < n) { + if (root->indices.empty()) + return parse_result(); + + if (root->indices[0] == type_colon) { + root = root->children[0]; + + p = find_pos(path, type_slash, i); + params.parameters.push_back( + paramters_t{root->path, path.substr(i, p - i)}); + params.size++; + i = p; + } + else if (root->indices[0] == type_asterisk) { + root = root->children[0]; + params.parameters.push_back(paramters_t{root->path, path.substr(i)}); + params.size++; + break; + } + else { + root = root->get_child(path[i]); + if (!root || path.substr(i, root->path.size()) != root->path) + return parse_result(); + i += root->path.size(); + } + } + + return parse_result{true, root->get_handler(method), params}; + } + + private: + int find_pos(const std::string &str, char target, int start) { + auto i = str.find(target, start); + return i == -1 ? str.size() : i; + } + + radix_tree_node *root; +}; +} // namespace cinatra \ No newline at end of file diff --git a/tests/test_coro_http_server.cpp b/tests/test_coro_http_server.cpp index 21d56a04..99f43425 100644 --- a/tests/test_coro_http_server.cpp +++ b/tests/test_coro_http_server.cpp @@ -960,6 +960,48 @@ TEST_CASE("test http download server") { } } +TEST_CASE("test restful api") { + cinatra::coro_http_server server(1, 9001); + server.set_http_handler( + "/test/:id/test2/:name", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.params_.parameters[0].first == "id"); + CHECK(req.params_.parameters[0].second == "11"); + CHECK(req.params_.parameters[1].first == "name"); + CHECK(req.params_.parameters[1].second == "cpp"); + response.set_status_and_content(status_type::ok, "ok"); + }); + + server.set_http_handler( + "/test2/{}/test3/{}", + [](coro_http_request &req, + coro_http_response &resp) -> async_simple::coro::Lazy { + co_await coro_io::post([&]() { + // coroutine in other thread. + CHECK(req.matches_.str(1) == "name"); + CHECK(req.matches_.str(2) == "test"); + resp.set_status_and_content(cinatra::status_type::ok, "hello world"); + }); + co_return; + }); + + server.set_http_handler( + R"(/numbers/(\d+)/test/(\d+))", + [](coro_http_request &req, coro_http_response &response) { + CHECK(req.matches_.str(1) == "100"); + CHECK(req.matches_.str(2) == "200"); + response.set_status_and_content(status_type::ok, "number regex ok"); + }); + + server.async_start(); + std::this_thread::sleep_for(200ms); + + coro_http_client client; + client.get("http://127.0.0.1:9001/test/11/test2/cpp"); + client.get("http://127.0.0.1:9001/test2/name/test3/test"); + client.get("http://127.0.0.1:9001/numbers/100/test/200"); +} + DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) int main(int argc, char **argv) { return doctest::Context(argc, argv).run(); } DOCTEST_MSVC_SUPPRESS_WARNING_POP \ No newline at end of file From 5ddab2b3a1c6ca25fffeeea1f818ba85e4a23a80 Mon Sep 17 00:00:00 2001 From: helintongh Date: Mon, 1 Jan 2024 22:51:37 +0800 Subject: [PATCH 2/2] fix: fix memory leak --- include/cinatra/coro_http_router.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/cinatra/coro_http_router.hpp b/include/cinatra/coro_http_router.hpp index 2f832954..d129b9eb 100644 --- a/include/cinatra/coro_http_router.hpp +++ b/include/cinatra/coro_http_router.hpp @@ -30,6 +30,7 @@ constexpr inline bool is_lazy_v = class coro_http_router { public: + ~coro_http_router() { delete router_tree_; } // eg: "GET hello/" as a key template void set_http_handler(std::string key, Func handler) {