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


1. Concurrency / I/O Model (Core Difference)

Dimension Paozhu Drogon (this tree)
Underlying lib Standalone Asio (#include <asio.hpp>, asio::io_context exposed at the bottom, full compatibility with the Asio ecosystem) trantor (in-house network lib, muduo branch, epoll_wait + timerfd + min-heap, not an asio::io_context, Asio ecosystem libraries cannot directly integrate)
Event loop topology Single shared io_context, N worker threads each calling io_context.run() One EventLoop per thread (one loop per thread, muduo style), multiple ioLoops running in parallel
Connection dispatch Each connection is bound to an asio::strand (client_session.h:194) to guarantee protocol ordering TcpServer uses nextLoopIdx_ round-robin to assign new connections to an ioLoop (TcpServer.cc:65)
Cross-thread communication Coroutine co_await + strand serialization Lock-free MPSC queue funcs_ + wakeupFd_ (EventLoop.h:311-321)
Business threads Dedicated business thread pool clientrunpool (server.h:487, hardware*2+2, dynamically resizable). Synchronous handlers run inside the pool Handlers default to running on the owning ioLoop thread; can manually offload or coroutine-switch threads
Thread config cothreadnum, threadmax (conf/server.conf) numThreads / EventLoopThreadPool size

Philosophical divergence:

Blocking Scenario: 4 Slow Handlers Arrive Simultaneously

This is the most concrete illustration of the two architectures' core difference. Suppose four blocking handlers (slow SQL, heavy computation, file I/O) arrive at the same time — what happens?

Framework Thread model Outcome
Drogon Handlers run on the owning ioLoop thread by default. 4 threads = 4 ioLoops Clogs. All 4 ioLoops are pinned by blocking handlers, epoll_wait is never called, every other connection's read/write events on those threads queue up — 4 threads all blocked = the whole service freezes
Paozhu I/O thread pool handles accept/read/write only; blocking work goes to clientrunpool Doesn't clog. I/O threads never run business logic. Even if the business pool is fully saturated (queue buildup), I/O threads keep accepting new connections, reading requests, and returning overload responses (e.g. 503) — the service stays alive

Drogon's mitigation paths (but there is no built-in business thread pool):

Drogon's architecture simply has no such thing as a business thread pool — handlers run directly on their owning ioLoop thread. So a blocking handler can avoid pinning the ioLoop only through coroutine coverage or user-built infrastructure:

Mitigation A: Full coroutines (covers most scenarios, but has hard limits)
Drogon handlers can be written as coroutine functions. As long as every blocking operation has an async API — database via CoroMapper, outbound HTTP via co_await, file I/O via async versions — co_await suspends and the ioLoop is free to serve other connections. This works well.

But coroutines can't save operations with no async API. Think Chromium PDF generation, ffmpeg transcoding, OpenCV image processing, AI inference models — pure CPU computation or libraries that only expose synchronous interfaces. co_await does nothing for them — they run straight on the ioLoop and block it.

// ✅ Coroutine helps: DB query (has async API)
METHOD_ADD(MyController::slowQuery, "/query", Get) {
    auto result = co_await drogon::orm::CoroMapper(drogon::app().getDbClient()).filter(...).exec();
    // co_await yields, ioLoop is free to serve other connections
}

// ❌ Coroutine can't help: PDF generation (pure CPU, Chromium only has sync API)
METHOD_ADD(MyController::genPdf, "/pdf", Get) {
    auto pdf = chromium.generatePdf(html);  // synchronous block — pins the ioLoop!
    res->setBody(pdf);
    res->send();
}

Mitigation B: User-built business thread pool
For scenarios coroutines can't cover (pure computation, sync third-party libs), Drogon's architecture has zero built-in business thread pool or offload mechanism. Developers must design and implement this themselves — custom std::thread pools, task queues, inter-thread communication. There is no generic offload solution in the Drogon ecosystem to pull off the shelf.

Mitigation C: Multi-instance + Nginx load balancing
Spread load across instances at the architecture level — if one clogs, others keep serving.

But every option above is opt-in, developer-initiated. By default, a handler runs straight on its ioLoop thread. No manual offload = blocking = every other connection on that ioLoop queueing up = production incident.


Paozhu's advantage (architectural safety net — no developer action required): I/O and business are inherently separated. Handlers run on a dedicated clientrunpool business thread pool. Even if a synchronous handler contains blocking code, it only queues the business pool — I/O threads keep accepting connections, reading requests, and returning overload responses. Coroutine handlers go further: co_await suspends rather than blocks, making thread occupation very rare.


Bottom-line divergence:

Paozhu Drogon
Default behavior Blocking handler → clogs business pool only → I/O layer unaffected Blocking handler → clogs ioLoop → every connection on that thread freezes
Offload Built-in via architecture — no manual work needed DB only (CoroMapper) or self-managed threading
Developer mindset Blocking code won't take the service down Must offload intentionally to be safe
Tradeoff Higher architectural complexity (dual thread pools, strand/coroutine scheduling) Single-thread Reactor — simpler model, easier to reason about

The deepest design-philosophy divergence: OOM-prevention vs DDoS-prevention

The "safe by default vs dangerous by default" distinction above is surface level. Beneath it, each framework's architecture is engineered to defend against a different class of disaster entirely:

Paozhu Drogon
Defends against OOM / service freeze (business-layer runaway) DDoS / linear memory growth (connection-layer runaway)
Core mechanism I/O and business are inherently separated: add_sync_task returns false when the pool can't take more → I/O layer returns fail_503 but keeps accepting connections; deadlock detection + 15s watchdog exit prevents unbounded queue growth I/O thread count is fixed (setThreadNum): 4 threads whether 1k or 100k connections arrive; HttpConnectionLimit plugin caps total connections
The arithmetic Without a reject path, 100k queued requests × 1KB avg = 100MB and climbing → OOM 100k connections × 8MB thread stack each = 800GB — the single-thread Reactor model eliminates this by design
Tradeoff Higher architectural complexity (dual thread pools, strand, coroutine scheduling) Handler runs on ioLoop — blocking = every connection on that thread freezes

Drogon's DDoS resilience is widely acknowledged in the industry — muduo single-thread Reactor + fixed threads + epoll means memory doesn't grow with connection count. Paozhu's OOM resilience is an architectural safety net — the business layer can blow up any which way, the I/O layer keeps breathing and never gets dragged into a freeze.


2. HTTP Protocol Layer

Dimension Paozhu Drogon (this tree)
HTTP/1.1 Custom parser http_parse.cpp (122KB) + request.cpp / httppeer.cpp. No HTTP/1.1 pipeline (request-response serial; however, pipeline is practically dead — Chrome and Firefox never enabled it by default, Firefox 54 removed it entirely, Nginx defaulted to HTTP/1.0 until 1.29.7 which has no pipeline at all; HTTP/2 multiplexing + h2c (plaintext) fully covers this — Paozhu supports h2c so internal proxies and inter-service calls enjoy multiplexing without TLS overhead, head-of-line blocking solved natively) Custom state machine HttpRequestParser.cc (kExpectMethod → ...). Full HTTP/1.1 pipeline (server-side pushResponseToPipelining / popReadyResponses queues responses in receive order, configurable pipeliningRequestsNumber max depth; client-side setPipeliningDepth() for batch sends; only effective on HTTP/1.1 connections, TCP head-of-line blocking remains)
HTTP/2 Built-in: http2_parse.cpp (192KB) + http2_frame/huffman/ring_queue/send_queue, including HPACK, ALPN, h2c, enabled via http2_enable=1 drogon-master's lib/ has no HTTP/2 server implementation (no Http2Server/Http2Connection; HttpServer.cc only outputs HTTP/1.1, no h2/ALPN registration); trantor only has TLS-layer ALPN capability stubs
Request/response model httppeer (httppeer.h:41, enable_shared_from_this) holding urlpath / get / post / output / session Abstract classes HttpRequest/HttpResponse + Impl (HttpRequestImpl/HttpResponseImpl, serialized via renderToBuffer)
Caveat request.h names a built-in JSON micro-object obj_val, not an HTTP request body Passed around via HttpRequestPtr smart pointer; implicit conversion via fromRequest<T>/toRequest<T>

Note: request.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):

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

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):

