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)
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 : Note: . 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 ( ): Drogon — Pre-compressed file lookup ( ): Essential difference: Paozhu makes compression caching an auto-managed framework concern (lazy compression + atomic write + bad-cache guard + reclaim) — zero developer cost. Drogon pushes compression caching to the deployment phase — framework only reads, never writes, keeping itself lighter. Both have equivalent runtime performance (zero CPU after cache hit), but the first-access path and operational model are different. Both use "declarative route registration", but the mechanisms fork: Drogon uses compile-time reflection + macros + static self-registration; Paozhu uses source annotations + build-time codegen generating (see §3.4). Drogon has no equivalent. Paozhu ( at build time): Drogon (via macros, compile-time): Paozhu: Core , RPC/sync chains skip redundant exact lookup). Drogon: ): Paozhu — Each route splits into 4 independent slots; sync and coroutine are "first-class routing citizens": ); synchronous steps go to the business thread pool. Drogon: Pre-handler logic lives in Filters ( like "one route declares a sync-pre + coro-handler together". Conclusion: Yes, Paozhu has built-in simple task orchestration on for "sequential multi-handler orchestration within a single request". This means one request can do "handler A → push B, C, D → after A returns empty, run B→C→D in order", forming a lightweight sequential workflow / pipeline. Drogon has no equivalent built-in mechanism : its composition primitives are Filters/Middlewares (cross-cutting concerns, not sequential step orchestration) and handler-internal logic. To chain multiple handlers in Drogon, you'd manually call or redirect inside the handler — the framework layer provides no orchestration stack. Paozhu: Routes naturally attach to site slots — host_index → slot, _site_to_slot maintains one slot per site. Route tables are isolated by slot (router.cpp:337 find_name_in_slots(peer_slot(peer), ...)); the first argument of reg_raw is site (domain, empty string = global slot 0). Combined with theme views (§9), this is framework-level multi-tenancy. Drogon: Routes themselves carry no tenant concept. Multi-tenancy is achieved at the business layer via "multiple apps / multiple controller groups + DrObject naming" — no one-stop slot isolation. 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 + orchestration" with built-in light sequential workflow capability and multi-tenancy right at the routing layer. Both support MySQL / PostgreSQL / SQLite and their ORM queries support C++20 coroutine async. But they differ significantly in generation timing, code organization, query style, caching, transactions, and relationship expression . Paozhu ( per table: 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.io_context, multiple workers" is the classic Asio model. Coroutine frames live on the heap and don't occupy I/O threads. I/O and business are explicitly layered (I/O coroutines vs. business clientrunpool). router::co_resolve dispatches incrementally: coroutine steps co_await in place; synchronous steps go to the business pool (server.cpp:2351 comment).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=1drogon-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 stubsRequest/response model
httppeer (httppeer.h:41, enable_shared_from_this) holding urlpath / get / post / output / sessionAbstract 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 bodyPassed around via
HttpRequestPtr smart pointer; implicit conversion via fromRequest/toRequest
request.h is a misleading name — Paozhu's actual HTTP request model is httppeer2.1 Static File Serving + Compression Cache (Both Have It, Opposite Strategies)
server.cpp:395-518rename atomic write (safe for concurrent first hits).{original_path}.{hash(host+"\n"+urlpath)}.br or .gzip. Subsequent hits just fopen and serve — zero CPU cost.content_length > 16MB (saves memory).mtime back on large-file hits; background daemon reclaims cold entries based on CONST_HTTP_STATIC_CACHE_REFRESH_AGE.config.example.json:296-301 + StaticFileRouter.ccgzip_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.gzip -k / brotli in CI/CD deploy pipelines. Framework itself never computes compression.IOThreadStorage-isolated static file content cache (StaticFileRouter.h:150-153, per-EventLoop isolation, lock-free).3. Routing System (Deep Dive)
reg_rawtables. Additionally, Paozhu extra-provides simple task orchestration based on httppeer::flow_method3.1 Registration & Code Generation
controller/src/*.cpp with //@urlpath(pre,urlpath):mask, scanned by paozhu_precommon/autocontrolmethod.hpp contains a reg_raw call table (autocontrolmethod.hpp:127-141): reg_raw(site, path, pre_sync, co_pre, reg_sync, co_reg, mask_code). Each route registers 5 slots: synchronous pre, co_await-pre, synchronous handler, coroutine handler, and mask_code (numeric in the //@urlpath annotation distinguishing HTTP methods).:param segment get a separate reg_urlpath(site, path, {param_names...}) table right after (autocontrolmethod.hpp:139-140).common/autorestfulpaths.hpp, common/reghttpmethod.hpp, etc.HttpController.hMETHOD_ADD(method, pattern, ...) / ADD_METHOD_TO(method, path, ...) (HttpController.h:31-34) → registerMethod(&method, pattern, {...}, ...). Also supports ADD_METHOD_VIA_REGEX (:35) for regex patterns.class methodRegistrator + static methodRegistrator registrator_ (HttpController.h:127-139). The constructor registers the method name + parameter pack into the framework — completed by CRT static init before main(), zero runtime scanning. Class-name-to-path mapping is auto-derived by DrClassMap::demangle(typeid(T).name()) (HttpBinder.h:235), :: becomes /.3.2 Matching Structures
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-299router.cpp two-level lookup: lookup_exact → lookup_registered → lookup_key_in_slots, router.cpp:180-185). Unregistered hop names are skipped during chain draining (router.cpp:350-363).HttpControllersRouter + StaticFileRouter. Three structures (HttpControllersRouter.ccsimpleCtrlMap_: no-parameter exact match (lowercase-keyed std::unordered_map).ctrlMap_: parameterized but structurally fixed exact routes. ctrlMap_.find(loweredPath) directly hash-hits (HttpControllersRouter.cc:626).ctrlVector_: routes with :param / regex. At compile time {param} placeholders are replaced with ([^/]*) via regex_replace (:564). At runtime, std::regex_match does linear matching (:635-640).binders_[req->method()] buckets by HttpMethod; initMiddlewaresAndCorsMethods auto-injects middlewares and opens OPTIONS/CORS (:58/63/75).3.3 Pre-handler Chain & Sync/Coroutine Mixing
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)pre_kind/reg_kind. Developers never need to manually switch threads or wrap in std::async. This is a different design philosophy from Drogon's "handler picks one form, CoroMapper is a separate type" — Paozhu treats "is this a coroutine" as a property of each registration slot, not the handler as a whole.pre_sync/co_pre chain up in declaration order into a serial pre chain; classify_step() decides direction based on return value — @ok (pass), same-name (continue), @exit (explicit stop), jump name (router.cpp:76-87). First non-empty return short-circuits; empty string is the only "pass" signal (router.cpp:458-461). Additionally, site-level pre/after hooks: run_site_hooks (sync :462) and co_run_site_hooks (coro :478) batch-run action_pre_lists/action_after_lists. When the list exceeds the limit, it truncates to the first site_hook_limit entries rather than dropping the whole thing (avoids silent auth failures).pop_flow_index), if the driver is a sync chain but the popped entry is a coroutine, draining stops (:364-368); if coroutine-driven, it co_awaits inline (:865-873).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 declaration3.4 Simple Task Orchestration (Paozhu
flow_method)
httppeer, implemented via httppeer::flow_methodhttppeer holds a lazily-allocated stack std::unique_ptr> flow_method (httppeer.h:300-301) — stores names of subsequent handlers to execute.httppeer.cpp:2135-2155):
push_next(name): push_back (stack top, LIFO — last pushed runs first).push_last(name): push_front (stack bottom, FIFO — first pushed runs first, the default recommended for sequential breakdowns).push_steps({...}): batch-push.httppeer.cpp:2156-2169 pop_flow() pops back; router.cpp:340-368 pop_flow_index): After a handler completes normally (returns empty string), the router pops the next registered handler name from the stack and executes it. One handler pops exactly one; the rest wait for that handler to return empty before popping, until the stack empties or the chain hits the top (router.cpp:651-657 sync chain, 834-839 coro chain).kFlowOk / kFlowExit (explicit stop, no pop) / kFlowTimeloop (transfer to event loop), interpreted by classify_step (router.cpp:76-87).:350-363); a coroutine entry pushed onto a sync chain stops by design (:364-368).
3.5 Multi-Tenant / Site Slot
3.6 Summary Table
Dimension
Paozhu
Drogon (drogon-master 1.9.14)
Registration mechanism
Source
//@urlpath annotation + build-time codegen generating reg_raw tablesReflection 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 ([^/]*) regexExact match
_urlpath_map + prefix fallback (up to 6 segments)simpleCtrlMap_/ctrlMap_ hash; ctrlVector_ regexPre / cross-cutting
Per-route
pre_sync/co_pre serial chain (@ok/@exit/jump) + site-level pre/after hooksFilters + 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 interleavesSync/async lives on the individual method form (Callback/future/
CoroMapper), no slot-based mixed declarationSimple task orchestration
Yes:
httppeer::flow_method stack (push_next/push_last/pop_flow), multiple handlers execute in sequence within one requestNo built-in; manual call / redirect
Multi-tenant routing
Site slot isolation (
_site_to_slot)None; business-layer grouping
flow_method4. ORM Deep Dive
4.1 Generated Code Structure
paozhu_cli orm , generator vendor/httpcli/modelfun.hpp) produces three layers_base.h: pure data/reflection layer — namespace _info contains col_count, enum cols, struct meta (field definitions), type:: type aliases, col_names, null_col bitmap, assign_field_value (world_base.h:41-45, L76-80), plus dirty-field bitmap __dirty_bits + get_dirty_indices()/clear_dirty()/make_update_dirty_sql().
_opsql.h: template class _opsql : public B_BASE containing all query/cache/dirty-update methods. Constructor fetches orm_conn_pool from get_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 build UPDATE for dirty-bit fields. On success clear_dirty(). Short-circuits when the dirty set is empty (prevents accidental all-field updates) and refuses execution without WHERE (article_opsql.h:9866/15248/10319/15323/13120). Mechanistically equivalent to Drogon's dirtyFlag_[] — both are "incremental writes of changed columns only".
models/include/.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
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).
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
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)
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_contextcoroutine 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
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_conndirectly 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_connbinds 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 Transactionobject:
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
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
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
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.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 + Criteriaexpression; 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
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
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
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
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_numper 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 (maintaining std::unordered_map>) inside router's site pre hooks (action_pre_lists, router.cpp:462), and on limit hit return httppeer::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
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)
This dimension diverges sharply: Paozhu builds all five protocol types natively into httpserver core (and uses a unified subpub registration 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
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
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
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
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
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
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
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
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
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
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
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
clientrunpoolthread 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
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)
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: opens fp when threshold exceeded, then post_multipart_itemcontent_append() flushes accumulated blocks via fwrite). HTTP client has save(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)
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_stopself-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
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)
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 with METHOD_LIST_BEGIN/ADD_METHOD_TOmacros" — macro reflection, requires inheriting from an HttpController base class.
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: Drogon create_model; Paozhu modelfun.hpp runs SHOW CREATE TABLE / PG table lists). The difference is Paozhu additionally supports pure offline schema/*.sql generation (CI-friendly, decoupled from DB), while Drogon's create_model must 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
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
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
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)
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
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
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
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.