Based on source-code verification (
paozhu(https://github.com/hggq/paozhu),drogon(https://github.com/drogonframework/drogon), both retrieved Oct 2026). Key finding: drogon-master (master branch checked out Oct 2026) has no HTTP/2 server implementation inlib/ortrantor/(noHttp2Server/Http2Connection;lib/src/HttpServer.cconly outputsHTTP/1.1, noh2/ALPN registration; trantor only has TLS-layer ALPN stubs). Paozhu ships HTTP/2 built-in (http2_parse.cppand related). All Drogon HTTP/2 references below are scoped to the actual code present in this checkout — Drogon's mainline had not yet landed an HTTP/2 server at time of writing, making Paozhu's HTTP/2 a genuine differentiator.
io_context + Asio C++20 coroutines throughout the stack (even ORM queries are coroutines). HTTP/1.1 and HTTP/2 both custom-built. "Full async coroutine + multi-tenant" orientation.| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Underlying lib | Standalone Asio (#include <asio.hpp>, asio::io_context exposed at the bottom, full compatibility with the Asio ecosystem) |
trantor (in-house network lib, muduo branch, epoll_wait + timerfd + min-heap, not an asio::io_context, Asio ecosystem libraries cannot directly integrate) |
| Event loop topology | Single shared io_context, N worker threads each calling io_context.run() |
One EventLoop per thread (one loop per thread, muduo style), multiple ioLoops running in parallel |
| Connection dispatch | Each connection is bound to an asio::strand (client_session.h:194) to guarantee protocol ordering |
TcpServer uses nextLoopIdx_ round-robin to assign new connections to an ioLoop (TcpServer.cc:65) |
| Cross-thread communication | Coroutine co_await + strand serialization |
Lock-free MPSC queue funcs_ + wakeupFd_ (EventLoop.h:311-321) |
| Business threads | Dedicated business thread pool clientrunpool (server.h:487, hardware*2+2, dynamically resizable). Synchronous handlers run inside the pool |
Handlers default to running on the owning ioLoop thread; can manually offload or coroutine-switch threads |
| Thread config | cothreadnum, threadmax (conf/server.conf) |
numThreads / EventLoopThreadPool size |
Philosophical divergence:
io_context, multiple workers" is the classic Asio model. Coroutine frames live on the heap and don't occupy I/O threads. I/O and business are explicitly layered (I/O coroutines vs. business clientrunpool). router::co_resolve dispatches incrementally: coroutine steps co_await in place; synchronous steps go to the business pool (server.cpp:2351 comment).This is the most concrete illustration of the two architectures' core difference. Suppose four blocking handlers (slow SQL, heavy computation, file I/O) arrive at the same time — what happens?
| Framework | Thread model | Outcome |
|---|---|---|
| Drogon | Handlers run on the owning ioLoop thread by default. 4 threads = 4 ioLoops |
Clogs. All 4 ioLoops are pinned by blocking handlers, epoll_wait is never called, every other connection's read/write events on those threads queue up — 4 threads all blocked = the whole service freezes |
| Paozhu | I/O thread pool handles accept/read/write only; blocking work goes to clientrunpool |
Doesn't clog. I/O threads never run business logic. Even if the business pool is fully saturated (queue buildup), I/O threads keep accepting new connections, reading requests, and returning overload responses (e.g. 503) — the service stays alive |
Drogon's mitigation paths (but there is no built-in business thread pool):
Drogon's architecture simply has no such thing as a business thread pool — handlers run directly on their owning ioLoop thread. So a blocking handler can avoid pinning the ioLoop only through coroutine coverage or user-built infrastructure:
Mitigation A: Full coroutines (covers most scenarios, but has hard limits)
Drogon handlers can be written as coroutine functions. As long as every blocking operation has an async API — database via CoroMapper, outbound HTTP via co_await, file I/O via async versions — co_await suspends and the ioLoop is free to serve other connections. This works well.
But coroutines can't save operations with no async API. Think Chromium PDF generation, ffmpeg transcoding, OpenCV image processing, AI inference models — pure CPU computation or libraries that only expose synchronous interfaces. co_await does nothing for them — they run straight on the ioLoop and block it.
// ✅ Coroutine helps: DB query (has async API)
METHOD_ADD(MyController::slowQuery, "/query", Get) {
auto result = co_await drogon::orm::CoroMapper(drogon::app().getDbClient()).filter(...).exec();
// co_await yields, ioLoop is free to serve other connections
}
// ❌ Coroutine can't help: PDF generation (pure CPU, Chromium only has sync API)
METHOD_ADD(MyController::genPdf, "/pdf", Get) {
auto pdf = chromium.generatePdf(html); // synchronous block — pins the ioLoop!
res->setBody(pdf);
res->send();
}
Mitigation B: User-built business thread pool
For scenarios coroutines can't cover (pure computation, sync third-party libs), Drogon's architecture has zero built-in business thread pool or offload mechanism. Developers must design and implement this themselves — custom std::thread pools, task queues, inter-thread communication. There is no generic offload solution in the Drogon ecosystem to pull off the shelf.
Mitigation C: Multi-instance + Nginx load balancing
Spread load across instances at the architecture level — if one clogs, others keep serving.
But every option above is opt-in, developer-initiated. By default, a handler runs straight on its ioLoop thread. No manual offload = blocking = every other connection on that ioLoop queueing up = production incident.
Paozhu's advantage (architectural safety net — no developer action required): I/O and business are inherently separated. Handlers run on a dedicated clientrunpool business thread pool. Even if a synchronous handler contains blocking code, it only queues the business pool — I/O threads keep accepting connections, reading requests, and returning overload responses. Coroutine handlers go further: co_await suspends rather than blocks, making thread occupation very rare.
Bottom-line divergence:
| Paozhu | Drogon | |
|---|---|---|
| Default behavior | Blocking handler → clogs business pool only → I/O layer unaffected | Blocking handler → clogs ioLoop → every connection on that thread freezes |
| Offload | Built-in via architecture — no manual work needed | DB only (CoroMapper) or self-managed threading |
| Developer mindset | Blocking code won't take the service down | Must offload intentionally to be safe |
| Tradeoff | Higher architectural complexity (dual thread pools, strand/coroutine scheduling) | Single-thread Reactor — simpler model, easier to reason about |
The deepest design-philosophy divergence: OOM-prevention vs DDoS-prevention
The "safe by default vs dangerous by default" distinction above is surface level. Beneath it, each framework's architecture is engineered to defend against a different class of disaster entirely:
| Paozhu | Drogon | |
|---|---|---|
| Defends against | OOM / service freeze (business-layer runaway) | DDoS / linear memory growth (connection-layer runaway) |
| Core mechanism | I/O and business are inherently separated: add_sync_task returns false when the pool can't take more → I/O layer returns fail_503 but keeps accepting connections; deadlock detection + 15s watchdog exit prevents unbounded queue growth |
I/O thread count is fixed (setThreadNum): 4 threads whether 1k or 100k connections arrive; HttpConnectionLimit plugin caps total connections |
| The arithmetic | Without a reject path, 100k queued requests × 1KB avg = 100MB and climbing → OOM | 100k connections × 8MB thread stack each = 800GB — the single-thread Reactor model eliminates this by design |
| Tradeoff | Higher architectural complexity (dual thread pools, strand, coroutine scheduling) | Handler runs on ioLoop — blocking = every connection on that thread freezes |
Drogon's DDoS resilience is widely acknowledged in the industry — muduo single-thread Reactor + fixed threads + epoll means memory doesn't grow with connection count. Paozhu's OOM resilience is an architectural safety net — the business layer can blow up any which way, the I/O layer keeps breathing and never gets dragged into a freeze.
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| HTTP/1.1 | Custom parser http_parse.cpp (122KB) + request.cpp / httppeer.cpp. No HTTP/1.1 pipeline (request-response serial; however, pipeline is practically dead — Chrome and Firefox never enabled it by default, Firefox 54 removed it entirely, Nginx defaulted to HTTP/1.0 until 1.29.7 which has no pipeline at all; HTTP/2 multiplexing + h2c (plaintext) fully covers this — Paozhu supports h2c so internal proxies and inter-service calls enjoy multiplexing without TLS overhead, head-of-line blocking solved natively) |
Custom state machine HttpRequestParser.cc (kExpectMethod → ...). Full HTTP/1.1 pipeline (server-side pushResponseToPipelining / popReadyResponses queues responses in receive order, configurable pipeliningRequestsNumber max depth; client-side setPipeliningDepth() for batch sends; only effective on HTTP/1.1 connections, TCP head-of-line blocking remains) |
| HTTP/2 | Built-in: http2_parse.cpp (192KB) + http2_frame/huffman/ring_queue/send_queue, including HPACK, ALPN, h2c, enabled via http2_enable=1 |
drogon-master's lib/ has no HTTP/2 server implementation (no Http2Server/Http2Connection; HttpServer.cc only outputs HTTP/1.1, no h2/ALPN registration); trantor only has TLS-layer ALPN capability stubs |
| Request/response model | httppeer (httppeer.h:41, enable_shared_from_this) holding urlpath / get / post / output / session |
Abstract classes HttpRequest/HttpResponse + Impl (HttpRequestImpl/HttpResponseImpl, serialized via renderToBuffer) |
| Caveat | request.h names a built-in JSON micro-object obj_val, not an HTTP request body |
Passed around via HttpRequestPtr smart pointer; implicit conversion via fromRequest<T>/toRequest<T> |
Note:
request.his a misleading name — Paozhu's actual HTTP request model ishttppeer.
Neither does "real-time compression on every request" — both use a cache after the first access. But the caching strategies are diametrically opposed: Paozhu lazily compresses and auto-writes to disk; Drogon expects pre-compressed files to already exist.
Paozhu — Lazy cache + auto-reclaim (server.cpp:395-518):
rename atomic write (safe for concurrent first hits).{original_path}.{hash(host+"\n"+urlpath)}.br or .gzip. Subsequent hits just fopen and serve — zero CPU cost.content_length > 16MB (saves memory).mtime back on large-file hits; background daemon reclaims cold entries based on CONST_HTTP_STATIC_CACHE_REFRESH_AGE.Drogon — Pre-compressed file lookup (config.example.json:296-301 + StaticFileRouter.cc):
gzip_static: true (default), brotli_static: false. On request, framework directly looks for pre-compressed .gz / .br files in the same directory. If not found, serves original.gzip -k / brotli in CI/CD deploy pipelines. Framework itself never computes compression.IOThreadStorage-isolated static file content cache (StaticFileRouter.h:150-153, per-EventLoop isolation, lock-free).Essential difference: Paozhu makes compression caching an auto-managed framework concern (lazy compression + atomic write + bad-cache guard + reclaim) — zero developer cost. Drogon pushes compression caching to the deployment phase — framework only reads, never writes, keeping itself lighter. Both have equivalent runtime performance (zero CPU after cache hit), but the first-access path and operational model are different.
Both use "declarative route registration", but the mechanisms fork: Drogon uses compile-time reflection + macros + static self-registration; Paozhu uses source annotations + build-time codegen generating
reg_rawtables. Additionally, Paozhu extra-provides simple task orchestration based onhttppeer::flow_method(see §3.4). Drogon has no equivalent.
Paozhu (controller/src/*.cpp with //@urlpath(pre,urlpath):mask, scanned by paozhu_pre at build time):
common/autocontrolmethod.hpp contains a reg_raw call table (autocontrolmethod.hpp:127-141): reg_raw(site, path, pre_sync, co_pre, reg_sync, co_reg, mask_code). Each route registers 5 slots: synchronous pre, co_await-pre, synchronous handler, coroutine handler, and mask_code (numeric in the //@urlpath annotation distinguishing HTTP methods).:param segment get a separate reg_urlpath(site, path, {param_names...}) table right after (autocontrolmethod.hpp:139-140).common/autorestfulpaths.hpp, common/reghttpmethod.hpp, etc.Drogon (via HttpController.h macros, compile-time):
METHOD_ADD(method, pattern, ...) / ADD_METHOD_TO(method, path, ...) (HttpController.h:31-34) → registerMethod(&method, pattern, {...}, ...). Also supports ADD_METHOD_VIA_REGEX (:35) for regex patterns.class methodRegistrator + static methodRegistrator registrator_ (HttpController.h:127-139). The constructor registers the method name + parameter pack into the framework — completed by CRT static init before main(), zero runtime scanning. Class-name-to-path mapping is auto-derived by DrClassMap::demangle(typeid(T).name()) (HttpBinder.h:235), :: becomes /.Paozhu: Core router class (router.h:32). At runtime, reg_raw populates _handlers / _urlpath_map / _site_to_slot (global slot 0; multi-tenant see §3.5). co_resolve(peer, known_idx) (router.h:79) coroutine-drives the entire match + pre + handler chain, with route_exact_checked optimization (httppeer.h:298-299, RPC/sync chains skip redundant exact lookup).
router.cpp two-level lookup: lookup_exact → lookup_registered → lookup_key_in_slots, router.cpp:180-185). Unregistered hop names are skipped during chain draining (router.cpp:350-363).Drogon: HttpControllersRouter + StaticFileRouter. Three structures (HttpControllersRouter.cc):
simpleCtrlMap_: no-parameter exact match (lowercase-keyed std::unordered_map).ctrlMap_: parameterized but structurally fixed exact routes. ctrlMap_.find(loweredPath) directly hash-hits (HttpControllersRouter.cc:626).ctrlVector_: routes with :param / regex. At compile time {param} placeholders are replaced with ([^/]*) via regex_replace (:564). At runtime, std::regex_match does linear matching (:635-640).binders_[req->method()] buckets by HttpMethod; initMiddlewaresAndCorsMethods auto-injects middlewares and opens OPTIONS/CORS (:58/63/75).Paozhu — Each route splits into 4 independent slots; sync and coroutine are "first-class routing citizens": reg_raw(site, path, pre_sync, co_pre, reg_sync, co_reg, mask_code) (common/autocontrolmethod.hpp) registers a route as sync-pre / coro-pre / sync-handler / coro-handler four separate entry points. The main driver co_resolve (router.cpp:694) transparently interleaves them: coroutine steps co_await in place (pre co_await h.co_pre(peer); handler co_await h.co_regfun(peer)); synchronous steps go to the business thread pool.
pre_kind/reg_kind. Developers never need to manually switch threads or wrap in std::async. This is a different design philosophy from Drogon's "handler picks one form, CoroMapper is a separate type" — Paozhu treats "is this a coroutine" as a property of each registration slot, not the handler as a whole.pre_sync/co_pre chain up in declaration order into a serial pre chain; classify_step() decides direction based on return value — @ok (pass), same-name (continue), @exit (explicit stop), jump name (router.cpp:76-87). First non-empty return short-circuits; empty string is the only "pass" signal (router.cpp:458-461). Additionally, site-level pre/after hooks: run_site_hooks (sync :462) and co_run_site_hooks (coro :478) batch-run action_pre_lists/action_after_lists. When the list exceeds the limit, it truncates to the first site_hook_limit entries rather than dropping the whole thing (avoids silent auth failures).pop_flow_index), if the driver is a sync chain but the popped entry is a coroutine, draining stops (:364-368); if coroutine-driven, it co_awaits inline (:865-873).Drogon: Pre-handler logic lives in Filters (HttpFilter + ADD_FILTER_TO / GlobalFilters.cc) and Middlewares (HttpMiddleware, MiddlewaresFunction.cc + HttpAppFramework.h:718). The framework executes them before handlers, ordered by path / global ordering, decoupled from the routing table, reusable, sortable. The sync/async choice lives on the individual method form (binder's Blocking/Callback/future triad, or coroutines via separate CoroMapper). There is no slot-based mixed declaration like "one route declares a sync-pre + coro-handler together".
flow_method)Conclusion: Yes, Paozhu has built-in simple task orchestration on httppeer, implemented via httppeer::flow_method for "sequential multi-handler orchestration within a single request".
httppeer holds a lazily-allocated stack std::unique_ptr<std::list<std::string>> flow_method (httppeer.h:300-301) — stores names of subsequent handlers to execute.httppeer.cpp:2135-2155):
push_next(name): push_back (stack top, LIFO — last pushed runs first).push_last(name): push_front (stack bottom, FIFO — first pushed runs first, the default recommended for sequential breakdowns).push_steps({...}): batch-push.httppeer.cpp:2156-2169 pop_flow() pops back; router.cpp:340-368 pop_flow_index): After a handler completes normally (returns empty string), the router pops the next registered handler name from the stack and executes it. One handler pops exactly one; the rest wait for that handler to return empty before popping, until the stack empties or the chain hits the top (router.cpp:651-657 sync chain, 834-839 coro chain).kFlowOk / kFlowExit (explicit stop, no pop) / kFlowTimeloop (transfer to event loop), interpreted by classify_step (router.cpp:76-87).:350-363); a coroutine entry pushed onto a sync chain stops by design (:364-368).This means one request can do "handler A → push B, C, D → after A returns empty, run B→C→D in order", forming a lightweight sequential workflow / pipeline. Drogon has no equivalent built-in mechanism: its composition primitives are Filters/Middlewares (cross-cutting concerns, not sequential step orchestration) and handler-internal logic. To chain multiple handlers in Drogon, you'd manually call or redirect inside the handler — the framework layer provides no orchestration stack.
Paozhu: Routes naturally attach to site slots — host_index → slot, _site_to_slot maintains one slot per site. Route tables are isolated by slot (router.cpp:337 find_name_in_slots(peer_slot(peer), ...)); the first argument of reg_raw is site (domain, empty string = global slot 0). Combined with theme views (§9), this is framework-level multi-tenancy.
Drogon: Routes themselves carry no tenant concept. Multi-tenancy is achieved at the business layer via "multiple apps / multiple controller groups + DrObject naming" — no one-stop slot isolation.
| Dimension | Paozhu | Drogon (drogon-master 1.9.14) |
|---|---|---|
| Registration mechanism | Source //@urlpath annotation + build-time codegen generating reg_raw tables |
Reflection macro METHOD_ADD/ADD_METHOD_TO + static self-registration (methodRegistrator, before main) |
| Path params | :param placeholder + reg_urlpath parameter name table |
{param} placeholder, compiled to ([^/]*) regex |
| Exact match | _urlpath_map + prefix fallback (up to 6 segments) |
simpleCtrlMap_/ctrlMap_ hash; ctrlVector_ regex |
| Pre / cross-cutting | Three-layer hook system: per-route pre_sync/co_pre serial chain (@ok/@exit/jump) + per-site action_pre_lists/action_after_lists (serverconfig.h:63-64, run_site_hooks serial, trimmed at site_hook_limit=15) + per-site 404 action_404_lists (httppeer.cpp:1088, triggered when no route matches) |
Filters (one-way intercept) + Middlewares (before/after route wrapping, registerMiddleware), sortable, reusable, decoupled from routing — no per-site slot isolation |
| Sync/coroutine mixing | First-class in routing layer: reg_raw splits into sync-pre / coro-pre / sync-handler / coro-handler four slots; co_resolve transparently interleaves |
Sync/async lives on the individual method form (Callback/future/CoroMapper), no slot-based mixed declaration |
| Simple task orchestration | Yes: httppeer::flow_method stack (push_next/push_last/pop_flow), multiple handlers execute in sequence within one request |
No built-in; manual call / redirect |
| Multi-tenant routing | Multi-domain virtual host built-in at framework level: host_index matches sites by Host header (alias_domain supports alias domains), each site gets independent wwwpath/cert/hooks/rewrite/CORS/upload limits/slot_id (serverconfig.h:30-79) |
None; business-layer grouping or multi-instance |
Key difference: Drogon routing is an industrial-class classic pattern — "macro reflection + static registration + Filters/Middlewares" with clean separation of concerns. Paozhu takes an integrated approach — "annotation + codegen + site slot + flow_method orchestration" with finer-grained per-site hook system (each site gets independent pre/post/404 hook lists), built-in light sequential workflow capability, multi-tenancy right at the routing layer, and PHP-FPM FastCGI support baked in.
Paozhu (built-in at framework level): isuse_php + php_root_document + rewrite_php_lists three config keys, check_php_dispatch() (server.cpp:1369) full implementation: .php suffix hit → check rewrite rules → locate PHP file → set compress=10 marker → forward to php-fpm via FastCGI. Supports URL rewriting (rewrite_php_lists regex maps URLs to PHP files) and index.php fallback (empty URL path checks php_root_document/index.php).
Drogon (absent): Zero PHP/FastCGI files in the entire repository. To run PHP, implement the FastCGI protocol yourself on top of trantor TcpClient, or front with Nginx reverse proxy.
Each Paozhu site maps to sitehostinfos struct in serverconfig.h (serverconfig.h:30-79). host_index matches sites by HTTP Host header (supports alias_domain for alias domains). Per-site independent configuration fields:
| Field | Meaning | Drogon has it? |
|---|---|---|
mainhost + alias_domain | Primary domain + alias domains (one site can bind multiple domains) | ❌ Zero virtual host files in entire repo |
wwwpath + php_root_document | Static file root + PHP root | ❌ |
certificate_file + privateKey_file | Per-site independent TLS certificates | ❌ (only one global cert via addListener) |
action_pre_lists / action_after_lists / action_404_lists | Per-site pre / post / 404 hook lists | ❌ (Filters/Middlewares are framework-registered, no site isolation) |
rewrite_php_lists | Per-site PHP rewrite rules | ❌ |
cors_domain + cors_origin_allowed | Per-site CORS allowlist | ❌ |
upload_max_size | Per-site upload size limit | ❌ |
themes + themes_url | Per-site theme/template directory | ❌ |
slot_id | Routing table slot isolation (each site gets its own routing table) | ❌ |
http2_enable | Per-site HTTP/2 on/off | ❌ |
fastcgi_host + fastcgi_port | Per-site PHP-FPM backend address | ❌ |
siteid + groupid | Site/group ID (business layer can use for tenant auth) | ❌ |
Matching flow: HTTP request arrives → find_host_index() (httppeer.h:170) looks up Host header in mainhost → miss → check alias_domain → resolve host_index → all routing, hooks, certs, roots, CORS, upload limits switch automatically by host_index. No domain branching needed in business code.
Both support MySQL / PostgreSQL / SQLite and their ORM queries support C++20 coroutine async. But they differ significantly in generation timing, code organization, query style, caching, transactions, and relationship expression.
Paozhu (paozhu_cli orm <dbtag>, generator vendor/httpcli/modelfun.hpp) produces three layers per table:
<table>_base.h: pure data/reflection layer — namespace <table>_info contains col_count, enum cols, struct meta (field definitions), type:: type aliases, col_names, null_col bitmap, assign_field_value (world_base.h:41-45, L76-80), plus dirty-field bitmap __dirty_bits + get_dirty_indices()/clear_dirty()/make_update_dirty_sql().<table>_opsql.h: template<M_MODEL,B_BASE> class <table>_opsql : public B_BASE containing all query/cache/dirty-update methods. Constructor fetches orm_conn_pool from get_orm_conn_pool_obj() per dbtag.
setXxx() marks the corresponding dirty bit when writing a field. update_dirty() (text protocol) / exec_update_dirty() (prepared) / async_update_dirty() / async_exec_update_dirty() / commit_update_dirty() only build UPDATE for dirty-bit fields. On success clear_dirty(). Short-circuits when the dirty set is empty (prevents accidental all-field updates) and refuses execution without WHERE (article_opsql.h:9866/15248/10319/15323/13120). Mechanistically equivalent to Drogon's dirtyFlag_[] — both are "incremental writes of changed columns only".models/include/<Model>.h: thin shell class <Model> : public <table>_opsql<<Model>,<table>_base> (models/include/World.h:11).orm/fk_child_opsql.cpp, orm/fk_parent_opsql.cpp (see §4.6).Drogon (drogon_ctl create_model, reuses CSP templates model_h.csp/model_cc.csp) generates one model class per table (example orm_lib/tests/mysql/Users.h):
struct Cols (column-name constants), table metadata (tableName/primaryKeyName/PrimaryKeyType: single PK = concrete type, composite = std::tuple, no PK = void).getValueOfXxx() / getXxx() / setXxx() + setXxxToNull() (nullable).toJson() / updateByJson() / validateJsonForCreation/Update.outputArgs / updateColumns / updateArgs / insertColumns, called internally by Mapper.dirtyFlag_[]: determines insert/update writes only changed columns (Users.h:252-274).sqlForFindingByPrimaryKey() / sqlForDeletingByPrimaryKey() / sqlForInserting(bool& needSelection) (dynamically builds column list from dirtyFlag_[]; PG appends returning *, MySQL/SQLite relies on LAST_INSERT_ID/insertId()).Key difference: Paozhu separates "data definition (base)" from "SQL operations (opsql)" into two files and supports external FK .cpp; Drogon puts everything into one model class with private+friend persistence internals — more OOP.
Paozhu — Chained builder (returns M_MODEL&), method names lean toward "direct SQL verbs":
count() / page() / fetch() / fetch_one() / fetch_to<T>() / fetch_one_to().update_col / exec_update_col / replace_col.where() / whereIn / whereNull / whereEQ / per-column shortcuts eqId(); condition operators orm::wq::eq/nq/bt/be/lt/le.leftJoin().async_fetch / async_count / async_page, plus exec_* variants with _prepared.page() internally calls count() first. count() reads the already-set .where() filters from the model; subsequent fetch() also goes through the same build_text_where — COUNT and data queries share the same where, set conditions once. page() also directly returns the navigation 4-tuple (nav_min, nav_max, cur_page, total_page) — no manual pagination math.Drogon — Streaming Mapper<T> + Criteria (template Mapper in orm/Mapper.h:115, constructor takes DbClientPtr):
findByPrimaryKey / findAll / count / findOne / findBy, each in three forms (sync/callback/future).insert / update / updateBy / increment / deleteOne / deleteBy / deleteByPrimaryKey.Criteria (Criteria.h:73): CompareOperator has EQ/NE/GT/GE/LT/LE/Like/In/IsNull/IsNotNull; supports operator&&/|| composition, CustomSql + _sql literals, Criteria(Json::Value).limit / offset / orderBy / paginate / forUpdate / innerJoin / leftJoin / rightJoin; placeholder $? is DB-aware and replaced with $1,.. (PG) or ? (MySQL/SQLite).Style divergence: Paozhu leans toward "familiar chained verbs + per-column shortcuts" — low learning curve. Drogon uses
Criteriaexpression objects +Mapperstreaming — more type-safe, but needs understanding theCriteria/SqlBindersystem. Pagination comparison: Paozhu'spage()auto-reuses already-setwhere, completes "count + fetch + return navigation 4-tuple" in one call. Drogon'spaginate(page,perPage)(Mapper.h:173/2013) only sets offset/limit;count()defaults to emptyCriteria()(Mapper.h:459), so you need to pass the sameCriteriato bothcount(criteria)and the query. Pagination navigation (total pages / window) you compute yourself.
Paozhu — Optional capability on the model object, ready out of the box: Caching is an optional feature on the model object, no third-party library needed. Core API:
use_cache() before query; subsequent calls automatically hit cache; save_cache(exptime) writes back.try_get_cache(name) returns std::optional (miss = nullopt, non-throwing); is_hit_cache flag indicates whether the current result came from cache.std::hash<std::string>{}(sqlstring) or custom cache_key_name.orm_cache_deadline() clamps to ORM_CACHE_DEFAULT_TTL (120s) ~ ORM_CACHE_MAX_TTL (3600s); a single background daemon scans expired entries every ORM_CACHE_SCAN_INTERVAL (30s). All three are overridable at compile time via -D.| Paozhu | Drogon (this tree) | |
|---|---|---|
| Query-result cache | Yes (orm_cache.hpp's model_meta_cache<BASE_T>, std::map<size_t,data_cache_t> + mutex) |
No (zero cache in ORM layer; every query hits the pool) |
| Access | use_cache() / save_cache() / try_get_cache() / remove_cache() |
— |
| Reclaim | Background daemon periodically scans expired entries | — |
Paozhu — Sync + coroutine dual form; coroutine is the primary paradigm:
fetch() / count() / page() / exec_fetch() / exec_count(); coro async_fetch() / async_count() / async_page() / async_exec_fetch().co_await select_conn->async_fetch_directly(...); DML does co_await edit_conn->async_exec_dml.orm_conn_pool.cpp: coroutine async_get_mysql_select_conn / async_get_mysql_edit_conn uses co_await async_add_*_connect to fetch/build connections; sync get_mysql_*_conn.pg_conn.cpp async_read_loop co_await; sqlite_conn.cpp goes through run_on_worker queue.reg_raw provides sync slots (reg_sync / pre_sync) and coro slots (co_reg / co_pre) simultaneously; co_resolve interleaves transparently — developers choose the form by function signature (see §3.3).Drogon — Three forms (sync / callback / future) + coroutine overlay:
Mapper method provides three sets: binder << Mode::Blocking (sync, throws exception) / binder >> rcb >> ecb (callback) / std::promise + get_future (future).DbClient::operator<<(sql) returns SqlBinder (DbClient.h:256) — << binds params, >> registers callbacks, exec() executes.CoroMapper (CoroMapper.h:67, inherits Mapper<T>) returns MapperAwaiter, whose await_suspend plugs the callback into CallbackAwaiter and resume()s. Also has execSqlCoro, newTransactionCoro.Result (Result.h:57, reference-counted, polymorphic size()/affectedRows()/insertId() etc.) / Row (Row.h:46, operator[] returns Field).io_context Coroutine Pool + Dedicated ThreadPool)Paozhu's execution layer runs two thread pools side by side, with concurrency split into three layers via "strand serialization + business-pool dispatch-back-to-strand + peer chained shared ownership". No GC needed, still safe:
io_context coroutine pool (server.h:381, single io_context + runthreads). Runs all co_spawn coroutines — HTTP/1.1 · HTTP/2 · WS · MQTT · RPC.ThreadPool clientrunpool (server.h:487, initial size hardware_concurrency()*2+2, threadpool.h). Picks up sync/blocking tasks offloaded from the coroutine loop (router sync pre/regfun, timers, WS data messages). Supports dynamic resize (addthread/fixthread, cap 32) and 503 backpressure (force_reject_* → add_sync_task/add_conn_task returns false).client_session (client_session.h:76, enable_shared_from_this) binds an asio::strand. Per-connection send/receive serialized, lock-free. Sends go through post_write (:81-91) into the per-connection send ring, written out serially by a consumer coroutine.clientrunpool via co_pool_run_step (pool_step.h:41). After fn() completes in the pool thread, finish uses asio::dispatch to put the continuation back onto the session strand — business threads only run fn(), response sending always lands on the strand (pool_step.cpp:18-20).peer is a first-class parameter throughout "task closure → finish → wake-up op → strand resume", each carrying one strong reference sequentially (pool_step.cpp:91-117). Release point is fixed on the strand thread, send threads never get freed objects. When the pool can't accept work, it doesn't drop tasks but completes the awaitable in-place with rejected=true (pool_step.cpp:79-86), eliminating permanent coroutine hangs.Drogon comparison: Drogon uses "multiple independent EventLoop" (one EventLoop per worker, MPSC cross-thread, epoll/kqueue). Handlers run on the owning ioLoop; heavy blocking needs manual offload. It doesn't have Paozhu's "shared io_context + independent generic ThreadPool + strand closed loop" cross-pool model, so it also avoids the cross-pool object ownership question.
Paozhu transactions have two layers of API, both with real examples in business code (controller/src/test_sql_commit.cpp):
① Native SQL layer (orm::db_conn directly holds the transaction connection):
auto ulink = std::make_unique<orm::db_conn>("cms");
ulink->begin_commit(); // BEGIN / BEGIN IMMEDIATE
ulink->edit_query("UPDATE sysuser set ...");
ulink->edit_query("INSERT INTO sysuser ...");
// Queries also go through the same write connection (reads what we just wrote, no master-replica replication lag)
orm::cms::LocalusersqlStruct row;
ulink->edit_query("SELECT * FROM sysuser WHERE ...", row);
if (ulink->iserror) ulink->rollback();
else ulink->commit();
db_conn instance holds one mysql_edit_conn. Inside a transaction, both reads and writes go through this one write connection — edit_query(sql) writes, edit_query(sql, struct&) reads (second param is the output). This guarantees intra-transaction read consistency, no master-replica replication lag.async_begin_commit() / async_commit() / async_rollback() / async_edit_query().② ORM layer (lock_conn binds the transaction connection to the model):
auto txnlink = std::make_unique<orm::db_conn>("cms");
txnlink->begin_commit();
// Multiple different model objects all lock onto the same transaction connection — cross-object transaction
orm::cms::Sysuser user_m;
user_m.lock_conn(txnlink->mysql_edit_conn);
user_m.where("adminid", 10000);
user_m.update("nickname");
orm::cms::Sysroles role_m; // Different table, different model class
role_m.lock_conn(txnlink->mysql_edit_conn); // Also locks onto the same one
role_m.where("roleid", 50);
role_m.update("remark");
user_m.unlock_conn();
role_m.unlock_conn();
txnlink->commit(); // Unified commit or rollback
db_conn transaction would land on another auto-committed session — lock_conn() binds the model onto the write connection opened by db_conn, so all models' reads and writes inside the transaction go through the same one.lock_conn onto the same db_conn's write connection, forming a cross-table transaction.unlock_conn() only unbinds, does not return to pool (prevents uncommitted session leaks). The final commit()/rollback() on db_conn unifies the return.Drogon — First-class Transaction object:
Transaction inherits DbClient; Mapper<T>(transactionPtr) makes subsequent reads and writes happen on the same transaction connection.newTransaction / newTransactionAsync / newTransactionCoro. TransactionImpl borrows a single connection from readyConnections_ and moves it to busyConnections_, auto-sends BEGIN / BEGIN IMMEDIATE / BEGIN EXCLUSIVE; after commit/rollback, returns via usedUpCallback.TransactionType::{Deferred, Immediate, Exclusive}.Comparison: Drogon uses a first-class Transaction object passed to Mapper — API is clean, models don't know they're inside a transaction. Paozhu is explicit and manual — either use db_conn for native SQL, or lock_conn to bind ORM models to the transaction connection (models need to be aware of transaction state). Both work, but Drogon is more elegant.
Paozhu: oneXXX/manyXXX live in external .cpp (orm/fk_child_opsql.cpp / orm/fk_parent_opsql.cpp), implemented by applying where()/whereIn() to the related model using this table's FK values. Also provides generic leftJoin() for explicit JOINs.
Drogon: Relationships are "generation-time config + per-model methods at runtime", no generic Mapper API for hasMany/hasOne style.
Relationship (create_model.h:211, Type{HasOne,HasMany,ManyToMany} + PivotTable for many-to-many join tables).tryAddAutoRelationship (create_model.cc:181); PG queries information_schema FK views, MySQL queries KEY_COLUMN_USAGE, SQLite queries PRAGMA foreign_key_list.model_cc.csp:1580-1789): per-type generates getXxx(clientPtr) sync/async pairs — HasOne builds select * from <target> where <key> = ?, HasMany returns vector<Target>, ManyToMany does join + pivot returning vector<pair<Target,Pivot>>. Relationship queries go through DbClient/SqlBinder, can take a transaction client.Paozhu: At generation time, modelfun.hpp uses pg_type_to_mysql_type() + SQLite affinity mapping to convert SQL types to MySQL binary type codes, then get_field_type() maps codes to C++ (integers/dates/strings etc.), landing in the struct meta + type:: of _base.h.
Drogon: At generation time, create_model.cc writes per-DB hardcoded mappings (PG / MySQL / SQLite branches). Parameter binding SqlBinder::operator<< branches on ClientType:
htons/htonl/htonll for network order, format=1 (binary);getMysqlType<ParaType>() mapping (with static_assert rejecting unsupported types);sizeof(T); also supports trantor::Date / vector<char> / optional<T> / Json::Value.Field::as<T>() (Field.h:70-107, extensive specializations) + model constructor T(const Row&, indexOffset), does special parsing for date/bytea, and controls incremental column writes based on dirtyFlag_[].Paozhu: orm_conn_pool.h — each database gets its own independent read-write separated pool (get_mysql_select_conn for reads / get_mysql_edit_conn for writes, all three databases support this). Connection pools are indexed by database name (get_orm_conn_pool_obj() returns std::map<std::string, std::shared_ptr<orm_conn_pool>>, keys are "cms"/"blog"/"lite" etc.). ORM model classes are generated inside per-database namespaces (modelfun.hpp:9467 produces namespace orm { namespace db_tag { class model {...} }}), so tables with the same name in different databases are compile-time isolated — orm::cms::user and orm::blog::user are completely different C++ classes. Multi-database call: auto db = make_unique<orm::db_conn>("cms"); selects the target DB directly. Framework-level table prefix (orm_conn_t::pretable, orm_common.h:718, configured in conf/orm.conf): under [docs] section write pretable = web_, allowing multiple business ORM models to share one physical database — namespace + prefix double isolation.
Drogon: Multi-database via multiple db sections in config + app().getDbClient("cms") to fetch; ORM models also support namespace isolation, but you must specify it manually — drogon_ctl create model --namespace cms command-line flag, or "namespace": "cms" in the JSON config; default value is the database name. No read-write separation: a single DbClient's connections_ pool is shared for reads and writes. No table prefix: zero related implementations in the entire repository; to host multiple business ORMs in one physical DB you must either create separate DBs or hand-edit generated model source.
Paozhu exclusive:
async_get_*_select_conn / async_get_*_edit_conn — reads hit read-only instances, writes hit primary)--namespace flag or JSON field)pretable = web_ in conf/orm.conf; Drogon unsupported)orm_connect_mar_t (orm_connect_mar.h/cpp): When borrowing, watch_mysql_connect(conn) registers a weak_ptr + time_begin. The server main loop calls clear_connect() every 320 ticks — automatically prunes expired weak_ptr, skips async connections, and hard-closes (hard_close()) sync connections exceeding CONST_ORM_QUERY_CONNECT_TIMEOUT. Rate-limit: max 10 connections per round, scan over multiple rounds, sleeps 200ns between rounds to yield CPU.push_log(sql_string, duration_ms) records slow queries; save_log() flushes to disk every CONST_ORM_QUERY_LOG_TIME ticks.Drogon: DbClientLockFree (orm_lib/src/DbClientLockFree.cc) with three-state collections connections_/readyConnections_/busyConnections_ (mutex-protected). execSql reuses an idle one if available, otherwise pushes into sqlCmdBuffer_ (max 20000; exceeding it reports "Too many queries in buffer", DbClientLockFree.cc:203). Number of event loops decided by connNum (SQLite fixed at 1, PG/MySQL take min(hardware_concurrency(), connNum)). Per-library connection classes PgConnection/MysqlConnection/Sqlite3Connection only handle single-connection non-blocking I/O; supports execSqlWithTimeout which throws TimeoutError on timeout. No framework-level connection health check — zombie cleanup relies on the DB server's wait_timeout setting.
Underlying protocol implementation divergence (architecture-level):
mysql_conn.cpp hand-written read_server_hello (parses capability_flags, auth_plugin_salt_data), caching_sha2_password auth handshake, mysqlnd_xor_string encryption, seq_next_id packet sequence number.pg_conn.cpp hand-written build_startup_message, SCRAM-SHA-256 handshake (sasl_scram_sha256, int32_to_buf message frames).sqlite_conn.cpp / sqlite_conn.h (L45) directly uses the official sqlite3 C API (sqlite3_open/sqlite3_prepare_v2/sqlite3_step/sqlite3_stmt*), guarded by #ifdef ENABLE_SQLITE, CMake find_package(SQLite3 REQUIRED) linking. SQLite is an embedded database with no wire protocol of its own to self-implement.CMakeLists.txt does not link libmysqlclient / libpq (the cost of self-implementing MySQL/PG wire protocols), but SQLite needs SQLite::SQLite3.MysqlConnection directly #include <mysql.h> (links unofficial::libmariadb); PgConnection uses libpq (find_package(pg)); SQLite uses SQLite3_lib. Wire protocols are delegated entirely to third-party libraries; Drogon only provides async wrappers (based on non-blocking APIs like mysql_real_connect_start/cont).§4.2 covered API shapes. Here's what real business code looks like (examples taken from both sides' source).
Paozhu — The model object IS the query builder; chained verbs + coro/sync dual form (example controller/src/testormcache.cpp / testcrud.cpp):
// The model object itself is the builder; conditions, caching, and query all chained in one go
Article art;
art.where("isopen", 1).whereEQ(art.Alias::aid, aid).use_cache(); // Chained conditions + query cache
co_await art.fetch_one(); // Coroutine query, result backfills art
client << "<br/>title:" << art.getTitle();
if (art.is_hit_cache()) { /* cache hit branch */ }
// Pagination: directly returns (nav_min, nav_max, cur_page, total_page)
unsigned int page = client.get["page"].to_int();
auto [nav_min, nav_max, cur_page, total_page] = art.page(page, 10, 5); // page, per_page, nav width
Style takeaway: get a model instance → chain .where()/.whereEQ()/.use_cache()/.fetch_one()/.page() on it → co_await or sync fetch_one(). No separate Mapper layer — the model IS the DAO.
Drogon — Generic Mapper<T> + Criteria expression; CRUD by model type (example orm_lib/tests/db_test.cc):
using namespace drogon_model::postgres;
Mapper<Users> mapper(clientPtr); // clientPtr = app().getDbClient()
Users user;
user.setUserId("pg"); user.setUserName("alice");
mapper.insert(user); // Sync write
auto u = mapper.findByPrimaryKey(2); // PK lookup
// Coroutine form
CoroMapper<Users> cmapper(clientPtr);
auto cu = co_await cmapper.findByPrimaryKey(2);
auto us = co_await cmapper.orderBy(Users::Cols::_user_id).limit(2).findAll();
auto one = co_await cmapper.findOne(
Criteria(Users::Cols::_id, CompareOperator::EQ, 1));
Style takeaway: Mapper<Model> is a generic CRUD facade independent of the model object. Conditions use Criteria(col, op, val) expression construction. The model object only carries data + setter/getter; all persistence goes through Mapper.
One-liner: Paozhu is "the model queries itself" (builder attached to the model, verb-style, quick to pick up). Drogon is "Mapper proxies model CRUD" (generic facade + Criteria expressions, type-safe, cleaner separation of concerns).
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Supported DBs | MySQL / PostgreSQL / SQLite (configured via dbtype) |
PostgreSQL / MySQL(MariaDB) / SQLite3 |
| Generation source | Dual mode: Live DB introspection (modelfun.hpp runs SHOW CREATE TABLE/PG table list, database-first) + also pure offline reading schema/<dbtag>/tables/*.sql (CI-friendly, decoupled from runtime DB) |
Live DB only — reads information_schema/PRAGMA (create_model, database-first) |
| Artifact organization | base (opsql separated) + external FK .cpp | Single model class + private/friend encapsulation + dirtyFlag |
| Query style | Chained verb builder (where/page/fetch) |
Mapper<T> + Criteria streaming expressions |
| Query-result cache | Built-in orm_cache.hpp / model_meta_cache |
None (cache delegated to upper layer / Redis) |
| Async paradigm | Sync + coroutine dual form: sync count()/fetch()/page() + coro async_count()/async_fetch()/async_page() (asio::awaitable) |
Sync / callback / future three forms + CoroMapper coroutine overlay (four forms) |
| Transactions | Two-layer API: orm::db_conn native SQL transaction + model lock_conn(txn_conn) to bind to transaction connection (manual unbind + returned by db_conn lifecycle) |
First-class Transaction object threaded into Mapper; model is unaware, API cleaner |
| Relationships / FK | oneXXX/manyXXX in external .cpp |
Generated-time Relationship + model getXxx(client) method |
| Connection pool | dbtag read-write separated coroutine pool + framework-level health check (orm_connect_mar periodic zombie scan + hard-close on timeout) + ORM query logging |
DbClientLockFree three-state pool + buffer cap 20000; no framework-level connection health check (zombie cleanup relies on DB server wait_timeout) |
| Type mapping | SQL → MySQL binary code → C++ | Per-DB hardcoded mapping + SqlBinder binary binding |
| Wire protocol / client dependency | Self-implemented MySQL/PG wire protocols (mysql_conn.cpp/pg_conn.cpp hand-written handshake and auth); SQLite uses official sqlite3 C API (sqlite3_open/sqlite3_prepare_v2), does not link libmysqlclient/libpq, links SQLite3 |
Wraps all official client libraries (MysqlConnection→libmariadb, PgConnection→libpq, SQLite→sqlite3), wire protocols delegated to third parties |
Key difference takeaways:
create_model; Paozhu modelfun.hpp connects to mysql_conn_base/pg_conn_base/sqlite_db_conn running SHOW CREATE TABLE/PG table list, and can dump live DB into schema/*.sql). Paozhu additionally supports pure offline schema/*.sql generation (decoupled from DB, CI-friendly, can be offline).Mapper<T> + Criteria for unified CRUD (OOP, type-safe); Paozhu generates per-table independent _opsql.h operation classes (verb-style builder, quick to pick up).Transaction object), Paozhu is more manual (lock_conn).__dirty_bits bitmap + update_dirty()/exec_update_dirty() (short-circuit on empty dirty set, rejects execution without WHERE); Drogon uses dirtyFlag_[] controlling insertColumns/updateColumns. Mechanistically equivalent.Both have rate limiting at different layers, with some overlap: Drogon has both hard connection caps (
HttpConnectionLimit, global + per-IP hard caps) and request-level (per-IP / per-user / per-route, token bucket/sliding window/fixed window, plugin-ready out of the box). Paozhu has connection-admission-level overload protection (60-second connection window + load-adaptive admission rate + queued slow admission) + site upload limits + IP blocking. They approach connection-level protection differently (hard reject vs. queued admission); request-level quotas are Drogon's unique business capability.
Core class (lib/inc/drogon/RateLimiter.h):
RateLimiterType three algorithms: kFixedWindow / kSlidingWindow / kTokenBucket (RateLimiter.h:10-15).RateLimiter::newRateLimiter(type, capacity, timeUnit=60s) (RateLimiter.h:42-45), interface isAllowed()→bool (:52); SafeRateLimiter wraps with mutex for thread safety (:56-74).TokenBucketRateLimiter / SlidingWindowRateLimiter / FixedWindowRateLimiter (lib/src/ .cc/.h files).Hodor plugin (lib/inc/drogon/plugins/Hodor.h, official Drogon rate-limiting plugin):
onHttpRequest (Hodor.h:144) AdviceCallback pre-advice. On limit hit returns rejection_message (default "Too many requests").Hodor.h:36-77): algorithm (default token_bucket), urls regex (empty = all), time_unit, capacity (global), ip_capacity (per-IP), user_capacity (per-user, configurable setUserIdGetter), sub_limits (per-URL group sub-policies), trust_ips / use_real_ip_resolver (XFF / real IP), limiter_expire_time (default 600s, uses CacheMap storing limiter per IP/user).Drogon's request-level rate limiting is production-grade, fine-grained, and declarative: configure one JSON block to get "global 1000/s + per-IP 100/s + per-user 50/s" combo policies matching URLs like
/api/.*, with swappable algorithms.
Hard connection cap (lib/src/HttpConnectionLimit.h:26, atomic counting at accept stage):
HttpConnectionLimit::setMaxConnectionNum(num) — global hard connection cap; setMaxConnectionNumPerIP(num) — per-IP hard connection cap.HttpServer.cc:L121 calls tryAddConnection() at accept time; over-limit rejects immediately; releaseConnection() atomically decrements on connection close.Connection admission throttling (built into httpserver core, server.h:247 ratelimiter() thread-driven):
update_minute_count() (server.cpp:8583-8632) — each accept thread holds one 60-bucket thread-local ring, snapshots global total_count (connection counter), computes "connections entering in the last ~60 seconds".compute_rate_from_load(has_save_link_count) (server.cpp:8636-8647) — maps active connection count to [lo,hi] interval as percentage, then linearly converts to admission rate [num1,num2]; thresholds controlled by rate_limit_new_wait_num / rate_limit_accept_wait_num / rate_limit_second_num1 / rate_limit_second_num2.has_save_link_count > rate_limit_new_wait_num && live_link_count > rate_limit_new_wait_num, server.cpp:9014-9022 / 9197-9204), calls enqueue_rate_limited() (:8650) to push connections into socket_session_wait_rate. The throttling thread wakes up on rate_limit_condition and admits them gradually at rate_limit_second_time_num per second (:8749-8758 slot loop).Upload limits: httppeer::check_upload_limit() (httppeer.cpp:789, guarded by site-level host_index) — limits upload body size per site.
IP blocking: check_blockip(peer_session) (server.cpp:5757, TLS handshake failed connections excluded from traffic counts), blacklist/whitelist access-layer interception.
Paozhu has no built-in "per-IP / per-user / per-route request quota" layer, but can be implemented with framework capabilities: add an IP sliding-window counter (maintaining
std::unordered_map<ip, deque<timestamp>>) insiderouter's site pre hooks (action_pre_lists,router.cpp:462), and on limit hit returnhttppeer::kFlowExit(@exit) to reject the request — the pre chain short-circuits on the first non-empty return. The whole implementation closes within the framework, no core extension needed.
| Dimension | Paozhu | Drogon (drogon-master 1.9.14) |
|---|---|---|
| Limiting layer | Connection-admission-level (overload protection) + upload limits + IP blocking | Hard connection cap + request-level quota (per-IP / per-user / per-route) |
| Hard connection cap | No hard upper bound; over threshold → queued slow admission (gradual per-tick) | Global + per-IP hard caps (HttpConnectionLimit, atomic counter at accept), immediate reject on over-limit |
| Algorithms | Fixed admission rate + load-adaptive (compute_rate_from_load) |
Token bucket / sliding window / fixed window (pick one), Hodor plugin |
| Measurement window | 60-second connection count window + active connection load | Configurable time_unit (default 60s), counted by request |
| Per-IP / per-user request quota | No built-in; can self-implement with site pre hooks + IP sliding-window counter (framework-internal closed loop) | Built-in (ip_capacity / user_capacity) |
| Per-route subdivision | None (site-wide total gate) | urls regex + sub_limits grouping |
| Enablement | Compiled into httpserver, tune via threshold params |
Connection gate hardcoded in HttpAppFrameworkImpl.cc + Hodor plugin JSON config |
| Over-limit response | Connections queued / slow-admitted; IP directly stopped | Connections over limit closed immediately; request over limit returns rejection_message (default 429 text) |
Core difference: Drogon rate limiting is two-layer — bottom layer HttpConnectionLimit hard cap (prevent overload, reject immediately on over-limit) + top layer Hodor request-level quota (business-friendly, per-IP/user/route, swappable algorithms, pluginized). Paozhu is single-layer connection-admission-level protection — over threshold it doesn't reject, it queues and admits gradually (gentler, avoids transient rejection shock), but request-level quotas need self-implementation via site pre hooks. The two-layer connection protection is different strategies for the same problem (hard reject vs. queued admission), not entirely non-overlapping.
This dimension diverges sharply: Paozhu builds all five protocol types natively into
httpservercore (and uses a unifiedsubpubregistration model across ws/mqtt/socket); Drogon only builds WebSocket and Redis natively, with no framework-level support for MQTT / RPC / raw Socket servers.
websockets.h + ws_conn.cpp (connection) / ws_handshake.cpp (handshake) / ws_parser.cpp + ws_wire.h (frame codec) / http_websocket_client.cpp (65KB client); websockets_callback.cpp + websockets.conf config; controller layer also provides websockets/loopwebsockets.hpp / loopwebsockets_co.hpp (coroutine loop) / wssleepwebsockets.hpp and other loop abstractions.WebSocketController.h (server controller) / WebSocketConnection.h (connection) / WebSocketClient.h (client), with matching *Impl.cc files; tests/ has complete coro/keepalive/upgrade tests. Capabilities are equivalent.vendor/pzredis/): Complete client redis_client.h + redis_conn.h (connection) + redis_pool.h (per-section multi-instance connection pool) + redis_pubsub.h/redis_subpub.h (publish-subscribe) + redis_reply.h (reply parsing) + redis_types.h; synchronous bare names / coroutine async_ prefix (redis_client.h:294-296 asio::awaitable), configured via redis.conf. Does not depend on hiredis — self-implemented RESP parsing.nosql_lib/redis/): RedisClient + RedisConnection + RedisSubscriber + RedisTransaction + RedisClientManager; uses hiredis underneath (cmake_modules/FindHiredis.cmake). Capability matches Paozhu, but leans on a third-party library.mqtt_broker.h is a singleton broker (mqtt_broker::instance()), containing subscription tables subscribe()/unsubscribe()/unsubscribe_all, retained messages, topic filter matching, publish() delivery, prune_expired() expiry cleanup, plus a global cap kMaxSubscriptionsTotal=100000 (:84-198); mqtt_session.cpp + mqtt_frame.cpp (35KB, MQTT frame codec including MQTT5's No Local / Retain As Published / Retain Handling / shared subscriptions group+device); http_mqtt_client.cpp client + mqtt.conf, controllers mqtt/echo_mqtt_client.hpp / my_test_mqtt.hpp. Ready-to-use MQTT service.drogon-master repository contains zero MQTT source files. The framework ships neither a broker nor a client; you'll need a third-party plugin (e.g. the community drogon-mqtt) or build one on top of WebSocket/TCP yourself.rpc_parse.h / rpc_parse.cpp parses a custom rpc wire format (request line + KV headers + binary body, with CONST_RPC_* size limits). http_rpcclient.cpp (73KB) handles client-side and connection logic, dispatches by its own routing. Controller test_rpc_handle.cpp demonstrates handlers. Provides RPC semantics on the same connection layer.rpc in lib/ is a Windows-specific #include <rpc.h> (Utilities.cc:24, unrelated to any RPC framework). No protobuf / thrift / gRPC wrappers exist. RPC has to be built on top yourself — for instance JSON-over-custom-controllers, or an external gRPC process.http_socket.h + http_socket_client.cpp provides raw TCP connection support; sock_subpub_reg.h/.cpp + sock_subpub_client.h plugs sockets into the unified subpub registration/subscription model, configured via sockets_config.cpp. The framework can act as HTTP/WS/MQTT/Socket multi-protocol server simultaneously.HttpAppFramework only exposes addListener() for HTTP listeners (:864). There is no createTcpServer, UdpServer, or any raw socket server API. If you need raw TCP, you'd build directly on trantor's TcpServer or self-managed Asio — the framework won't do protocol dispatch for you.subpub Registration ModelPaozhu uses *_subpub_reg across ws/mqtt/socket (ws_subpub_reg / mqtt_subpub_reg / sock_subpub_reg) — the same "subscription registration + publish dispatch" kernel spans multiple protocols. Redis also goes through redis_subpub. This is the unified abstraction behind Paozhu's "protocol big family". Drogon's capabilities are independent of each other (WebSocket controller, Redis nosql lib, no MQTT/socket), with no framework-level cross-protocol unified pub/sub kernel. However, the business layer has a PubSubService<MessageType> template class (lib/inc/drogon/PubSubService.h) — it provides named topics + subscribe/publish/unsubscribe, uses shared_mutex + copy-on-write, and is an in-process tool for business code, not a protocol dispatch mechanism inside the framework.
jwt.cpp / jwt.h is a complete self-implementation depending on OpenSSL 3; supports HS256, RS256/RS384/RS512 (PKCS1 + PSS), ES256 (:523 es256_verify), includes alg:none protection (signature segment can be empty but has a clear error state), strict scan of exp/iat/nbf claims, full token length bound CONST_JWT_TOKEN_MAX_LEN, separates decode_without_verify and verify (unverified tokens are never treated as valid) — security-oriented self-implementation.libs/oauth/oauth_store.cpp (35KB, auth code / token / refresh storage) + full endpoints in controller/src/oauth/: oauth_authorize / oauth_token / oauth_introspect / oauth_jwks / oauth_userinfo / oauth_revoke / oauth_metadata / oauth_purge; paired with conf/oauth.conf + RSA key pairs; schema/cms/tables/oauth_*.sql generates OauthClient / OauthCode / OauthRefresh ORM models. Ready-to-use authorization server.libs/oauth_client/oauth_client.cpp + controller/src/oauth_client/oauth_client_demo.cpp; libs/github/github_oauth.cpp has GitHub OAuth login built in.drogon-master repository contains no JWT or OAuth implementation files (lib/ only has incidental Bearer header mentions in HttpResponse.h and HttpHeaderTest.cc, not a real implementation). For JWT you'll need a third-party library (e.g. jwt-cpp) or a community plugin; an OAuth 2.0 server/client has to be built yourself or wired to an external service (there is a community drogon-oauth among others).httppeer directly holds http::obj_val session (httppeer.h:228) — the same JSON micro-object, naturally nested, weak-typed. Lifecycle methods parse_session()/save_session()/clear_session()/get_session_id(). Two persistence modes — file storage save_session_file/parse_session_file + memory storage save_session_memory/parse_session_memory. Automatic SameSite handling — session_samesite(): when the site has cors_credentials configured and the current request is HTTPS, auto-sets "None"; otherwise empty string (attribute omitted).Session class (lib/inc/drogon/Session.h, map<string, std::any> + mutex) with lifecycle managed by framework-level SessionManager, configurable timeouts. any + any_cast<T> strong-typed access — safer than Paozhu's obj_val but heavier. SameSite needs manual configuration.httpclient + http_websocket_client.h client, sync and coroutine (asio::awaitable) dual form — co_await httpclient(peer, "GET https://...") directly returns the response. Supports add_client_task_loop for client_context to manage lifecycle. Example controller/src/testcowaitclient.cpp demonstrates coroutine HTTP client. No HTTP/1.1 pipeline client — but this is barely a gap: Chrome and Firefox never enabled pipeline by default, Firefox 54 removed it entirely, Nginx defaulted to HTTP/1.0 until 1.29.7; HTTP/2 multiplexing interlaces requests and responses on a single connection, equivalent and more thorough (head-of-line blocking solved natively). Paozhu also supports h2c (plaintext HTTP/2), so internal microservice calls and proxy forwarding get the same multiplexing benefit without any TLS negotiation overhead. Most modern scenarios negotiate HTTP/2 and don't need pipeline.HttpClient.h + HttpClientImpl.cc, sync/callback/future three forms + coroutine Task overlay. Exclusive HTTP/1.1 pipeline client — setPipeliningDepth() batch sends on a single HTTP/1.1 connection, pipeliningCallbacks_ queue calls back in order (HttpClientImpl.h:62/185). Complete tests in examples/client_example/ and tests/integration_test/client/. This capability only works on HTTP/1.1 connections; TCP head-of-line blocking remains, and practically no browser or reverse proxy enables pipeline by default.controller/src/serverwatch.cpp registers a paozhu_status route (returns version, current online connection count total_count, coroutine pool clientrunpool thread status); frametasks_timeloop is the framework-level interval task registration point (router triggers it periodically by run_timeloop chain name). Access log is embedded in server.cpp (access_loglist + httpwatch_flush_access_log). However, no Prometheus-style Collector/Registry/Counter/Gauge/Histogram abstraction — if Prometheus metrics are needed, business layer must instrument with prometheus-cpp (docs/qiyedev.txt mentions this).lib/inc/drogon/utils/monitoring/ — full Collector/Counter/Gauge/Histogram/Metric/Registry abstraction + PromExporter plugin (plugins/PromExporter.h). Enable with JSON config, default exposes /metrics endpoint, configurable collectors + labels (e.g. http_requests_total{method, status}); version.cc explicitly lists Prometheus-related dependencies. Production observability ready out of the box.Both frameworks ship framework-level general-purpose in-memory caches (not ORM-specific), but the implementation styles are dramatically different — directly reflecting their respective threading models:
| Dimension | Paozhu pzcache<BASE_TYPE> |
Drogon CacheMap<T1, T2> |
|---|---|---|
| Location | vendor/httpserver/include/pzcache.h |
lib/inc/drogon/CacheMap.h |
| Underlying storage | static std::map<string, data_cache_t> + one global mutex |
Time wheel (4 wheels × 200 buckets), tickInterval * 200^4 seconds max validity |
| Expiry cleanup | Background thread scans for exptime < nowtime |
Time wheel fires fnOnErase callback automatically on expiry — no polling needed |
| Instance model | Global singleton pzcache<T>::conn() (intentionally not destructed — cleanup thread safety) |
One independent instance per EventLoop (IOThreadStorage pattern) |
| Thread safety | Global std::mutex (I/O threads + business pool threads cross-pool, both access it) |
Per-EventLoop instance → zero lock (handler always runs on that ioLoop) |
| API | save(hashid, data, expnum, cover_data) / get(hashid) / check(hashid) / remove(hashid) / update(hashid, exptime) / clear() / remove_exptime() |
Internal insert / erase, notifies business via fnOnInsert / fnOnErase callbacks |
| Demo / usage | controller/src/testpzcache.cpp full CRUD example; ORM query result cache orm_cache.hpp follows the same pattern |
Framework-internal: HttpConnectionLimit limiter states, static file cache, rate limiter states |
Threading model is everything: Drogon handlers are pinned to ioLoops → one CacheMap per EventLoop → zero lock. Paozhu's I/O and business threads live in separate pools → both need access → global mutex. This is the same architectural tradeoff as IOThreadStorage vs global pzcache.
| Dimension | Paozhu | Drogon (trantor EventLoop Timer) |
|---|---|---|
| Underlying mechanism | Frame-driven: fps counter in server.cpp main loop (server.cpp:7656 clientloop_ticks += 1), triggers when fps % timeloop_num == 0 |
timerfd + min-heap (trantor/net/inner/TimerQueue.h), kernel-level high-precision timer |
| Registration | Business layer zero-boilerplate plug-in: controller writes //@urlpath(null, taskname) annotation → auto-registers as both route AND scheduled task target. Any handler (URL-triggered, pre hook, startup init) just calls peer->add_timeloop_task("taskname", interval_ticks) + returns "@timeloop" |
EventLoop::runAt(Date) / runAfter(delay) / runEvery(interval) — framework does NOT provide auto-mapping from interval tasks to route handlers. Business code must fetch an EventLoop + manage callback lifecycle itself |
| Task carrier | Route handler (controller function); on trigger goes through router.run_timeloop_task() → call_sync_regfun(taskname) — shares the same registration system as HTTP routes. But only accepts sync annotation functions (fn_kind::sync); coroutine handlers are skipped (timeloop runs in a sync thread pool, no strand available for co_spawn) |
std::function<void()> — any callback |
| Business-layer plug-in cost | Extremely low: add a scheduled task = write one sync controller function + one arm route. Remove scheduled task = delete controller file, task table auto-clears. All sync functions registered via //@urlpath can be called by timeloop (name match only) |
Higher: fetch EventLoop → write lambda → manage TimerId → lifecycle bound to EventLoop. No controller annotation auto-registration |
| Running thread | clientrunpool thread pool (sync-only, explained in §3.3) |
EventLoop's owning I/O thread (single-thread serial, no cross-thread return issue) |
| Time precision | Depends on server main loop frame rate (~ms typical) | Sub-millisecond (timerfd) |
| Periodic scheduling | ✅ Trigger every N ticks | ✅ runEvery(interval) every interval seconds |
| One-shot task | ✅ Implemented via counting: timecount_num starts at 1, handler calls peer->add_timeloop_count() to accumulate. After N calls, add_timeloop_count(0) clears → next tick server finds timecount_num==0 and auto-removes the task |
✅ runAfter(delay) run once with auto-removal (framework-managed) |
| Specified time point | ❌ No | ✅ runAt(Date) trigger at a specific absolute time |
| Cancel | clear_timeloop_task() sets timeloop_num=0 |
cancel(TimerId) |
| Single-flight protection | ✅ timeloop_inflight atomic CAS: if still running from last tick, skip current tick — no backlog accumulation |
Callbacks are responsible for their own idempotency; TimerQueue itself does NOT enforce single-flight |
| Cron expressions | ❌ No | ❌ No |
| Cross-EventLoop | Globally unified scheduling (main loop's single fps counter drives all tasks) | Each EventLoop has its own independent TimerQueue; cross-thread coordination must be done manually |
Paozhu timeloop workflow (using frametasks_timeloop as example):
1. Business code calls httppeer.add_timeloop_task("frametasks_timeloop", 100)
→ peer.linktype = 7, timeloop_num = 100
2. server.cpp main loop: fps++ every frame
3. When fps % 100 == 0 AND timeloop_inflight == false:
→ CAS sets timeloop_inflight
→ Dispatches to clientrunpool thread pool (linktype==7 → timetasks_run)
4. Inside thread pool (threadpool.cpp:603):
→ router.run_timeloop_task(peer, "frametasks_timeloop")
→ call_sync_regfun finds the controller function with the same name (registered at serverwatch.cpp:67)
→ After execution, inflight_release() resets timeloop_inflight
Drogon EventLoop Timer usage:
auto loop = drogon::app().getMainEventLoop();
// Run every 5 seconds
auto tid = loop->runEvery(5.0, [] { doSomething(); });
// Cancel
loop->cancel(tid);
Summary: Neither supports cron expressions; both are interval-based timers. But business-layer plug-in cost is drastically different. Paozhu binds scheduled tasks to route handlers + thread-pool execution, fully integrated with the framework routing system — write a sync controller function annotated with //@urlpath(null, taskname), plus an arm route (URL trigger or startup-time registration), and the task is into the scheduler; deleting the controller file auto-clears the task table. Zero boilerplate, zero framework-API learning cost, routes and tasks share one registration. But it only accepts sync annotation functions (fn_kind::sync); coroutine handlers are skipped (timeloop runs in a sync thread pool, no strand to co_spawn on). Drogon is a low-level EventLoop primitive (higher precision, richer API — runAt/runAfter), but interval-task→business-handler mapping, EventLoop ownership, TimerId lifecycle all must be handled by business code itself, with no controller annotation auto-registration.
| Protocol / Middleware | Paozhu | Drogon (drogon-master 1.9.14) |
|---|---|---|
| WebSocket | Built-in server + client | Built-in server + client |
| Redis | Built-in (pzredis, self-implemented RESP, per-section pool, sync/async) |
Built-in (nosql_lib/redis, based on hiredis) |
| MQTT | Built-in broker + client (MQTT5, retained / shared subscriptions) | None (third-party plugin required) |
| RPC | Built-in custom rpc protocol (rpc_parse / http_rpcclient) |
No built-in (no protobuf/thrift/gRPC) |
| Raw Socket / TCP | Built-in (http_socket + sock_subpub_reg) |
None (only HTTP addListener; raw TCP needs self-managed trantor/asio) |
| JWT | Built-in (jwt.cpp, HS256/RS256/ES256, OpenSSL3, self-implemented with security orientation) |
No built-in (third-party lib like jwt-cpp / community plugin required) |
| OAuth 2.0 / OIDC | Built-in authorization server + client (full endpoints + ORM models + GitHub integration) | No built-in (self-implementation / community plugin required) |
| Session | Built-in (httppeer.session is obj_val, weak-typed, file/memory dual storage, automatic SameSite) |
Built-in (Session class + SessionManager, strong-typed std::any, manual SameSite config) |
| HTTP Client | Built-in (sync/coroutine dual form; no HTTP/1.1 pipeline client, but pipeline is practically dead — Chrome/Firefox never enabled by default, HTTP/2 multiplexing + h2c covers both internet and intranet) | Built-in (sync/callback/future/coroutine four forms + HTTP/1.1 pipeline client setPipeliningDepth, HTTP/1.1 only — practically no browser or reverse proxy enables pipeline by default) |
| Monitoring / Prometheus | Basic built-in: serverwatch.cpp status route + Access log + interval task registration; no Collector/Registry abstraction; Prometheus metrics require business-layer instrumentation |
Built-in complete system (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable) |
| Scheduled tasks | Frame-driven: add_timeloop_task(taskname, interval_ticks) → route handler + clientrunpool thread pool. Globally unified scheduling, built-in single-flight protection. But precision limited by main loop frame rate, only "every N ticks" |
EventLoop-level: runAt/runAfter/runEvery + timerfd. Precision sub-millisecond, supports absolute time point and one-shot. But each EventLoop has its own independent TimerQueue, no global scheduling |
| Cross-protocol unified abstraction | Yes: ws/mqtt/socket/redis share subpub registration model |
None in framework layer (each capability independent). Business layer has PubSubService<> template tool |
| RequestStream streaming request body | Partial: multipart upload auto-persists at 3MB threshold (memory protection) + HTTP client save() streaming file writes; but no handler-per-chunk-callback streaming API |
Complete: RequestStreamReader + MultipartStreamParser, handler gets per-chunk callbacks |
| Reverse proxy | None (self-implement via HTTP client) | Yes (framework-level forward() + plugin example SimpleReverseProxy) |
| PostgreSQL LISTEN/NOTIFY | None (sync/coroutine queries only) | Yes (DbListener, event-driven, no polling) |
| Resident outbound clients | Yes: four types — Redis subscription / WS / raw TCP / MQTT — all start on framework boot, lifecycle managed by client_context, self-check route resident_stop |
None (DbListener is DB-layer only; HttpClient is on-demand) |
| Plugin system | No unified mechanism (AccessLog / static-file redirect embedded in server.cpp) | Complete AOP Advice (registerSyncAdvice/registerPreHandlingCallback/registerPostHandlingCallback + dependency topological sort) |
Core difference: Paozhu is "multi-protocol + built-in identity stack + resident outbound" — one framework is simultaneously HTTP/2, WebSocket, MQTT(broker), Redis, RPC, raw Socket server, with built-in JWT + OAuth 2.0/OIDC authorization server, plus Session, HTTP client, and four resident outbound clients (Redis subscription / WS / TCP / MQTT). But monitoring needs business-layer integration, no HTTP pipeline, no handler-per-chunk-callback streaming request body (only parser-layer auto-persist memory protection), no reverse proxy, no LISTEN/NOTIFY. Drogon is a "production-grade Web framework focused on HTTP" — WebSocket and Redis are first-class citizens, HTTP client is stronger (with pipeline), Session/monitoring/Prometheus/RequestStream/reverse proxy/DbListener are all built-in, with a unified plugin system. But JWT/OAuth/MQTT/RPC/TCP/resident outbound are delegated to external libraries or self-implementation — the framework itself doesn't build them in.
The following capabilities are directly provided by Drogon's framework layer, with no equivalent API in Paozhu:
RequestStream streaming request body (RequestStream.h + MultipartStreamParser.h): per-chunk callbacks; handler doesn't wait for the entire body to arrive before processing. Multipart stream parser decomposes while receiving. Enable with enableRequestStream(true); handler receives RequestStreamPtr. Official example examples/async_stream/RequestStreamExampleCtrl.cc.
Note: Paozhu also has large-file memory protection, but by a different mechanism. Multipart uploads have auto-persist at 3MB threshold (
http_parse.cpp: opensfpwhen threshold exceeded, thenpost_multipart_itemcontent_append()flushes accumulated blocks viafwrite). HTTP client hassave(path)→respreadtofile()streaming file-write interface. Neither accumulates large files in memory, but Paozhu doesn't have Drogon's framework-level "handler gets per-chunk callbacks" streaming API: Paozhu's parser layer auto-persists to disk, business handlers receive paths to already-persisted temp files (httppeer::upload_file.tempfile), business code never sees the byte stream itself.
Reverse proxy / forward() (HttpAppFramework.h:732-764): framework-level built-in reverse proxy. forward(req, targetHost) forwards requests to a specified backend. Plugin example examples/simple_reverse_proxy/ uses IOThreadStorage<vector<HttpClientPtr>> as a connection pool, isolated per EventLoop for lock-freedom.
DbListener — PostgreSQL LISTEN/NOTIFY (DbListener.h): based on PostgreSQL native LISTEN/NOTIFY, event-driven, no polling. DbListener::newPgListener(connectionInfo) → listen(chan, cb) / unlisten(chan). Complete db_listener_test.cc.
Plugin system (AOP Advice): unified framework plugin mechanism, three hooks registerSyncAdvice / registerPreHandlingCallback / registerPostHandlingCallback. Redirector, SlashRemover, AccessLogger, GlobalFilters, PromExporter, Hodor all go through this system; plugins can declare dependencies, startup order auto-topologically sorted.
At framework boot, four types of resident outbound clients can be spun up, sharing the same coroutine pool as HTTP handlers, for cross-process reporting / subscription:
| Type | Enable macro | Registration container | Typical use |
|---|---|---|---|
| Redis subscription resident | ENABLE_REDIS_CLIENT |
server.redis_subpub_tasks |
Subscribe to Redis channels, receive messages from other services |
| WebSocket outbound resident | ENABLE_WEBSOCKETS_CLIENT |
server.ws_subpub_tasks |
Connect to another WS service for reporting / two-way subscription |
| Raw TCP outbound resident | ENABLE_SOCKETS_CLIENT |
server.sockets_clients |
Connect to arbitrary TCP services (e.g. private protocol push) |
| MQTT outbound resident | ENABLE_MQTT_CLIENT |
server.mqtt_clients |
Connect to external MQTT broker for device reporting |
Unified management infrastructure:
client_context (client_context.h/cpp) — manages resident client lifecycle, auto-shuts down underlying connection on timeout, wakes blocked poll/recv (prevents zombie coroutines).resident_stop self-check route (resident_stop.cpp) — GET /resident_stop?kind=redis|ws|sock|mqtt&name=<conf_section>: calls stop() then waits for connection handles to clear, verifying resident coroutines exit correctly.add_client_task_loop(peer) at initialization time to hand resident coroutines over to client_context; no need to write heartbeats/reconnect loops yourself.subpub_reg model from §6.6, naturally interoperating with WS/MQTT/Socket servers inside the framework.Drogon comparison: No framework-level resident client registration system. DbListener (PostgreSQL LISTEN/NOTIFY) is DB-layer only; HttpClient is on-demand — neither is a "process-boot-spun, long-lived outbound long connection".
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Code generator | Build-time paozhu_pre (CMake target paozhu_codegen, CMakeLists.txt:466-492), scans controller/src |
drogon_ctl CLI tool |
| Input | //@urlpath(pre,urlpath) annotations in controller/src/*.cpp |
Manual / semi-automated skeleton generation via CLI |
| Incremental | cache/method_notes.data (md5+mtime) skips unchanged files (autopickmethod.cpp:170-288) |
— |
| Generated artifacts | controller/include/<name>.h (forward declarations) + common/autocontrolmethod.hpp (reg_raw route table) |
create_controller skeleton; create_view compiles .csp → DrTemplate subclass |
| Template engine | view/ HTML compiled into viewsrc/ C++ source (viewsrc/view/) |
CSP (C++ Server Pages): .csp compiles into DrTemplateBase subclass genText() |
| View mechanism | view() / theme_view() (multi-theme prefix) |
DrTemplate<ClassName> + newTemplate() |
Key differences:
drogon_ctl is a manually-executed CLI — artifacts persist in the repo.Paozhu — Annotation functions (from controller/src/testrand.cpp, real minimal form):
#include "httppeer.h"
namespace http
{
//@urlpath(null,testrand) // site(null = global slot) + URL path; no pre-filter
std::string testrand(std::shared_ptr<httppeer> peer)
{
httppeer &client = peer->get_peer();
client << "<p>Random number: " << rand_range(100, 999) << "</p>";
return ""; // empty string = normal completion / pass; non-empty = jump or terminate
}
}//namespace http
std::string handler(shared_ptr<httppeer>) registers as sync handler (reg_sync slot in reg_raw); returning asio::awaitable<std::string> falls into the co_reg coroutine slot — sync/coroutine determined by signature, no extra macro needed.paozhu_pre scans //@urlpath at build time and auto-generates the reg_raw route table; developers write zero registration statements.Drogon — Registration macro (HttpController + reflection, class name auto-derived to path):
class TestCtrl : public drogon::HttpController<TestCtrl>
{
public:
METHOD_LIST_BEGIN
// Path "/testrand", GET method, binds testrand method
ADD_METHOD_TO(TestCtrl::testrand, "/testrand", Get);
METHOD_LIST_END
void testrand(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&cb) const
{
auto resp = HttpResponse::newHttpResponse();
resp->setBody("<p>Random number</p>");
cb(resp); // callback-style return
}
};
HttpController<Derived> subclasses. Static self-registration happens via DrObject reflection before main. Class name TestCtrl maps to /testctrl by default (can be explicitly overridden with ADD_METHOD_TO).std::future / CoroMapper/Task coroutine), unlike Paozhu's "signature is slot".One-liner: Paozhu is "one
//@urlpathannotation in business code completes registration" — declarative, zero boilerplate. Drogon is "declare routes inside the class withMETHOD_LIST_BEGIN/ADD_METHOD_TOmacros" — macro reflection, requires inheriting from anHttpControllerbase class.
vendor/httpcli) vs. Database Migration ToolsClarification: Paozhu's vendor/httpcli/ is not a database migration tool — it's the build-time codegen / ORM schema parsing engine (the implementation behind paozhu_pre, the CMake target paozhu_codegen), equivalent to Drogon's drogon_ctl. It does three things:
autopickmethod.cpp (@dest controller method pick file) scans //@urlpath annotations to generate the reg_raw route table; autopickcontrolmethod.hpp / controllerparse.hpp parse controllers.dbtypes.hpp (parse_pg_ddl/parse_sqlite_ddl/parse_mysql_show_create/gen_ddl) + dbconvert.hpp/dbcreate.hpp/modelfun.hpp, compiling schema/<tag>/tables/*.sql into orm/*/include/*_opsql.h and similar model headers.struct_reflectfile.cpp, templateparse.hpp, dbdump_all.hpp/dbdump_schema.hpp/dbschema_check.hpp (the latter is a "schema/<tag>/tables/*.sql management tool" — validates SQL completeness, uses gen_ddl() to rewrite normalized SQL — manages schema files themselves, not runtime migration).Migration tool comparison:
Paozhu (httpcli engine) |
Drogon (drogon_ctl) |
|
|---|---|---|
| Nature | Build-time codegen: dual mode — live DB introspection (modelfun.hpp runs SHOW CREATE TABLE / PG table lists) or offline reading schema/*.sql. Both generate ORM models + controller extraction + struct reflection |
CLI: connects to live DB (create_model) → ORM models / generates controller/view/plugin skeletons |
| Schema source | Dual mode: database-first (live DB introspection) + offline SQL files (handwritten or dumped from live DB) | Database-first (connects to live DB for create_model) |
| Runtime migration (versioned up/down) | None; dbschema_check only validates/normalizes SQL files |
None; create_model only reverse-generates models |
| Equivalence | httpcli ≈ drogon_ctl (both codegen tools, neither is a migration tool) |
Conclusion: Neither has Flyway/Liquibase-style versioned migration executors, and both support database-first (connect to live DB to reverse-generate models: Drogon
create_model; Paozhumodelfun.hpprunsSHOW CREATE TABLE/ PG table lists). The difference is Paozhu additionally supports pure offlineschema/*.sqlgeneration (CI-friendly, decoupled from DB), while Drogon'screate_modelmust connect to a live DB. If you need a real DB migration pipeline, both need external tools or self-managed SQL scripts — this is the uncovered area in the "full self-built stack" engineering story.
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Support | Full-stack coroutines: asio::awaitable<T> + co_await + co_spawn(..., detached) throughout server/router/httppeer/ORM/clients |
Complete C++20 coroutines: Task<T> (coroutine.h:160), AsyncTask, CallbackAwaiter, when_all, Mutex coroutine lock |
| Trigger points | Per-connection http1loop/http2loop coroutines; router co_resolve; DB queries |
ORM CoroMapper; co_future/sync_wait bridging futures |
| Positioning | Coroutines are the primary paradigm (even ORM); callbacks are rare | Coroutines are an optional paradigm; default is callbacks/future, coroutines are enhancement |
Key difference: Paozhu makes coroutines the only first-class citizen (IO, business, DB all end-to-end awaitable). Drogon is a hybrid model: "callbacks/future as base + coroutine overlay". Paozhu uses Asio's standard co_await; Drogon has self-developed Task/Awaiter system.
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Mechanism | Site slot model: host_index → slot, _site_to_slot maintains one slot per site. Directory namespaces via split_domain_path |
Multiple apps / multiple controller groups; depends on DrObject naming |
| Themes | theme_view() auto-adds themes= prefix; get_theme() (httppeer.h) |
No built-in equivalent to theme_view (requires self-implementation) |
Paozhu's multi-tenancy is framework-level built-in (domain → slot → route-table isolation + theme views). Drogon has no equivalent one-stop multi-tenant abstraction in this tree.
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Core third-party | Asio (can switch to boost::asio), OpenSSL 3; SQLite optionally links SQLite::SQLite3 (ENABLE_SQLITE=ON); MySQL/PG have no library dependencies (self-implemented protocol) |
trantor (self-developed net lib), OpenSSL / Botan (pluggable TLS) |
| Build | CMake, build-time codegen (paozhu_pre), controller/include auto-generated |
CMake, drogon_ctl external CLI generates |
| Additional modules | Mostly self-built inside vendor/ with zero external deps: pzimage(images) / pzexcel(Excel) / pzword(Word) / webpdf(PDF) / pzcharts(charts) / sms(SMS) / webpay(payments) / pzzip(compression) / pzredis(Redis client) / httpcli(codegen). Only SQLite links sqlite3; HTTP client optionally links brotlienc/brotlidec for brotli |
Data-layer libs: nosql_lib/redis (Redis client, based on hiredis) + orm_lib (ORM, all MySQL/MariaDB/PG/SQLite link official client libs). Note: nosql_lib/ in drogon-master only has redis/ (no MongoDB/Cassandra etc.). Images/Excel/Word/PDF/charts/SMS/payments — none built-in, requires opencv / libxlsxwriter / libharu etc. external libs |
| Parsers | All self-built (HTTP1/HTTP2/HPACK — no third parties) | Self-built HTTP1 parser; drogon-master has no HTTP/2 |
| Certificate automation (ACME / OCSP) | Built-in ACME client: acme_client.cpp + acme_run.cpp + acme_config.cpp auto-issue/renew, paired with acme.conf/ocsp_intv_time; OCSP stapling auto-refreshed by ocsp_check.cpp |
No built-in ACME client (only allows uri_prefix: /.well-known/acme-challenge/ in static location config to serve HTTP-01 challenge files). lib/ has no OCSP stapling implementation (ocsp in BotanTLSProvider is only the OCSP response received during client-side cert validation, not server-side stapling). Certificate issuance/renewal relies on external tools |
| TLS backend | OpenSSL 3 only | OpenSSL / Botan — pluggable with conditional compilation |
| Compression | Both gzip + brotli built-in (gzip.h / https_brotli.h; both follow §2.1 static-file compression cache strategy: first-request lazy compress to disk, zero CPU on hit) |
Both gzip + brotli supported (gzip static-file lookup enabled by default; brotli via USE_BROTLI conditional compilation; both follow pre-compressed file lookup — framework reads but never writes) |
| Process supervision | Framework-level self-protection + daemonize: ① Deadlock detection httpwatch_check_deadlock() — core mechanism is two counter equilibrium. Accept is an independent blocking thread (not running on io_context): SSL accept increments total_http2_count, HTTP accept increments total_http1_count; sslhandshake coroutine decrements total_http2_count, clientpeerfun coroutine decrements total_http1_count (increment/decrement should balance). A separate total_count (increment-only, cumulative connections that ever reached clientpeerfun since boot). When handler deadlocks, accept still happily accepts new connections (total_httpX_count keeps incrementing, but coroutines queue on the deadlocked strand and never get a chance to decrement) → total_httpX_count > 4 and total_count freezes → watchdog thread checks every 5 seconds. After 3 consecutive checks (15 seconds), isstop=true → active exit. Covers all deadlock scenarios (including partial stalls: both accepts run, one protocol's count keeps growing but never decrements); ② Load-threshold restart httpwatch_check_restart_threshold() (configurable time window, auto-exit when sustained over-threshold); ③ Watchdog thread exit std::terminate() protection (watchdog thread crashing also triggers restart); ④ External restart files (restart_file / restart_ssl_file); ⑤ init_daemon.hpp for daemonize (fork→setsid→close fds) |
No framework-level self-protection: no deadlock detection, no periodic health checks, no watchdog thread exit terminate; relies on drogon::app().quit() / shutdown() for active shutdown; daemonize needs external tools |
| External restart chain | Framework actively exits (isstop=true → main loop break → process normal exit), paired with systemd Restart=on-failure / Docker --restart=always pulling up a new process — forming a complete closed loop: internal deadlock detection (within 15s) → auto-exit → external restart |
No active deadlock-exit mechanism; process crashes/stalls are passively killed and restarted by systemd WatchdogSec= (typically 30s+) / Docker health check timeout |
Consolidating all self-built points from previous sections into one statement: Paozhu has extremely thin mandatory external dependencies (only Asio + OpenSSL 3). On top of that, almost the entire stack is self-built — the only exception is SQLite (embedded DB, no independent wire protocol to self-implement, uses official sqlite3 C API, linked when ENABLE_SQLITE=ON). Drogon's core is also thin (trantor + OpenSSL/Botan), but capabilities are assembled from an ecosystem.
Paozhu self-built coverage (no corresponding third-party lib pulled in)
libmysqlclient/libpq (SQLite using official sqlite3 C API is the natural choice for an embedded DB with no wire protocol to self-implement).jwt.cpp, HS256/RS256/ES256, OpenSSL3, security-oriented self-implementation) + OAuth 2.0/OIDC authorization server (libs/oauth + full endpoints in controller/src/oauth + GitHub integration) built-in and self-implemented — self-host SSO without Keycloak/Auth0 external IdP (§6.7).vendor/httpcli/modelfun.hpp runs SHOW CREATE TABLE / PG table list, database-first) + offline read schema/*.sql, the latter decoupled from runtime DB, CI-friendly (§4.1/§7.2).acme_client.cpp/acme_run.cpp/ocsp_check.cpp), no certbot etc. external tools needed.subpub registration kernel (§6.6), the thread connecting this big family together.Drogon comparison (thin core + ecosystem assembly)
Costs and benefits come from the same source: Paozhu's "self-built big family" brings simpler deployment (fewer external libs to install), capability consistency, zero protocol authorization/version entanglement — but enormous code size, self-implemented correctness risk, and a small ecosystem/community (see §11). Drogon takes "HTTP depth + easy assembly" as its engineering mainline.
Paozhu strengths
asio::io_context exposed at the bottom. Any Asio library (asio-grpc, boost::beast, async_mqtt, boost::redis, quiche/nghttp2_asio, etc.) can be dropped in directly, with co_spawn/co_await ready to use immediately. Drogon is completely unusable (trantor EventLoop is a muduo-branch self-build, not asio::io_context).subpub kernel spanning multiple protocols. Simple deployment, no protocol library version entanglement.client_context, naturally enabling cross-process reporting / subscription.std::terminate + ORM connection health check (orm_connect_mar_t).Paozhu costs
io_context coroutine pool + clientrunpool business thread pool), massive dialect ORM headers (~450KB/dialect), enormous code size and maintenance surface.Drogon strengths
forward(), PostgreSQL LISTEN/NOTIFY (DbListener), plugin system (AOP Advice + dependency topological sort), static-file pre-compression lookup, HTTP/1.1 pipeline and its client counterpart (HTTP/1.1 only; TCP head-of-line blocking remains — and practically nobody uses it: Chrome/Firefox never enabled by default, Firefox 54 removed it, Nginx defaulted to HTTP/1.0 until 1.29.7; HTTP/2 multiplexing + h2c solves this completely).Collector/Counter/Histogram + PromExporter plugin, JSON config to enable).Drogon costs (measured in drogon-master)
lib/ 1.9.14: HttpServer.cc only outputs HTTP/1.1, no Http2Server/ALPN registration).asio::io_context. All Asio third-party libraries (asio-grpc, boost::beast, async_mqtt, etc.) cannot directly connect.WatchdogSec= / Docker health check passive timeout kill+restart.wait_timeout).Concept clarification: Multi-domain virtual host = one physical server bound to multiple domains, each domain runs an entirely independent website (domain-level isolation: separate certs, roots, hooks); SaaS multi-tenancy = one or more domains serving multiple tenants, tenants share the same codebase (usually data-isolated by tenant_id field). These are different scenarios.
| Scenario | Recommendation |
|---|---|
| Multi-domain virtual host / hosting platform (one server bound to multiple domains, each site with independent certs/roots/hooks/CORS/upload limits) | Paozhu (built-in at framework level: host_index auto-matches sitehostinfos by Host header, each site gets 12+ independent config fields; Drogon has zero virtual host code — can only route at the Nginx proxy layer + multi-instance) |
| SaaS multi-tenancy (one or more domains serving multiple tenants, need tenant isolation) | Paozhu (framework-level virtual host + siteid/groupid fields assist tenant auth. When each tenant gets its own subdomain/domain, certs/roots/hooks/CORS/upload limits auto-isolate — business code only needs to switch tenant context by host_index. Drogon has no site isolation fields; tenant isolation is entirely business-layer) |
| Need HTTP/2 server support | Paozhu (drogon-master ships no HTTP/2 server; Paozhu has it built-in) |
| Prioritize mature ecosystem, docs, community, quick onboarding | Drogon |
| Full-stack coroutine async, unified mental model | Paozhu (coroutine as primary paradigm) |
| Themed sites / multiple template sets (same domain, different theme directories/template rendering) | Paozhu (themes + themes_url per-site fields, framework built-in; Drogon has no site-isolated theming mechanism) |
| Offline CI / ORM generation without runtime DB | Paozhu (SQL schema generation) |
| Classic Reactor + reflection macros + compile-time registration | Drogon |
| One-stop zero-dep big family (multi-protocol + self-managed certs + self-built DB protocol, install fewer external libs) | Paozhu |
| Built-in auth/SSO needed (JWT + OAuth 2.0/OIDC authorization server, avoid external IdP) | Paozhu |
| Need HTTP/1.1 pipeline (practically dead: Chrome/Firefox never enabled by default, Firefox 54 removed it, Nginx defaulted to HTTP/1.0 until 1.29.7; Paozhu has no HTTP/1.1 pipeline, but HTTP/2 multiplexing + h2c (plaintext) covers concurrent scenarios both inside and outside the network — equivalent and more thorough) | Drogon (HttpServer.cc pipeline queue + HttpClient setPipeliningDepth()) |
| Need built-in Prometheus monitoring | Drogon (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable) |
| Need HTTP/1.1 pipeline client (practically dead — HTTP/2 multiplexing already covers concurrent requests; intranet can use h2c) | Drogon (HttpClientImpl.h:62 exclusive capability) |
| Connection-level protection preference "hard reject" vs "queued slow admission" | Drogon (HttpConnectionLimit hard-reject type) vs Paozhu (queued admission type, gentler) |
| Need RequestStream streaming request body / low-memory large-file upload | Drogon (RequestStreamReader + MultipartStreamParser per-chunk callback) |
| Need PostgreSQL LISTEN/NOTIFY event-driven | Drogon (DbListener, event-driven, no polling) |
| Need framework-level reverse proxy | Drogon (forward() + plugin example SimpleReverseProxy) |
| Internal proxy / inter-service calls (plaintext HTTP/2 + multiplexing, avoid TLS negotiation overhead) | Paozhu (h2c plaintext HTTP/2 with multiplexing — zero TLS overhead, multiple requests interleaved on a single connection; Drogon has no HTTP/2 server, so reverse proxies and inter-service traffic can only use HTTP/1.1 with no multiplexing) |
| Need unified plugin system (AOP Advice + dependency topological sort) | Drogon (registerSyncAdvice/registerPreHandlingCallback/registerPostHandlingCallback) |
| Static-file compression cache preference | Paozhu (lazy auto-compress to disk + mtime-aware; source file updates auto-recompress, zero ops cost) vs Drogon (pre-compressed .gz/.br files created at deploy/build time; framework only reads, never writes, no source-file mtime awareness — when source files are updated, ops must also update .gz/.br; memory cache (default 5s) won't refresh before expiry either) |
| E-commerce development (payment module) | Paozhu: built-in vendor/webpay/ (Alipay + WeChat Pay + Mini Program Pay, with signing/verification/callback handling, ENABLE_WEBPAY switch) + libs/oauth/ (own OAuth 2.0 authorization server) + full-coroutine async handling of payment callback high concurrency. Drogon side needs to self-integrate third-party SDKs, no OAuth server built-in |
| Enterprise development (WebPDF / office document processing) | Paozhu: built-in vendor/webpdf/ (HTML→PDF) + vendor/pzexcel//pzword/ (Excel/Word read/write) + pzimage/ (image processing) + pzcharts/ (charts) — zero external deps, CI/CD one-line compile. Drogon side needs separate libharu/libxlsxwriter/opencv etc., complex build chain |
| IoT development (MQTT module) | Paozhu: built-in MQTT broker + client (MQTT5, retained messages, shared subscriptions, subpub_reg unified dispatch) — ready to use, no need to deploy extra EMQX/Mosquitto. Drogon has zero MQTT code across the whole repo, needs third-party plugin or external broker |
| Need HTTP/2 + large-module one-stop | Paozhu (HTTP/2 + MQTT broker + JWT + OAuth + ACME/OCSP + webpay/webpdf/pzexcel/pzword/pzredis, all built-in with zero deps) |
| Certificate automation, zero ops (ACME auto-issue/renew + OCSP stapling auto-refresh) | Paozhu: framework-built acme_client.cpp/acme_run.cpp/ocsp_check.cpp, configure acme.conf/ocsp_intv_time and it's fully automatic (daily scheduled OCSP renewal). Drogon has zero built-in: ACME requires certbot/lego/openssl scripts + cron + manual nginx stapling; OCSP stapling is zero code across the whole repo |
| Need Asio third-party library ecosystem compatibility | Paozhu (directly uses standalone Asio, asio::io_context exposed at the bottom. Any Asio library — asio-grpc / beast / async_mqtt / boost::redis / quiche / nghttp2_asio etc. — can be dropped in directly, co_spawn/co_await ready to use). Drogon unavailable (EventLoop is trantor's self-built muduo branch, epoll_wait + timerfd + min-heap, not asio::io_context, Asio ecosystem libraries cannot directly connect) |
| Framework-level self-protection (deadlock detection + active exit restart closed loop) | Paozhu (accept independent thread + handler-layer naturally isolated detection mechanism, covers all deadlock scenarios, actively exits within 15 seconds; faster than systemd WatchdogSec=30s) |
vendor/httpserver/src/server.cpp, include/client_session.h:56-108 (per-connection asio::strand serialization), src/pool_step.cpp:79-117 + include/pool_step.h (dual-pool concurrency: co_pool_run_step / run_one_step_with_peer / peer chained shared ownership / rejected=true in-place completion to prevent deadlock), include/router.h:79, include/httppeer.h:41, src/http2_parse.cpp, vendor/httpcli/autopickmethod.cpp, vendor/httpcli/modelfun.hpp (ORM dual-mode generation: live DB introspection + offline SQL), autopickcontrolmethod.hpp:411, conf/server.conf, vendor/httpserver/include/orm_cache.hpp (model_meta_cache + orm_cache_deadline TTL clamping).trantor/trantor/net/EventLoop.h, net/TcpServer.cc:65, net/inner/poller/EpollPoller.cc, lib/src/HttpRequestParser.cc, lib/src/HttpControllersRouter.cc:626, lib/inc/drogon/HttpController.h:31, orm_lib/inc/drogon/orm/Mapper.h:115, lib/inc/drogon/utils/coroutine.h:160, drogon_ctl/create_model.cc, drogon_ctl/create_view.cc.