Paozhu vs Drogon —C++ Web framework System Design Comparison
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 in lib/ or trantor/ (no Http2Server/Http2Connection; lib/src/HttpServer.cc only outputs HTTP/1.1, no h2/ALPN registration; trantor only has TLS-layer ALPN stubs). Paozhu ships HTTP/2 built-in (http2_parse.cpp and 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.
0. One-Line Positioning
- Drogon: Classic muduo-style reactor + reflection-macro registration + compile-time code generation. Industrial-grade, complete ecosystem, friendly high-level API.
- Paozhu (炮竹): Single
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.
1. Concurrency / I/O Model (Core Difference)
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| Underlying lib | Standalone Asio (#include , 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:
- Drogon's "multiple independent loops" fits the classic Reactor model more cleanly — a single connection binds to one thread for its full lifetime, lock-free, good cache locality.
- Paozhu's "single
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. businessclientrunpool).router::co_resolvedispatches incrementally: coroutine stepsco_awaitin place; synchronous steps go to the business pool (server.cpp:2351comment).
2. HTTP Protocol Layer
| Dimension | Paozhu | Drogon (this tree) |
|---|---|---|
| HTTP/1.1 | Custom parser http_parse.cpp (122KB) + request.cpp / httppeer.cpp. No HTTP pipeline (request-response serial) |
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) |
| 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/toRequest |
Note:
request.his a misleading name — Paozhu's actual HTTP request model ishttppeer.
2.1 Static File Serving + Compression Cache (Both Have It, Opposite Strategies)
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):
- Compress on first request: Detect Accept-Encoding → brotli / gzip → temp file →
renameatomic write (safe for concurrent first hits). - Cache filename:
{original_path}.{hash(host+"\n"+urlpath)}.bror.gzip. Subsequent hits justfopenand serve — zero CPU cost. - Large file protection: Skip compression for
content_length > 16MB(saves memory). - Bad-cache protection: Fall back to original content on compression failure, don't write cache; auto-detects 0-byte cache files, deletes and regenerates.
- Cache reclaim: Pushes
mtimeback on large-file hits; background daemon reclaims cold entries based onCONST_HTTP_STATIC_CACHE_REFRESH_AGE.
Drogon — Pre-compressed file lookup (config.example.json:296-301 + StaticFileRouter.cc):
- Framework reads but never writes:
gzip_static: true(default),brotli_static: false. On request, framework directly looks for pre-compressed.gz/.brfiles in the same directory. If not found, serves original. - Requires pre-compression via
gzip -k/brotliin CI/CD deploy pipelines. Framework itself never computes compression. - Plus:
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.
3. Routing System (Deep Dive)
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.
3.1 Registration & Code Generation
Paozhu (controller/src/*.cpp with //@urlpath(pre,urlpath):mask, scanned by paozhu_pre at build time):
- The generated
common/autocontrolmethod.hppcontains areg_rawcall 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, andmask_code(numeric in the//@urlpathannotation distinguishing HTTP methods). - Paths with a
:paramsegment get a separatereg_urlpath(site, path, {param_names...})table right after (autocontrolmethod.hpp:139-140). - Build also generates helper tables:
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 supportsADD_METHOD_VIA_REGEX(:35) for regex patterns.- Static self-registration: Each controller includes
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 beforemain(), zero runtime scanning. Class-name-to-path mapping is auto-derived byDrClassMap::demangle(typeid(T).name())(HttpBinder.h:235),::becomes/.
3.2 Matching Structures
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).
- If exact match fails, prefix fallback up to 6 segments (
router.cpptwo-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-keyedstd::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([^/]*)viaregex_replace(:564). At runtime,std::regex_matchdoes linear matching (:635-640).- Each route has
binders_[req->method()]buckets byHttpMethod;initMiddlewaresAndCorsMethodsauto-injects middlewares and opensOPTIONS/CORS (:58/63/75).
3.3 Pre-handler Chain & Sync/Coroutine Mixing
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.
- Sync/coroutine mixing is a built-in capability: Within one request, "sync-pre auth → coro-handler DB lookup" and "coro-pre → sync-handler" can be declared directly. The framework auto-dispatches based on each entry's
pre_kind/reg_kind. Developers never need to manually switch threads or wrap instd::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. - Built-in urlpath pre interception: Each route's
pre_sync/co_prechain 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) andco_run_site_hooks(coro:478) batch-runaction_pre_lists/action_after_lists. When the list exceeds the limit, it truncates to the firstsite_hook_limitentries rather than dropping the whole thing (avoids silent auth failures). - Flow stack draining: When the orchestration stack drains (
pop_flow_index), if the driver is a sync chain but the popped entry is a coroutine, draining stops (:364-368); if coroutine-driven, itco_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".
3.4 Simple Task Orchestration (Paozhu 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".
- Structure:
httppeerholds a lazily-allocated stackstd::unique_ptr> flow_method(httppeer.h:300-301) — stores names of subsequent handlers to execute. - Push API (
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.
- Pop/Consume (
httppeer.cpp:2156-2169pop_flow()popsback;router.cpp:340-368pop_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-657sync chain,834-839coro chain). - Flow control directives:
kFlowOk/kFlowExit(explicit stop, no pop) /kFlowTimeloop(transfer to event loop), interpreted byclassify_step(router.cpp:76-87). - Robustness: When popping an unregistered name, skip and continue to the next (don't swallow the whole chain,
: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.
3.5 Multi-Tenant / Site Slot
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.
3.6 Summary Table
| 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 | Per-route pre_sync/co_pre serial chain (@ok/@exit/jump) + site-level pre/after hooks |
Filters + Middlewares (sortable, reusable, decoupled from routing) |
| 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 | Site slot isolation (_site_to_slot) |
None; business-layer grouping |
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 built-in light sequential workflow capability and multi-tenancy right at the routing layer.
4. ORM Deep Dive
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.
4.1 Generated Code Structure
Paozhu (paozhu_cli orm , generator vendor/httpcli/modelfun.hpp) produces three layers per table:
_base.h: pure data/reflection layer —namespace_info containscol_count,enum cols,struct meta(field definitions),type::type aliases,col_names,null_colbitmap,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()._opsql.h:template class_opsql : public B_BASE containing all query/cache/dirty-update methods. Constructor fetchesorm_conn_poolfromget_orm_conn_pool_obj()per dbtag.Dirty-update mechanism: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 buildUPDATEfor dirty-bit fields. On successclear_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'sdirtyFlag_[]— both are "incremental writes of changed columns only".
models/include/.h: thin shell class : public _opsql<,_base> (models/include/World.h:11).
FK relationships live in external .cpp: 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).
Three pieces per field: getValueOfXxx() / getXxx() / setXxx() + setXxxToNull() (nullable).
JSON interop & validation: toJson() / updateByJson() / validateJsonForCreation/Update.
Private persistence friend methods: outputArgs / updateColumns / updateArgs / insertColumns, called internally by Mapper.
Dirty flag dirtyFlag_[]: determines insert/update writes only changed columns (Users.h:252-274).
Static SQL strings: 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.
4.2 Query API Style
4.2 Query API Style
Paozhu — Chained builder (returns M_MODEL&), method names lean toward "direct SQL verbs":
Read: count() / page() / fetch() / fetch_one() / fetch_to() / fetch_one_to().
Write: update_col / exec_update_col / replace_col.
Conditions: where() / whereIn / whereNull / whereEQ / per-column shortcuts eqId(); condition operators orm::wq::eq/nq/bt/be/lt/le.
JOIN: leftJoin().
Async: async_fetch / async_count / async_page, plus exec_* variants with _prepared.
Pagination auto-reuses existing conditions: 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 + Criteria (template Mapper in orm/Mapper.h:115, constructor takes DbClientPtr):
Read: findByPrimaryKey / findAll / count / findOne / findBy, each in three forms (sync/callback/future).
Write: insert / update / updateBy / increment / deleteOne / deleteBy / deleteByPrimaryKey.
Condition 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).
Streaming builder: 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 Criteria expression objects + Mapper streaming — more type-safe, but needs understanding the Criteria/SqlBinder system.
Pagination comparison:Paozhu's page() auto-reuses already-set where, completes "count + fetch + return navigation 4-tuple" in one call. Drogon's paginate(page,perPage) (Mapper.h:173/2013) only sets offset/limit; count() defaults to empty Criteria() (Mapper.h:459), so you need to pass the same Criteria to both count(criteria) and the query. Pagination navigation (total pages / window) you compute yourself.
4.3 Query-Result Caching (Largest Functional Gap)
4.3 Query-Result Caching (Largest Functional Gap)
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:
Enable: call use_cache() before query; subsequent calls automatically hit cache; save_cache(exptime) writes back.
Read: try_get_cache(name) returns std::optional (miss = nullopt, non-throwing); is_hit_cache flag indicates whether the current result came from cache.
Key: std::hash{}(sqlstring) or custom cache_key_name.
TTL clamping & background reclaim: 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, std::map + 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
—
4.4 Async & Coroutine Model
4.4 Async & Coroutine Model
Paozhu — Sync + coroutine dual form; coroutine is the primary paradigm:
Every query method has both sync and coroutine forms: sync fetch() / count() / page() / exec_fetch() / exec_count(); coro async_fetch() / async_count() / async_page() / async_exec_fetch().
Coro versions internally co_await select_conn->async_fetch_directly(...); DML does co_await edit_conn->async_exec_dml.
Connection pool 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.
Backends: pg_conn.cpp async_read_loop co_await; sqlite_conn.cpp goes through run_on_worker queue.
Business handler layer: 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:
Every Mapper method provides three sets: binder << Mode::Blocking (sync, throws exception) / binder >> rcb >> ecb (callback) / std::promise + get_future (future).
Bottom layer DbClient::operator<<(sql) returns SqlBinder (DbClient.h:256) — << binds params, >> registers callbacks, exec() executes.
Coroutines: CoroMapper (CoroMapper.h:67, inherits Mapper) returns MapperAwaiter, whose await_suspend plugs the callback into CallbackAwaiter and resume()s. Also has execSqlCoro, newTransactionCoro.
Result abstraction: Result (Result.h:57, reference-counted, polymorphic size()/affectedRows()/insertId() etc.) / Row (Row.h:46, operator[] returns Field).
4.4.1 Dual-Pool Concurrency Architecture (io_context Coroutine Pool + Dedicated ThreadPool)
4.4.1 Dual-Pool Concurrency Architecture (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:
Pool 1: Asio io_context coroutine pool (server.h:381, single io_context + runthreads). Runs all co_spawn coroutines — HTTP/1.1 · HTTP/2 · WS · MQTT · RPC.
Pool 2: Dedicated 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).
I/O layer: Each 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.
Business layer: Blocking code (router sync handlers, MQTT/socket sync hooks) is dispatched to 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).
Lifecycle / deadlock prevention: 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.
4.5 Transactions
4.5 Transactions
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("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();
A single 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.
Coroutine form: async_begin_commit() / async_commit() / async_rollback() / async_edit_query().
Cross-object: Native SQL has no model concept — naturally cross-table / cross-object.
② ORM layer (lock_conn binds the transaction connection to the model):
auto txnlink = std::make_unique("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
Models fetch their own connections from the pool (read-write separated). Running ORM operations directly inside a 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.
Cross-object: Multiple different model objects (different tables, different generated opsql classes) can all 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(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.
Supports isolation level selection: 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.
4.6 Relationships / Foreign Keys
4.6 Relationships / Foreign Keys
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.
Config structure Relationship (create_model.h:211, Type{HasOne,HasMany,ManyToMany} + PivotTable for many-to-many join tables).
Auto FK detection: tryAddAutoRelationship (create_model.cc:181); PG queries information_schema FK views, MySQL queries KEY_COLUMN_USAGE, SQLite queries PRAGMA foreign_key_list.
Generates method bodies (model_cc.csp:1580-1789): per-type generates getXxx(clientPtr) sync/async pairs — HasOne builds select * from where = ?, HasMany returns vector, ManyToMany does join + pivot returning vector>. Relationship queries go through DbClient/SqlBinder, can take a transaction client.
4.7 Type Mapping & Parameter Binding
4.7 Type Mapping & Parameter Binding
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:
PG: 2/4/8-byte integers go through htons/htonl/htonll for network order, format=1 (binary);
MySQL: uses getMysqlType() mapping (with static_assert rejecting unsupported types);
SQLite: picks type based on sizeof(T); also supports trantor::Date / vector / optional / Json::Value.
DB row → C++ field: Field::as() (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_[].
4.8 Connection Pools
4.8 Connection Pools
Paozhu: orm_conn_pool.cpp maintains a read-write separated pool per dbtag (async_get_mysql_select_conn for reads / async_get_mysql_edit_conn for writes); coroutine connection fetch/build via co_await async_add_*_connect; backends pg_conn/mysql_conn/sqlite_conn each implement async_* non-blocking I/O; started by init_orm_conn_pool(io_context,...) (server.cpp:9269).
Framework-level connection health check 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 weak_ptr that have expired, skips async connections, and hard-closes (hard_close()) sync connections exceeding CONST_ORM_QUERY_CONNECT_TIMEOUT. Rate-limit: max 10 connections per scan round, scan over multiple rounds, sleeps 200ns between rounds to yield CPU.
ORM query logging: In debug mode, 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.
Underlying protocol implementation divergence (architecture-level):
Paozhu self-implements MySQL / PostgreSQL wire protocols; SQLite uses the official library:
MySQL: 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.
PostgreSQL: pg_conn.cpp hand-written build_startup_message, SCRAM-SHA-256 handshake (sasl_scram_sha256, int32_to_buf message frames).
SQLite: 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.
The entire project CMakeLists.txt does not link libmysqlclient / libpq (the cost of self-implementing MySQL/PG wire protocols), but SQLite needs SQLite::SQLite3.
Drogon wraps all official client libraries: MysqlConnection directly #include (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.9 Business Code Style (Actual Usage)
4.9 Business Code Style (Actual Usage)
§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 << "
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 + Criteria expression; CRUD by model type (example orm_lib/tests/db_test.cc):
using namespace drogon_model::postgres;
Mapper 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 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 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).
4.10 Summary Table
4.10 Summary Table
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//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 + 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:
Generation timing: Both support live DB reverse engineering (Drogon 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).
Abstraction style: Drogon uses template Mapper + Criteria for unified CRUD (OOP, type-safe); Paozhu generates per-table independent _opsql.h operation classes (verb-style builder, quick to pick up).
Caching is a watershed: Paozhu supports query-result caching out of the box; Drogon ORM has no such built-in layer.
Transaction API: Drogon is more elegant (Transaction object), Paozhu is more manual (lock_conn).
Dirty updates (incremental column writes) both support: Paozhu uses __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.
Common ground: Both ORM queries support C++20 coroutine async, and cover all three SQL dialects.
5. Rate Limiting Comparison
5. Rate Limiting Comparison
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.
5.1 Drogon — Request-Level RateLimiter + Hodor Plugin
5.1 Drogon — Request-Level RateLimiter + Hodor Plugin
Core class (lib/inc/drogon/RateLimiter.h):
RateLimiterType three algorithms: kFixedWindow / kSlidingWindow / kTokenBucket (RateLimiter.h:10-15).
Factory RateLimiter::newRateLimiter(type, capacity, timeUnit=60s) (RateLimiter.h:42-45), interface isAllowed()→bool (:52); SafeRateLimiter wraps with mutex for thread safety (:56-74).
Three implementations: TokenBucketRateLimiter / SlidingWindowRateLimiter / FixedWindowRateLimiter (lib/src/ .cc/.h files).
Hodor plugin (lib/inc/drogon/plugins/Hodor.h, official Drogon rate-limiting plugin):
JSON config to enable. Intercepts via AOP onHttpRequest (Hodor.h:144) AdviceCallback pre-advice. On limit hit returns rejection_message (default "Too many requests").
Complete dimension support (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.
Property: hard-reject — over-limit immediately closes new connections, no queue, no wait. Different approach from Paozhu §5.2's "queued slow admission" for the same problem.
5.2 Paozhu — Connection-Admission Overload Protection + Upload Limits + IP Blocking
5.2 Paozhu — Connection-Admission Overload Protection + Upload Limits + IP Blocking
Connection admission throttling (built into httpserver core, server.h:247 ratelimiter() thread-driven):
60-second traffic window: 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".
Load-adaptive admission rate: 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.
Queued slow admission: When thresholds exceeded (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).
Property: per-connection (not per-request) total-cap gate — overload-smoothing admission of new connections, not "N requests per IP/user per second" quota.
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 (maintainingstd::unordered_map>) 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.
5.3 Summary Table
5.3 Summary Table
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.
6. Protocol & Middleware Support (WebSocket / MQTT / Redis / RPC / Socket)
6. Protocol & Middleware Support (WebSocket / MQTT / Redis / RPC / Socket)
This dimension diverges sharply: Paozhu builds all five protocol types natively intohttpservercore (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.
6.1 WebSocket
6.1 WebSocket
Paozhu (built-in server + client): 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.
Drogon (built-in server + client): 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.
6.2 Redis
6.2 Redis
Paozhu (built-in, 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.
Drogon (built-in, 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.
6.3 MQTT
6.3 MQTT
Paozhu (built-in broker + client, complete implementation): 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 (absent): The entire 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.
6.4 RPC
6.4 RPC
Paozhu (built-in custom RPC protocol): 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.
Drogon (not built-in): The only match for rpc in lib/ is a Windows-specific #include (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.
6.5 Raw Socket / TCP Server
6.5 Raw Socket / TCP Server
Paozhu (built-in): 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.
Drogon (absent): 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.
6.6 Architecture Theme: Unified subpub Registration Model
6.6 Architecture Theme: Unified subpub Registration Model
Paozhu 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 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.
6.7 JWT & OAuth 2.0 / OIDC
6.7 JWT & OAuth 2.0 / OIDC
Paozhu (built-in JWT + complete OAuth 2.0 / OIDC server + client):
JWT: 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.
OAuth 2.0 / OIDC authorization server: 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.
OAuth client + third-party integration: 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 (not built-in): The entire 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).
6.8 Session Management
6.8 Session Management
Paozhu (built-in, weak-typed): 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).
Drogon (built-in, strong-typed): Session class (lib/inc/drogon/Session.h, map + mutex) with lifecycle managed by framework-level SessionManager, configurable timeouts. any + any_cast strong-typed access — safer than Paozhu's obj_val but heavier. SameSite needs manual configuration.
6.9 HTTP Client
6.9 HTTP Client
Paozhu (built-in, full-coroutine style): 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 pipeline client.
Drogon (built-in, three forms + pipeline client): HttpClient.h + HttpClientImpl.cc, sync/callback/future three forms + coroutine Task overlay. Exclusive HTTP pipeline client — setPipeliningDepth() batch sends, pipeliningCallbacks_ queue calls back in order (HttpClientImpl.h:62/185). Complete tests in examples/client_example/ and tests/integration_test/client/.
6.10 Monitoring & Observability
6.10 Monitoring & Observability
Paozhu (base built-in + business-layer extensibility): Core framework provides basic self-check capability: 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).
Drogon (built-in complete Prometheus system): 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.
6.11 Timers & Scheduled Tasks
6.11 Timers & Scheduled Tasks
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 — 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.
6.12 Summary Table
6.12 Summary Table
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 pipeline client)
Built-in (sync/callback/future/coroutine four forms + pipeline client setPipeliningDepth)
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.
6.13 Drogon-Exclusive HTTP Enhancements (Items marked as Drogon-only in §6.12 table)
6.13 Drogon-Exclusive HTTP Enhancements (Items marked as Drogon-only in §6.12 table)
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> 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.
6.14 Paozhu-Exclusive Resident Outbound Clients (Items marked as Paozhu-only in §6.12 table)
6.14 Paozhu-Exclusive Resident Outbound Clients (Items marked as Paozhu-only in §6.12 table)
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=: calls stop() then waits for connection handles to clear, verifying resident coroutines exit correctly.
Framework-level coroutine registration point — business code calls add_client_task_loop(peer) at initialization time to hand resident coroutines over to client_context; no need to write heartbeats/reconnect loops yourself.
Integrated with subpub — all four resident client types share the 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".
7. Controllers & Code Generation
7. Controllers & Code Generation
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/.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 + newTemplate()
Key differences:
Paozhu's codegen is part of the build pipeline (auto-runs before each main-program compile, forces route table refresh). Drogon's drogon_ctl is a manually-executed CLI — artifacts persist in the repo.
Views: Paozhu self-developed HTML→C++ compilation; Drogon uses CSP (C++ inline templates). Both share the philosophy of "template compiled into binary", but syntax and mechanism differ.
7.1 Minimal Working Examples (Annotation Functions vs. Registration Macros)
7.1 Minimal Working Examples (Annotation Functions vs. Registration Macros)
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 peer)
{httppeer &client = peer->get_peer();client << "Random number: " << rand_range(100, 999) << "
";return ""; // empty string = normal completion / pass; non-empty = jump or terminate
}
}//namespace http
Signature std::string handler(shared_ptr) registers as sync handler (reg_sync slot in reg_raw); returning asio::awaitable 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
{public:METHOD_LIST_BEGIN// Path "/testrand", GET method, binds testrand methodADD_METHOD_TO(TestCtrl::testrand, "/testrand", Get);METHOD_LIST_ENDvoid testrand(const HttpRequestPtr &req,std::function &&cb) const{auto resp = HttpResponse::newHttpResponse();resp->setBody("Random number
");cb(resp); // callback-style return}
};
Methods live inside HttpController 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).
The sync/async choice lives on the individual method form (callback / 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.
7.2 Code Generation Engine (vendor/httpcli) vs. Database Migration Tools
7.2 Code Generation Engine (vendor/httpcli) vs. Database Migration Tools
Clarification: 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:
Controller method extraction: autopickmethod.cpp (@dest controller method pick file) scans //@urlpath annotations to generate the reg_raw route table; autopickcontrolmethod.hpp / controllerparse.hpp parse controllers.
SQL schema → ORM model: dbtypes.hpp (parse_pg_ddl/parse_sqlite_ddl/parse_mysql_show_create/gen_ddl) + dbconvert.hpp/dbcreate.hpp/modelfun.hpp, compiling schema//tables/*.sql into orm/*/include/*_opsql.h and similar model headers.
Struct reflection / templates: struct_reflectfile.cpp, templateparse.hpp, dbdump_all.hpp/dbdump_schema.hpp/dbschema_check.hpp (the latter is a "schema//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: Drogoncreate_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.
8. C++20 Coroutines
8. C++20 Coroutines
Dimension
Paozhu
Drogon (this tree)
Support
Full-stack coroutines: asio::awaitable + co_await + co_spawn(..., detached) throughout server/router/httppeer/ORM/clients
Complete C++20 coroutines: Task (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.
9. Multi-Tenant / Theme
9. Multi-Tenant / Theme
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.
10. Dependencies & Engineering
10. Dependencies & Engineering
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
10.1 Wrap-Up: Zero-Third-Party Built-in "Big Family" (Paozhu Engineering Mainline)
10.1 Wrap-Up: Zero-Third-Party Built-in "Big Family" (Paozhu Engineering Mainline)
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)
Transport/app protocols: HTTP/1.1, HTTP/2 (HPACK/ALPN/h2c), WebSocket, MQTT (incl. broker), Redis client, RPC, raw Socket — wire protocol/frame codecs all hand-written; no libwebsockets / mosquitto / hiredis / self-built RPC frameworks.
DB wire protocols: MySQL (caching_sha2_password handshake) + PostgreSQL (SCRAM-SHA-256 handshake) fully self-implemented, do not link libmysqlclient/libpq (SQLite using official sqlite3 C API is the natural choice for an embedded DB with no wire protocol to self-implement).
Identity/security: JWT (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).
ORM generation dual mode: live DB introspection (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).
Certificate automation: built-in ACME client + OCSP stapling (acme_client.cpp/acme_run.cpp/ocsp_check.cpp), no certbot etc. external tools needed.
Rate limiting: connection-admission overload protection self-implemented (§5), not a plugin.
Unified abstraction: ws/mqtt/socket/redis share subpub registration kernel (§6.6), the thread connecting this big family together.
Drogon comparison (thin core + ecosystem assembly)
Redis uses hiredis, DB drivers wrap libmysqlclient/libpq/sqlite3, rate limiting is the Hodor plugin, ACME depends on external tooling — and MQTT broker, RPC framework, raw Socket server are all absent.
Tradeoff: focus on HTTP web framework, push "protocol big family" to ecosystem / external libraries, in exchange for maturity, documentation completeness, and pluggable TLS (Botan/OpenSSL).
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.
11. Design Tradeoffs Summary
11. Design Tradeoffs Summary
Paozhu strengths
Exclusive Asio third-party ecosystem: bottom layer uses standalone Asio with 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).
Zero-third-party built-in big family: HTTP/2 + MySQL/PG/SQLite wire protocols + MQTT(broker) + WebSocket + Redis(self-implemented RESP) + RPC + raw Socket + JWT + OAuth 2.0/OIDC authorization server + ACME/OCSP + webpay(payments) + webpdf/pzexcel/pzword/pzimage/pzcharts(document processing) — all self-built, only mandatory deps are Asio + OpenSSL; unified subpub kernel spanning multiple protocols. Simple deployment, no protocol library version entanglement.
Four resident outbound clients (Redis subscription / WS / raw TCP / MQTT), spun up on framework boot, lifecycle managed by client_context, naturally enabling cross-process reporting / subscription.
Framework-level self-protection: accept independent thread + handler-layer naturally isolated deadlock detection (two-counter equilibrium, within 15 seconds finds and actively exits, covers all deadlock scenarios — faster than systemd WatchdogSec=30s) + watchdog thread exit std::terminate + ORM connection health check (orm_connect_mar_t).
Full-stack C++20 coroutine unified mental model + HTTP/2 built-in + multi-tenant/theme views + ORM dual-mode generation (live DB introspection / offline SQL) + build-time auto codegen force-refreshing routes.
Paozhu costs
Dual thread-pool architecture (io_context coroutine pool + clientrunpool business thread pool), massive dialect ORM headers (~450KB/dialect), enormous code size and maintenance surface.
Framework's own community scale, documentation completeness, and framework-exclusive plugin count are far smaller than Drogon (but Asio third-party library ecosystem is exclusive, not constrained by this).
Everything self-built (HTTP/1.1/HTTP/2/HPACK/routing/DB wire protocols), correctness must be borne alone.
Drogon strengths
Mature industrial-grade reactor model (multiple independent EventLoops, MPSC cross-thread, epoll/kqueue).
Reflection macro registration + compile-time static self-registration, zero runtime scanning, auto-derived registration paths.
Exclusive HTTP enhancements: RequestStream streaming request body, reverse proxy forward(), PostgreSQL LISTEN/NOTIFY (DbListener), plugin system (AOP Advice + dependency topological sort), static-file pre-compression lookup, HTTP pipeline, pipeline client.
Prometheus monitoring complete built-in (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable).
Rich ecosystem (CSP templates, nosql, plugin, filter, ORM three dialects, complete examples), active community, thorough documentation.
Drogon costs (measured in drogon-master)
No HTTP/2 server implementation (lib/ 1.9.14: HttpServer.cc only outputs HTTP/1.1, no Http2Server/ALPN registration).
No Asio ecosystem compatibility: EventLoop self-built, not asio::io_context. All Asio third-party libraries (asio-grpc, boost::beast, async_mqtt, etc.) cannot directly connect.
No framework-level big family: MQTT broker, RPC, raw Socket server, JWT, OAuth, ACME/OCSP, webpay, document processing, resident outbound clients — none built-in, all require self-extension.
No framework-level self-protection: no deadlock detection, no watchdog thread exit terminate, no active stall-exit mechanism — process crashes/stalls can only rely on systemd WatchdogSec= / Docker health check passive timeout kill+restart.
No framework-level connection health check (ORM zombie cleanup relies on DB server wait_timeout).
Handlers default to running on the ioLoop thread; heavy business logic needs manual offload; ORM generation depends on live DB, offline/CI requires spinning up a DB.
12. Selection Guide
12. Selection Guide
Scenario
Recommendation
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)
Multi-tenant SaaS / themed sites
Paozhu (framework built-in)
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 (complete server + client implementation)
Drogon (HttpServer.cc pipeline queue + HttpClient setPipeliningDepth())
Need built-in Prometheus monitoring
Drogon (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable)
Need HTTP pipeline client
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)
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)
Appendix: Key Source Evidence Index
Appendix: Key Source Evidence Index
Paozhu: 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).
Drogon (drogon-master): 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.