Drogon (via HttpController.h macros, compile-time):

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).

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

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.

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".

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 Three-layer hook system: per-route pre_sync/co_pre serial chain (@ok/@exit/jump) + per-site action_pre_lists/action_after_lists (serverconfig.h:63-64, run_site_hooks serial, trimmed at site_hook_limit=15) + per-site 404 action_404_lists (httppeer.cpp:1088, triggered when no route matches) Filters (one-way intercept) + Middlewares (before/after route wrapping, registerMiddleware), sortable, reusable, decoupled from routing — no per-site slot isolation
Sync/coroutine mixing First-class in routing layer: reg_raw splits into sync-pre / coro-pre / sync-handler / coro-handler four slots; co_resolve transparently interleaves Sync/async lives on the individual method form (Callback/future/CoroMapper), no slot-based mixed declaration
Simple task orchestration Yes: httppeer::flow_method stack (push_next/push_last/pop_flow), multiple handlers execute in sequence within one request No built-in; manual call / redirect
Multi-tenant routing Multi-domain virtual host built-in at framework level: host_index matches sites by Host header (alias_domain supports alias domains), each site gets independent wwwpath/cert/hooks/rewrite/CORS/upload limits/slot_id (serverconfig.h:30-79) None; business-layer grouping or multi-instance

Key difference: Drogon routing is an industrial-class classic pattern — "macro reflection + static registration + Filters/Middlewares" with clean separation of concerns. Paozhu takes an integrated approach — "annotation + codegen + site slot + flow_method orchestration" with finer-grained per-site hook system (each site gets independent pre/post/404 hook lists), built-in light sequential workflow capability, multi-tenancy right at the routing layer, and PHP-FPM FastCGI support baked in.

