Skip to content

Commit 3d2cb1f

Browse files
itheweiCopilot
andauthored
Optimize http router using trie (#833) (#836)
* feat: optimize http router using trie and fix wildcard match (#833) * docs: update PLAN.md * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent bdd9a32 commit 3d2cb1f

9 files changed

Lines changed: 421 additions & 107 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ unittest: prepare
281281
$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/synchronized_test unittest/synchronized_test.cpp -pthread
282282
$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/threadpool_test unittest/threadpool_test.cpp -pthread
283283
$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Icpputil -o bin/objectpool_test unittest/objectpool_test.cpp -pthread
284+
$(CXX) -g -Wall -O0 -std=c++11 -Ihttp/server -o bin/http_router_test unittest/http_router_test.cpp
284285
$(CXX) -g -Wall -O0 -std=c++11 -I. -Ibase -Issl -Ievent -Ievpp -Icpputil -Ihttp -Ihttp/client -Ihttp/server -o bin/sizeof_test unittest/sizeof_test.cpp
285286
$(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/nslookup unittest/nslookup_test.c protocol/dns.c base/hsocket.c base/htime.c
286287
$(CC) -g -Wall -O0 -std=c99 -I. -Ibase -Iprotocol -o bin/ping unittest/ping_test.c protocol/icmp.c base/hsocket.c base/htime.c -DPRINT_DEBUG

docs/PLAN.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@
99
- websocket client/server
1010
- mqtt client
1111

12-
## Improving
13-
14-
- Path router: optimized matching via trie?
15-
1612
## Plan
1713

1814
- redis client
@@ -21,7 +17,6 @@
2117
- js binding
2218
- hrpc = libhv + protobuf
2319
- rudp: FEC, ARQ, UDT, QUIC
24-
- kcptun
2520
- coroutine
2621
- cppsocket.io
2722
- IM-libhv

http/server/HttpHandler.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ void HttpHandler::onHeadersComplete() {
284284
++p;
285285
}
286286

287-
if (service && service->pathHandlers.size() != 0) {
287+
if (service && service->HasRoutes()) {
288288
service->GetRoute(pReq, &api_handler);
289289
}
290290

http/server/HttpRouter.h

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#ifndef HV_HTTP_ROUTER_H_
2+
#define HV_HTTP_ROUTER_H_
3+
4+
#include <map>
5+
#include <memory>
6+
#include <string>
7+
#include <unordered_map>
8+
#include <vector>
9+
10+
namespace hv {
11+
12+
template<typename Handler>
13+
struct RouteNode {
14+
std::unordered_map<std::string, std::unique_ptr<RouteNode<Handler>>> children;
15+
std::unique_ptr<RouteNode<Handler>> param_child;
16+
std::string param_name;
17+
Handler handler;
18+
bool has_handler;
19+
20+
RouteNode() : has_handler(false) {}
21+
};
22+
23+
template<typename Handler>
24+
struct WildcardRoute {
25+
std::string pattern;
26+
std::string prefix;
27+
std::string suffix;
28+
Handler handler;
29+
30+
bool Match(const std::string& path) const {
31+
if (!prefix.empty() && path.compare(0, prefix.size(), prefix) != 0) {
32+
return false;
33+
}
34+
if (suffix.empty()) {
35+
return true;
36+
}
37+
return path.size() >= prefix.size() + suffix.size() &&
38+
path.compare(path.size() - suffix.size(), suffix.size(), suffix) == 0;
39+
}
40+
};
41+
42+
namespace detail {
43+
44+
inline std::vector<std::string> splitPathSegments(const std::string& path) {
45+
std::vector<std::string> segments;
46+
const char* p = path.c_str();
47+
const char* segment = NULL;
48+
while (*p != '\0') {
49+
if (*p == '/') {
50+
if (segment != NULL) {
51+
segments.push_back(std::string(segment, p - segment));
52+
segment = NULL;
53+
}
54+
}
55+
else if (segment == NULL) {
56+
segment = p;
57+
}
58+
++p;
59+
}
60+
if (segment != NULL) {
61+
segments.push_back(std::string(segment, p - segment));
62+
}
63+
return segments;
64+
}
65+
66+
inline bool isParamSegment(const std::string& segment) {
67+
// RESTful style 1 /user/:id
68+
// RESTful style 2 /user/{id}
69+
return (!segment.empty() && segment[0] == ':') ||
70+
(segment.size() >= 3 && segment.front() == '{' && segment.back() == '}');
71+
}
72+
73+
inline std::string paramNameOf(const std::string& segment) {
74+
if (!segment.empty() && segment[0] == ':') {
75+
return segment.substr(1);
76+
}
77+
if (segment.size() >= 3 && segment.front() == '{' && segment.back() == '}') {
78+
return segment.substr(1, segment.size() - 2);
79+
}
80+
return std::string();
81+
}
82+
83+
template<typename Handler>
84+
inline bool matchNode(const RouteNode<Handler>* node, const std::vector<std::string>& segments, size_t index, Handler& handler, std::map<std::string, std::string>& params) {
85+
if (index == segments.size()) {
86+
if (!node->has_handler) {
87+
return false;
88+
}
89+
handler = node->handler;
90+
return true;
91+
}
92+
93+
auto literal_iter = node->children.find(segments[index]);
94+
if (literal_iter != node->children.end() && matchNode(literal_iter->second.get(), segments, index + 1, handler, params)) {
95+
return true;
96+
}
97+
98+
if (node->param_child && matchNode(node->param_child.get(), segments, index + 1, handler, params)) {
99+
params[node->param_name] = segments[index];
100+
return true;
101+
}
102+
103+
return false;
104+
}
105+
106+
} // namespace detail
107+
108+
template<typename Handler>
109+
class HttpRouter {
110+
public:
111+
HttpRouter() : has_param_routes_(false) {}
112+
113+
void Clear() {
114+
routes_.clear();
115+
param_root_ = RouteNode<Handler>();
116+
has_param_routes_ = false;
117+
wildcard_routes_.clear();
118+
}
119+
120+
void Insert(const std::string& path, const Handler& handler) {
121+
// all routes
122+
routes_[path] = handler;
123+
124+
// wildcard routes
125+
size_t wildcard_pos = path.find('*');
126+
if (wildcard_pos != std::string::npos) {
127+
for (auto& route : wildcard_routes_) {
128+
if (route.pattern == path) {
129+
route.handler = handler;
130+
return;
131+
}
132+
}
133+
WildcardRoute<Handler> route;
134+
route.pattern = path;
135+
route.prefix = path.substr(0, wildcard_pos);
136+
route.suffix = path.substr(wildcard_pos + 1);
137+
route.handler = handler;
138+
wildcard_routes_.push_back(route);
139+
return;
140+
}
141+
142+
// param routes
143+
if (path.find("/:") != std::string::npos || path.find("/{") != std::string::npos) {
144+
std::vector<std::string> segments = detail::splitPathSegments(path);
145+
RouteNode<Handler>* node = &param_root_;
146+
for (const auto& segment : segments) {
147+
if (detail::isParamSegment(segment)) {
148+
if (!node->param_child) {
149+
node->param_child.reset(new RouteNode<Handler>());
150+
}
151+
node->param_name = detail::paramNameOf(segment);
152+
node = node->param_child.get();
153+
continue;
154+
}
155+
156+
std::unique_ptr<RouteNode<Handler>>& child = node->children[segment];
157+
if (!child) {
158+
child.reset(new RouteNode<Handler>());
159+
}
160+
node = child.get();
161+
}
162+
node->handler = handler;
163+
node->has_handler = true;
164+
has_param_routes_ = true;
165+
}
166+
}
167+
168+
bool Find(const std::string& path, Handler& handler) const {
169+
auto route_iter = routes_.find(path);
170+
if (route_iter == routes_.end()) {
171+
return false;
172+
}
173+
handler = route_iter->second;
174+
return true;
175+
}
176+
177+
bool MatchParam(const std::string& path, Handler& handler, std::map<std::string, std::string>& params) const {
178+
if (!has_param_routes_) {
179+
return false;
180+
}
181+
std::vector<std::string> segments = detail::splitPathSegments(path);
182+
return detail::matchNode(&param_root_, segments, 0, handler, params);
183+
}
184+
185+
bool MatchWildcard(const std::string& path, Handler& handler) const {
186+
for (const auto& wildcard_route : wildcard_routes_) {
187+
if (wildcard_route.Match(path)) {
188+
handler = wildcard_route.handler;
189+
return true;
190+
}
191+
}
192+
return false;
193+
}
194+
195+
bool Match(const std::string& path, Handler& handler, std::map<std::string, std::string>& params) const {
196+
// Literal > Param > Wildcard
197+
return Find(path, handler) ||
198+
MatchParam(path, handler, params) ||
199+
MatchWildcard(path, handler);
200+
}
201+
202+
bool Empty() const {
203+
return routes_.empty();
204+
}
205+
206+
std::vector<std::string> Paths() const {
207+
std::vector<std::string> paths;
208+
paths.reserve(routes_.size());
209+
for (const auto& route : routes_) {
210+
paths.push_back(route.first);
211+
}
212+
return paths;
213+
}
214+
215+
private:
216+
std::unordered_map<std::string, Handler> routes_;
217+
RouteNode<Handler> param_root_;
218+
bool has_param_routes_;
219+
std::vector<WildcardRoute<Handler>> wildcard_routes_;
220+
};
221+
222+
}
223+
224+
#endif // HV_HTTP_ROUTER_H_

0 commit comments

Comments
 (0)