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. 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=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.h is a misleading name — Paozhu's actual HTTP request model is httppeer.

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 → rename atomic write (safe for concurrent first hits).
  • Cache filename: {original_path}.{hash(host+"\n"+urlpath)}.br or .gzip. Subsequent hits just fopen and 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 mtime back on large-file hits; background daemon reclaims cold entries based on CONST_HTTP_STATIC_CACHE_REFRESH_AGE.

Drogon — Pre-compressed file lookup (config.example.json:296-301 + StaticFileRouter.cc):

  • Framework reads but never writes: gzip_static: true (default), brotli_static: false. On request, framework directly looks for pre-compressed .gz / .br files in the same directory. If not found, serves original.
  • Requires pre-compression via gzip -k / brotli in 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_raw tables. Additionally, Paozhu extra-provides simple task orchestration based on httppeer::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.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).
  • Paths with a :param segment get a separate reg_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 supports ADD_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 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

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.cpp two-level lookup: lookup_exact → lookup_registered → lookup_key_in_slots, router.cpp:180-185). Unregistered hop names are skipped during chain draining (router.cpp:350-363).

Drogon: HttpControllersRouter + StaticFileRouter. Three structures (HttpControllersRouter.cc):

  • simpleCtrlMap_: no-parameter exact match (lowercase-keyed std::unordered_map).
  • ctrlMap_: parameterized but structurally fixed exact routes. ctrlMap_.find(loweredPath) directly hash-hits (HttpControllersRouter.cc:626).
  • ctrlVector_: routes with :param / regex. At compile time {param} placeholders are replaced with ([^/]*) via regex_replace (:564). At runtime, std::regex_match does linear matching (:635-640).
  • Each route has 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

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 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.
  • Built-in urlpath pre interception: Each route's 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).
  • 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, it co_awaits inline (:865-873).

Drogon: Pre-handler logic lives in Filters (HttpFilter + ADD_FILTER_TO / GlobalFilters.cc) and Middlewares (HttpMiddleware, MiddlewaresFunction.cc + HttpAppFramework.h:718). The framework executes them before handlers, ordered by path / global ordering, decoupled from the routing table, reusable, sortable. The sync/async choice lives on the individual method form (binder's Blocking/Callback/future triad, or coroutines via separate CoroMapper). There is no slot-based mixed declaration like "one route declares a sync-pre + coro-handler together".

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: httppeer holds a lazily-allocated stack std::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-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).
  • Flow control directives: kFlowOk / kFlowExit (explicit stop, no pop) / kFlowTimeloop (transfer to event loop), interpreted by classify_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 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).

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)

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_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

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

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 + 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

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_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 (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 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

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_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

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 //@urlpath annotation in business code completes registration" — declarative, zero boilerplate. Drogon is "declare routes inside the class with METHOD_LIST_BEGIN/ADD_METHOD_TO macros" — 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.