3.4 PHP-FPM FastCGI Support (Paozhu exclusive)

Paozhu (built-in at framework level): isuse_php + php_root_document + rewrite_php_lists three config keys, check_php_dispatch() (server.cpp:1369) full implementation: .php suffix hit → check rewrite rules → locate PHP file → set compress=10 marker → forward to php-fpm via FastCGI. Supports URL rewriting (rewrite_php_lists regex maps URLs to PHP files) and index.php fallback (empty URL path checks php_root_document/index.php).

Drogon (absent): Zero PHP/FastCGI files in the entire repository. To run PHP, implement the FastCGI protocol yourself on top of trantor TcpClient, or front with Nginx reverse proxy.

3.5 Multi-domain Virtual Host (Paozhu built-in at framework level)

Each Paozhu site maps to sitehostinfos struct in serverconfig.h (serverconfig.h:30-79). host_index matches sites by HTTP Host header (supports alias_domain for alias domains). Per-site independent configuration fields:

FieldMeaningDrogon has it?
mainhost + alias_domainPrimary domain + alias domains (one site can bind multiple domains)❌ Zero virtual host files in entire repo
wwwpath + php_root_documentStatic file root + PHP root❌
certificate_file + privateKey_filePer-site independent TLS certificates❌ (only one global cert via addListener)
action_pre_lists / action_after_lists / action_404_listsPer-site pre / post / 404 hook lists❌ (Filters/Middlewares are framework-registered, no site isolation)
rewrite_php_listsPer-site PHP rewrite rules❌
cors_domain + cors_origin_allowedPer-site CORS allowlist❌
upload_max_sizePer-site upload size limit❌
themes + themes_urlPer-site theme/template directory❌
slot_idRouting table slot isolation (each site gets its own routing table)❌
http2_enablePer-site HTTP/2 on/off❌
fastcgi_host + fastcgi_portPer-site PHP-FPM backend address❌
siteid + groupidSite/group ID (business layer can use for tenant auth)❌

Matching flow: HTTP request arrives → find_host_index() (httppeer.h:170) looks up Host header in mainhost → miss → check alias_domain → resolve host_index → all routing, hooks, certs, roots, CORS, upload limits switch automatically by host_index. No domain branching needed in business code.


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 <dbtag>, generator vendor/httpcli/modelfun.hpp) produces three layers per table:

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):

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":

Drogon — Streaming Mapper<T> + Criteria (template Mapper in orm/Mapper.h:115, constructor takes DbClientPtr):

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:

Paozhu Drogon (this tree)
Query-result cache Yes (orm_cache.hpp's model_meta_cache<BASE_T>, std::map<size_t,data_cache_t> + mutex) No (zero cache in ORM layer; every query hits the pool)
Access use_cache() / save_cache() / try_get_cache() / remove_cache() —
Reclaim Background daemon periodically scans expired entries —

4.4 Async & Coroutine Model

Paozhu — Sync + coroutine dual form; coroutine is the primary paradigm:

Drogon — Three forms (sync / callback / future) + coroutine overlay:

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:

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<orm::db_conn>("cms");
ulink->begin_commit();                 // BEGIN / BEGIN IMMEDIATE
ulink->edit_query("UPDATE sysuser set ...");
ulink->edit_query("INSERT INTO sysuser ...");
// Queries also go through the same write connection (reads what we just wrote, no master-replica replication lag)
orm::cms::LocalusersqlStruct row;
ulink->edit_query("SELECT * FROM sysuser WHERE ...", row);
if (ulink->iserror) ulink->rollback();
else                ulink->commit();

② ORM layer (lock_conn binds the transaction connection to the model):

auto txnlink = std::make_unique<orm::db_conn>("cms");
txnlink->begin_commit();

// Multiple different model objects all lock onto the same transaction connection — cross-object transaction
orm::cms::Sysuser user_m;
user_m.lock_conn(txnlink->mysql_edit_conn);
user_m.where("adminid", 10000);
user_m.update("nickname");

orm::cms::Sysroles role_m;              // Different table, different model class
role_m.lock_conn(txnlink->mysql_edit_conn);  // Also locks onto the same one
role_m.where("roleid", 50);
role_m.update("remark");

user_m.unlock_conn();
role_m.unlock_conn();
txnlink->commit();                      // Unified commit or rollback

Drogon — First-class Transaction object:

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.

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:

4.8 Connection Pools & Multi-database Isolation

Paozhu: orm_conn_pool.h — each database gets its own independent read-write separated pool (get_mysql_select_conn for reads / get_mysql_edit_conn for writes, all three databases support this). Connection pools are indexed by database name (get_orm_conn_pool_obj() returns std::map<std::string, std::shared_ptr<orm_conn_pool>>, keys are "cms"/"blog"/"lite" etc.). ORM model classes are generated inside per-database namespaces (modelfun.hpp:9467 produces namespace orm { namespace db_tag { class model {...} }}), so tables with the same name in different databases are compile-time isolated — orm::cms::user and orm::blog::user are completely different C++ classes. Multi-database call: auto db = make_unique<orm::db_conn>("cms"); selects the target DB directly. Framework-level table prefix (orm_conn_t::pretable, orm_common.h:718, configured in conf/orm.conf): under [docs] section write pretable = web_, allowing multiple business ORM models to share one physical database — namespace + prefix double isolation.

Drogon: Multi-database via multiple db sections in config + app().getDbClient("cms") to fetch; ORM models also support namespace isolation, but you must specify it manually — drogon_ctl create model --namespace cms command-line flag, or "namespace": "cms" in the JSON config; default value is the database name. No read-write separation: a single DbClient's connections_ pool is shared for reads and writes. No table prefix: zero related implementations in the entire repository; to host multiple business ORMs in one physical DB you must either create separate DBs or hand-edit generated model source.

Paozhu exclusive:

Drogon: DbClientLockFree (orm_lib/src/DbClientLockFree.cc) with three-state collections connections_/readyConnections_/busyConnections_ (mutex-protected). execSql reuses an idle one if available, otherwise pushes into sqlCmdBuffer_ (max 20000; exceeding it reports "Too many queries in buffer", DbClientLockFree.cc:203). Number of event loops decided by connNum (SQLite fixed at 1, PG/MySQL take min(hardware_concurrency(), connNum)). Per-library connection classes PgConnection/MysqlConnection/Sqlite3Connection only handle single-connection non-blocking I/O; supports execSqlWithTimeout which throws TimeoutError on timeout. No framework-level connection health check — zombie cleanup relies on the DB server's wait_timeout setting.

Underlying protocol implementation divergence (architecture-level):

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 << "<br/>title:" << art.getTitle();
if (art.is_hit_cache()) { /* cache hit branch */ }

// Pagination: directly returns (nav_min, nav_max, cur_page, total_page)
unsigned int page = client.get["page"].to_int();
auto [nav_min, nav_max, cur_page, total_page] = art.page(page, 10, 5); // page, per_page, nav width

Style takeaway: get a model instance → chain .where()/.whereEQ()/.use_cache()/.fetch_one()/.page() on it → co_await or sync fetch_one(). No separate Mapper layer — the model IS the DAO.

Drogon — Generic Mapper<T> + Criteria expression; CRUD by model type (example orm_lib/tests/db_test.cc):

using namespace drogon_model::postgres;
Mapper<Users> mapper(clientPtr);          // clientPtr = app().getDbClient()
Users user;
user.setUserId("pg"); user.setUserName("alice");
mapper.insert(user);                      // Sync write
auto u = mapper.findByPrimaryKey(2);      // PK lookup

// Coroutine form
CoroMapper<Users> cmapper(clientPtr);
auto cu  = co_await cmapper.findByPrimaryKey(2);
auto us  = co_await cmapper.orderBy(Users::Cols::_user_id).limit(2).findAll();
auto one = co_await cmapper.findOne(
    Criteria(Users::Cols::_id, CompareOperator::EQ, 1));

Style takeaway: Mapper<Model> is a generic CRUD facade independent of the model object. Conditions use Criteria(col, op, val) expression construction. The model object only carries data + setter/getter; all persistence goes through Mapper.

One-liner: Paozhu is "the model queries itself" (builder attached to the model, verb-style, quick to pick up). Drogon is "Mapper proxies model CRUD" (generic facade + Criteria expressions, type-safe, cleaner separation of concerns).

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/<dbtag>/tables/*.sql (CI-friendly, decoupled from runtime DB) Live DB only — reads information_schema/PRAGMA (create_model, database-first)
Artifact organization base (opsql separated) + external FK .cpp Single model class + private/friend encapsulation + dirtyFlag
Query style Chained verb builder (where/page/fetch) Mapper<T> + Criteria streaming expressions
Query-result cache Built-in orm_cache.hpp / model_meta_cache None (cache delegated to upper layer / Redis)
Async paradigm Sync + coroutine dual form: sync count()/fetch()/page() + coro async_count()/async_fetch()/async_page() (asio::awaitable) Sync / callback / future three forms + CoroMapper coroutine overlay (four forms)
Transactions Two-layer API: orm::db_conn native SQL transaction + model lock_conn(txn_conn) to bind to transaction connection (manual unbind + returned by db_conn lifecycle) First-class Transaction object threaded into Mapper; model is unaware, API cleaner
Relationships / FK oneXXX/manyXXX in external .cpp Generated-time Relationship + model getXxx(client) method
Connection pool dbtag read-write separated coroutine pool + framework-level health check (orm_connect_mar periodic zombie scan + hard-close on timeout) + ORM query logging DbClientLockFree three-state pool + buffer cap 20000; no framework-level connection health check (zombie cleanup relies on DB server wait_timeout)
Type mapping SQL → MySQL binary code → C++ Per-DB hardcoded mapping + SqlBinder binary binding
Wire protocol / client dependency Self-implemented MySQL/PG wire protocols (mysql_conn.cpp/pg_conn.cpp hand-written handshake and auth); SQLite uses official sqlite3 C API (sqlite3_open/sqlite3_prepare_v2), does not link libmysqlclient/libpq, links SQLite3 Wraps all official client libraries (MysqlConnection→libmariadb, PgConnection→libpq, SQLite→sqlite3), wire protocols delegated to third parties

Key difference takeaways:


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):

Hodor plugin (lib/inc/drogon/plugins/Hodor.h, official Drogon rate-limiting plugin):

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):

5.2 Paozhu — Connection-Admission Overload Protection + Upload Limits + IP Blocking

Connection admission throttling (built into httpserver core, server.h:247 ratelimiter() thread-driven):

Upload limits: httppeer::check_upload_limit() (httppeer.cpp:789, guarded by site-level host_index) — limits upload body size per site.

IP blocking: check_blockip(peer_session) (server.cpp:5757, TLS handshake failed connections excluded from traffic counts), blacklist/whitelist access-layer interception.

Paozhu has no built-in "per-IP / per-user / per-route request quota" layer, but can be implemented with framework capabilities: add an IP sliding-window counter (maintaining std::unordered_map<ip, deque<timestamp>>) 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

6.2 Redis

6.3 MQTT

6.4 RPC

6.5 Raw Socket / TCP Server

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<MessageType> template class (lib/inc/drogon/PubSubService.h) — it provides named topics + subscribe/publish/unsubscribe, uses shared_mutex + copy-on-write, and is an in-process tool for business code, not a protocol dispatch mechanism inside the framework.

6.7 JWT & OAuth 2.0 / OIDC

6.8 Session Management

6.9 HTTP Client

6.10 Monitoring & Observability

6.11 General-purpose In-memory Cache Layer

Both frameworks ship framework-level general-purpose in-memory caches (not ORM-specific), but the implementation styles are dramatically different — directly reflecting their respective threading models:

Dimension Paozhu pzcache<BASE_TYPE> Drogon CacheMap<T1, T2>
Location vendor/httpserver/include/pzcache.h lib/inc/drogon/CacheMap.h
Underlying storage static std::map<string, data_cache_t> + one global mutex Time wheel (4 wheels × 200 buckets), tickInterval * 200^4 seconds max validity
Expiry cleanup Background thread scans for exptime < nowtime Time wheel fires fnOnErase callback automatically on expiry — no polling needed
Instance model Global singleton pzcache<T>::conn() (intentionally not destructed — cleanup thread safety) One independent instance per EventLoop (IOThreadStorage pattern)
Thread safety Global std::mutex (I/O threads + business pool threads cross-pool, both access it) Per-EventLoop instance → zero lock (handler always runs on that ioLoop)
API save(hashid, data, expnum, cover_data) / get(hashid) / check(hashid) / remove(hashid) / update(hashid, exptime) / clear() / remove_exptime() Internal insert / erase, notifies business via fnOnInsert / fnOnErase callbacks
Demo / usage controller/src/testpzcache.cpp full CRUD example; ORM query result cache orm_cache.hpp follows the same pattern Framework-internal: HttpConnectionLimit limiter states, static file cache, rate limiter states

Threading model is everything: Drogon handlers are pinned to ioLoops → one CacheMap per EventLoop → zero lock. Paozhu's I/O and business threads live in separate pools → both need access → global mutex. This is the same architectural tradeoff as IOThreadStorage vs global pzcache.

6.12 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<void()> — any callback
Business-layer plug-in cost Extremely low: add a scheduled task = write one sync controller function + one arm route. Remove scheduled task = delete controller file, task table auto-clears. All sync functions registered via //@urlpath can be called by timeloop (name match only) Higher: fetch EventLoop → write lambda → manage TimerId → lifecycle bound to EventLoop. No controller annotation auto-registration
Running thread clientrunpool thread pool (sync-only, explained in §3.3) EventLoop's owning I/O thread (single-thread serial, no cross-thread return issue)
Time precision Depends on server main loop frame rate (~ms typical) Sub-millisecond (timerfd)
Periodic scheduling ✅ Trigger every N ticks ✅ runEvery(interval) every interval seconds
One-shot task ✅ Implemented via counting: timecount_num starts at 1, handler calls peer->add_timeloop_count() to accumulate. After N calls, add_timeloop_count(0) clears → next tick server finds timecount_num==0 and auto-removes the task ✅ runAfter(delay) run once with auto-removal (framework-managed)
Specified time point ❌ No ✅ runAt(Date) trigger at a specific absolute time
Cancel clear_timeloop_task() sets timeloop_num=0 cancel(TimerId)
Single-flight protection ✅ timeloop_inflight atomic CAS: if still running from last tick, skip current tick — no backlog accumulation Callbacks are responsible for their own idempotency; TimerQueue itself does NOT enforce single-flight
Cron expressions ❌ No ❌ No
Cross-EventLoop Globally unified scheduling (main loop's single fps counter drives all tasks) Each EventLoop has its own independent TimerQueue; cross-thread coordination must be done manually

Paozhu timeloop workflow (using frametasks_timeloop as example):

1. Business code calls httppeer.add_timeloop_task("frametasks_timeloop", 100)
   → peer.linktype = 7, timeloop_num = 100
2. server.cpp main loop: fps++ every frame
3. When fps % 100 == 0 AND timeloop_inflight == false:
   → CAS sets timeloop_inflight
   → Dispatches to clientrunpool thread pool (linktype==7 → timetasks_run)
4. Inside thread pool (threadpool.cpp:603):
   → router.run_timeloop_task(peer, "frametasks_timeloop")
   → call_sync_regfun finds the controller function with the same name (registered at serverwatch.cpp:67)
   → After execution, inflight_release() resets timeloop_inflight

Drogon EventLoop Timer usage:

auto loop = drogon::app().getMainEventLoop();
// Run every 5 seconds
auto tid = loop->runEvery(5.0, [] { doSomething(); });
// Cancel
loop->cancel(tid);

Summary: Neither supports cron expressions; both are interval-based timers. But business-layer plug-in cost is drastically different. Paozhu binds scheduled tasks to route handlers + thread-pool execution, fully integrated with the framework routing system — write a sync controller function annotated with //@urlpath(null, taskname), plus an arm route (URL trigger or startup-time registration), and the task is into the scheduler; deleting the controller file auto-clears the task table. Zero boilerplate, zero framework-API learning cost, routes and tasks share one registration. But it only accepts sync annotation functions (fn_kind::sync); coroutine handlers are skipped (timeloop runs in a sync thread pool, no strand to co_spawn on). Drogon is a low-level EventLoop primitive (higher precision, richer API — runAt/runAfter), but interval-task→business-handler mapping, EventLoop ownership, TimerId lifecycle all must be handled by business code itself, with no controller annotation auto-registration.

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 HTTP/1.1 pipeline client, but pipeline is practically dead — Chrome/Firefox never enabled by default, HTTP/2 multiplexing + h2c covers both internet and intranet) Built-in (sync/callback/future/coroutine four forms + HTTP/1.1 pipeline client setPipeliningDepth, HTTP/1.1 only — practically no browser or reverse proxy enables pipeline by default)
Monitoring / Prometheus Basic built-in: serverwatch.cpp status route + Access log + interval task registration; no Collector/Registry abstraction; Prometheus metrics require business-layer instrumentation Built-in complete system (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable)
Scheduled tasks Frame-driven: add_timeloop_task(taskname, interval_ticks) → route handler + clientrunpool thread pool. Globally unified scheduling, built-in single-flight protection. But precision limited by main loop frame rate, only "every N ticks" EventLoop-level: runAt/runAfter/runEvery + timerfd. Precision sub-millisecond, supports absolute time point and one-shot. But each EventLoop has its own independent TimerQueue, no global scheduling
Cross-protocol unified abstraction Yes: ws/mqtt/socket/redis share subpub registration model None in framework layer (each capability independent). Business layer has PubSubService<> template tool
RequestStream streaming request body Partial: multipart upload auto-persists at 3MB threshold (memory protection) + HTTP client save() streaming file writes; but no handler-per-chunk-callback streaming API Complete: RequestStreamReader + MultipartStreamParser, handler gets per-chunk callbacks
Reverse proxy None (self-implement via HTTP client) Yes (framework-level forward() + plugin example SimpleReverseProxy)
PostgreSQL LISTEN/NOTIFY None (sync/coroutine queries only) Yes (DbListener, event-driven, no polling)
Resident outbound clients Yes: four types — Redis subscription / WS / raw TCP / MQTT — all start on framework boot, lifecycle managed by client_context, self-check route resident_stop None (DbListener is DB-layer only; HttpClient is on-demand)
Plugin system No unified mechanism (AccessLog / static-file redirect embedded in server.cpp) Complete AOP Advice (registerSyncAdvice/registerPreHandlingCallback/registerPostHandlingCallback + dependency topological sort)

Core difference: Paozhu is "multi-protocol + built-in identity stack + resident outbound" — one framework is simultaneously HTTP/2, WebSocket, MQTT(broker), Redis, RPC, raw Socket server, with built-in JWT + OAuth 2.0/OIDC authorization server, plus Session, HTTP client, and four resident outbound clients (Redis subscription / WS / TCP / MQTT). But monitoring needs business-layer integration, no HTTP pipeline, no handler-per-chunk-callback streaming request body (only parser-layer auto-persist memory protection), no reverse proxy, no LISTEN/NOTIFY. Drogon is a "production-grade Web framework focused on HTTP" — WebSocket and Redis are first-class citizens, HTTP client is stronger (with pipeline), Session/monitoring/Prometheus/RequestStream/reverse proxy/DbListener are all built-in, with a unified plugin system. But JWT/OAuth/MQTT/RPC/TCP/resident outbound are delegated to external libraries or self-implementation — the framework itself doesn't build them in.

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<vector<HttpClientPtr>> as a connection pool, isolated per EventLoop for lock-freedom.

DbListener — PostgreSQL LISTEN/NOTIFY (DbListener.h): based on PostgreSQL native LISTEN/NOTIFY, event-driven, no polling. DbListener::newPgListener(connectionInfo) → listen(chan, cb) / unlisten(chan). Complete db_listener_test.cc.

Plugin system (AOP Advice): unified framework plugin mechanism, three hooks registerSyncAdvice / registerPreHandlingCallback / registerPostHandlingCallback. Redirector, SlashRemover, AccessLogger, GlobalFilters, PromExporter, Hodor all go through this system; plugins can declare dependencies, startup order auto-topologically sorted.

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:

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/<name>.h (forward declarations) + common/autocontrolmethod.hpp (reg_raw route table) create_controller skeleton; create_view compiles .csp → DrTemplate subclass
Template engine view/ HTML compiled into viewsrc/ C++ source (viewsrc/view/) CSP (C++ Server Pages): .csp compiles into DrTemplateBase subclass genText()
View mechanism view() / theme_view() (multi-theme prefix) DrTemplate<ClassName> + newTemplate()

Key differences:

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<httppeer> peer)
{
    httppeer &client = peer->get_peer();
    client << "<p>Random number: " << rand_range(100, 999) << "</p>";
    return "";   // empty string = normal completion / pass; non-empty = jump or terminate
}
}//namespace http

Drogon — Registration macro (HttpController + reflection, class name auto-derived to path):

class TestCtrl : public drogon::HttpController<TestCtrl>
{
  public:
    METHOD_LIST_BEGIN
    // Path "/testrand", GET method, binds testrand method
    ADD_METHOD_TO(TestCtrl::testrand, "/testrand", Get);
    METHOD_LIST_END

    void testrand(const HttpRequestPtr &req,
                  std::function<void(const HttpResponsePtr &)> &&cb) const
    {
        auto resp = HttpResponse::newHttpResponse();
        resp->setBody("<p>Random number</p>");
        cb(resp);   // callback-style return
    }
};

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:

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<T> + co_await + co_spawn(..., detached) throughout server/router/httppeer/ORM/clients Complete C++20 coroutines: Task<T> (coroutine.h:160), AsyncTask, CallbackAwaiter, when_all, Mutex coroutine lock
Trigger points Per-connection http1loop/http2loop coroutines; router co_resolve; DB queries ORM CoroMapper; co_future/sync_wait bridging futures
Positioning Coroutines are the primary paradigm (even ORM); callbacks are rare Coroutines are an optional paradigm; default is callbacks/future, coroutines are enhancement

Key difference: Paozhu makes coroutines the only first-class citizen (IO, business, DB all end-to-end awaitable). Drogon is a hybrid model: "callbacks/future as base + coroutine overlay". Paozhu uses Asio's standard co_await; Drogon has self-developed Task/Awaiter system.


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)

Drogon comparison (thin core + ecosystem assembly)

Costs and benefits come from the same source: Paozhu's "self-built big family" brings simpler deployment (fewer external libs to install), capability consistency, zero protocol authorization/version entanglement — but enormous code size, self-implemented correctness risk, and a small ecosystem/community (see §11). Drogon takes "HTTP depth + easy assembly" as its engineering mainline.


11. Design Tradeoffs Summary

Paozhu strengths

Paozhu costs

Drogon strengths

Drogon costs (measured in drogon-master)


12. Selection Guide

Concept clarification: Multi-domain virtual host = one physical server bound to multiple domains, each domain runs an entirely independent website (domain-level isolation: separate certs, roots, hooks); SaaS multi-tenancy = one or more domains serving multiple tenants, tenants share the same codebase (usually data-isolated by tenant_id field). These are different scenarios.

Scenario Recommendation
Multi-domain virtual host / hosting platform (one server bound to multiple domains, each site with independent certs/roots/hooks/CORS/upload limits) Paozhu (built-in at framework level: host_index auto-matches sitehostinfos by Host header, each site gets 12+ independent config fields; Drogon has zero virtual host code — can only route at the Nginx proxy layer + multi-instance)
SaaS multi-tenancy (one or more domains serving multiple tenants, need tenant isolation) Paozhu (framework-level virtual host + siteid/groupid fields assist tenant auth. When each tenant gets its own subdomain/domain, certs/roots/hooks/CORS/upload limits auto-isolate — business code only needs to switch tenant context by host_index. Drogon has no site isolation fields; tenant isolation is entirely business-layer)
Need HTTP/2 server support Paozhu (drogon-master ships no HTTP/2 server; Paozhu has it built-in)
Prioritize mature ecosystem, docs, community, quick onboarding Drogon
Full-stack coroutine async, unified mental model Paozhu (coroutine as primary paradigm)
Themed sites / multiple template sets (same domain, different theme directories/template rendering) Paozhu (themes + themes_url per-site fields, framework built-in; Drogon has no site-isolated theming mechanism)
Offline CI / ORM generation without runtime DB Paozhu (SQL schema generation)
Classic Reactor + reflection macros + compile-time registration Drogon
One-stop zero-dep big family (multi-protocol + self-managed certs + self-built DB protocol, install fewer external libs) Paozhu
Built-in auth/SSO needed (JWT + OAuth 2.0/OIDC authorization server, avoid external IdP) Paozhu
Need HTTP/1.1 pipeline (practically dead: Chrome/Firefox never enabled by default, Firefox 54 removed it, Nginx defaulted to HTTP/1.0 until 1.29.7; Paozhu has no HTTP/1.1 pipeline, but HTTP/2 multiplexing + h2c (plaintext) covers concurrent scenarios both inside and outside the network — equivalent and more thorough) Drogon (HttpServer.cc pipeline queue + HttpClient setPipeliningDepth())
Need built-in Prometheus monitoring Drogon (Collector/Counter/Histogram + PromExporter plugin, JSON config to enable)
Need HTTP/1.1 pipeline client (practically dead — HTTP/2 multiplexing already covers concurrent requests; intranet can use h2c) Drogon (HttpClientImpl.h:62 exclusive capability)
Connection-level protection preference "hard reject" vs "queued slow admission" Drogon (HttpConnectionLimit hard-reject type) vs Paozhu (queued admission type, gentler)
Need RequestStream streaming request body / low-memory large-file upload Drogon (RequestStreamReader + MultipartStreamParser per-chunk callback)
Need PostgreSQL LISTEN/NOTIFY event-driven Drogon (DbListener, event-driven, no polling)
Need framework-level reverse proxy Drogon (forward() + plugin example SimpleReverseProxy)
Internal proxy / inter-service calls (plaintext HTTP/2 + multiplexing, avoid TLS negotiation overhead) Paozhu (h2c plaintext HTTP/2 with multiplexing — zero TLS overhead, multiple requests interleaved on a single connection; Drogon has no HTTP/2 server, so reverse proxies and inter-service traffic can only use HTTP/1.1 with no multiplexing)
Need unified plugin system (AOP Advice + dependency topological sort) Drogon (registerSyncAdvice/registerPreHandlingCallback/registerPostHandlingCallback)
Static-file compression cache preference Paozhu (lazy auto-compress to disk + mtime-aware; source file updates auto-recompress, zero ops cost) vs Drogon (pre-compressed .gz/.br files created at deploy/build time; framework only reads, never writes, no source-file mtime awareness — when source files are updated, ops must also update .gz/.br; memory cache (default 5s) won't refresh before expiry either)
E-commerce development (payment module) Paozhu: built-in vendor/webpay/ (Alipay + WeChat Pay + Mini Program Pay, with signing/verification/callback handling, ENABLE_WEBPAY switch) + libs/oauth/ (own OAuth 2.0 authorization server) + full-coroutine async handling of payment callback high concurrency. Drogon side needs to self-integrate third-party SDKs, no OAuth server built-in
Enterprise development (WebPDF / office document processing) Paozhu: built-in vendor/webpdf/ (HTML→PDF) + vendor/pzexcel//pzword/ (Excel/Word read/write) + pzimage/ (image processing) + pzcharts/ (charts) — zero external deps, CI/CD one-line compile. Drogon side needs separate libharu/libxlsxwriter/opencv etc., complex build chain
IoT development (MQTT module) Paozhu: built-in MQTT broker + client (MQTT5, retained messages, shared subscriptions, subpub_reg unified dispatch) — ready to use, no need to deploy extra EMQX/Mosquitto. Drogon has zero MQTT code across the whole repo, needs third-party plugin or external broker
Need HTTP/2 + large-module one-stop Paozhu (HTTP/2 + MQTT broker + JWT + OAuth + ACME/OCSP + webpay/webpdf/pzexcel/pzword/pzredis, all built-in with zero deps)
Certificate automation, zero ops (ACME auto-issue/renew + OCSP stapling auto-refresh) Paozhu: framework-built acme_client.cpp/acme_run.cpp/ocsp_check.cpp, configure acme.conf/ocsp_intv_time and it's fully automatic (daily scheduled OCSP renewal). Drogon has zero built-in: ACME requires certbot/lego/openssl scripts + cron + manual nginx stapling; OCSP stapling is zero code across the whole repo
Need Asio third-party library ecosystem compatibility Paozhu (directly uses standalone Asio, asio::io_context exposed at the bottom. Any Asio library — asio-grpc / beast / async_mqtt / boost::redis / quiche / nghttp2_asio etc. — can be dropped in directly, co_spawn/co_await ready to use). Drogon unavailable (EventLoop is trantor's self-built muduo branch, epoll_wait + timerfd + min-heap, not asio::io_context, Asio ecosystem libraries cannot directly connect)
Framework-level self-protection (deadlock detection + active exit restart closed loop) Paozhu (accept independent thread + handler-layer naturally isolated detection mechanism, covers all deadlock scenarios, actively exits within 15 seconds; faster than systemd WatchdogSec=30s)

Appendix: Key Source Evidence Index

Back to main docs →