Drogon: An Introduction and Practice of the High-Performance C++ Web Framework

Drogon: An Introduction and Practice of the High-Performance C++ Web Framework

Drogon is a high-performance HTTP web application framework written in C++14/17/20, designed to help developers easily build various types of web application server programs. It adopts an asynchronous non-blocking architecture, utilizing efficient event handling mechanisms such as epoll (Linux) and kqueue (macOS/FreeBSD), maintaining excellent performance even under a large number of concurrent requests. Drogon is cross-platform, supporting various operating systems including Linux, macOS, FreeBSD, OpenBSD, and Windows.

Core Features of the Drogon Framework

Drogon boasts a range of powerful features that make it stand out in modern web development:

  • Fully Asynchronous Programming Model: Based on non-blocking I/O and callback mechanisms, effectively handling high-concurrency requests
  • Flexible Routing System: Supports RESTful style routing design, allowing easy definition of various HTTP endpoints
  • Support for Multiple Protocols: Fully supports HTTP/1.0/1.1, WebSocket, HTTPS, and file uploads/downloads
  • Database Integration: Provides asynchronous read/write capabilities for PostgreSQL, MySQL, and SQLite3, with a built-in lightweight ORM implementation
  • View Rendering: Supports backend rendering, generating dynamic HTML pages through CSP (similar to JSP) templates
  • JSON Handling: Built-in JSON serialization and deserialization, ideal for developing RESTful APIs
  • Filter Mechanism: Provides AOP support, allowing easy implementation of unified logic such as login validation and logging
  • Plugin System: Supports installation and configuration of plugins via configuration files

Installation and Environment Configuration

The steps to install Drogon on Ubuntu are as follows:

# Install dependencies
sudo apt-get install git cmake make g++ libjsoncpp-dev uuid-dev libsqlite3-dev zlib1g-dev libcurl4-openssl-dev libssl-dev

# Install Drogon from source
git clone https://github.com/drogonframework/drogon.git
cd drogon
git submodule update --init
mkdir build && cd build
cmake ..
make
sudo make install

# Verify installation
drogon_ctl version

On other operating systems, you can also install via package managers; for example, on Debian, you can use apt-get install libdrogon-dev.

Creating a Simple Web Service

Below is a simple Drogon application example that creates an HTTP service returning a JSON response:

#include <drogon/drogon.h>

using namespace drogon;

int main() {
    // Set HTTP route
    app().registerHandler(
        "/hello",
        [](const HttpRequestPtr& req,
           std::function<void(const HttpResponsePtr)>& callback) {
            // Create JSON response
            Json::Value json;
            json["message"] = "Hello, Drogon!";
            json["status"] = "success";

            auto resp = HttpResponse::newHttpJsonResponse(json);
            callback(resp);
        },
        {Get});

    // Configure and start server
    app().setLogPath("./")
          .setLogLevel(trantor::Logger::kWarn)
          .addListener("0.0.0.0", 8080)
          .setThreadNum(16)
          .run();

    return 0;
}

This simple example demonstrates the basic usage of Drogon:

  • Register a handler function to the path /hello
  • Return a JSON response when a GET request is received
  • The server listens on port 8080, using 16 processing threads

Controllers: Structuring Request Handling

For more complex applications, Drogon provides the concept of controllers to better organize code. You can quickly create a controller using the drogon_ctl tool:

drogon_ctl create controller TestCtrl

This will generate header and implementation files:

TestCtrl.h:

#pragma once
#include <drogon/HttpSimpleController.h>

using namespace drogon;

class TestCtrl : public drogon::HttpSimpleController<TestCtrl>
{
public:
    virtual void asyncHandleHttpRequest(const HttpRequestPtr &req,
                                        std::function<void (const HttpResponsePtr &)> &callback) override;
    PATH_LIST_BEGIN
    // Path definitions
    PATH_ADD("/test", Get);
    PATH_ADD("/test/{1}", Get);
    PATH_LIST_END
};

TestCtrl.cc:

#include "TestCtrl.h"

void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
                                      std::function<void (const HttpResponsePtr &)> &callback)
{
    // Write application logic
    auto resp = HttpResponse::newHttpResponse();
    resp->setStatusCode(k200OK);
    resp->setContentTypeCode(CT_TEXT_HTML);
    resp->setBody("Hello from TestCtrl!");
    callback(resp);
}

Filters: Implementing AOP Aspect-Oriented Programming

Drogon’s filter functionality allows you to execute common logic before requests reach the handler, such as authentication:

class LoginFilter : public drogon::HttpFilter<LoginFilter>
{
public:
    virtual void doFilter(const HttpRequestPtr &req,
                          FilterCallback &fcb,
                          FilterChainCallback &fccb) override
    {
        // Check if the user is logged in
        auto session = req->getSession();
        if (session->find("user")) {
            // User is logged in, continue processing
            fccb();
        } else {
            // User is not logged in, return error
            Json::Value json;
            json["error"] = "Unauthorized";
            json["message"] = "Please login first";

            auto resp = HttpResponse::newHttpJsonResponse(json);
            resp->setStatusCode(k401Unauthorized);
            fcb(resp);
        }
    }
};

Then register the filter in the controller:

PATH_LIST_BEGIN
PATH_ADD("/profile", Get, "LoginFilter");
PATH_LIST_END

Database Integration and ORM

Drogon has built-in ORM support, making it easy to interact with databases. Here is an example using ORM:

#include <drogon/orm/DbClient.h>
#include <drogon/orm/Mapper.h>

// User model class
class User : public drogon::orm::BaseModel<User>
{
public:
    struct PrimaryKey {
        int id;
        PrimaryKey(int value = 0) : id(value) {}
    };

    int id;
    std::string name;
    std::string email;

    // Mapping relationship
    struct relation {
        static const char table_name[];
        static const char primary_key[];
        static const bool auto_increment = true;
    };

    // Field definitions
    static constexpr const char* column_list = "id, name, email";
};

// Using in handler
app().registerHandler("/users/{id}",
    [](const HttpRequestPtr& req,
       std::function<void(const HttpResponsePtr&)>& callback,
       int user_id) {

        auto client = drogon::app().getDbClient();

        // Asynchronous query for user
        client->execSqlAsync(
            "SELECT * FROM users WHERE id = $1",
            [callback](const drogon::orm::Result& result) {
                if (result.empty()) {
                    auto resp = HttpResponse::newHttpResponse();
                    resp->setStatusCode(k404NotFound);
                    callback(resp);
                    return;
                }

                Json::Value json;
                json["id"] = result[0]["id"].as<int>();
                json["name"] = result[0]["name"].as<std::string>();
                json["email"] = result[0]["email"].as<std::string>();

                auto resp = HttpResponse::newHttpJsonResponse(json);
                callback(resp);
            },
            [callback](const drogon::orm::DrogonDbException& e) {
                // Handle database error
                Json::Value json;
                json["error"] = "Database error";
                json["message"] = e.base().what();

                auto resp = HttpResponse::newHttpJsonResponse(json);
                resp->setStatusCode(k500InternalServerError);
                callback(resp);
            },
            user_id
        );
    },
    {Get});

Building a RESTful API

Combining the above features, we can build a complete RESTful API:

#include <drogon/drogon.h>

using namespace drogon;

class UserAPI : public HttpSimpleController<UserAPI>
{
public:
    void asyncHandleHttpRequest(const HttpRequestPtr& req,
                               std::function<void(const HttpResponsePtr&)>& callback) override
    {
        auto method = req->getMethod();
        Json::Value response;

        if (method == Get) {
            // Get user list
            response["users"] = Json::arrayValue;
            response["count"] = 0;
        } else if (method == Post) {
            // Create new user
            auto json = req->getJsonObject();
            if (json) {
                response["status"] = "user created";
                response["id"] = 1001;
            } else {
                response["error"] = "invalid input";
            }
        } else if (method == Put) {
            // Update user
            response["status"] = "user updated";
        } else if (method == Delete) {
            // Delete user
            response["status"] = "user deleted";
        }

        auto resp = HttpResponse::newHttpJsonResponse(response);
        callback(resp);
    }

    PATH_LIST_BEGIN
    PATH_ADD("/api/users", Get, Post, Put, Delete);
    PATH_LIST_END
};

Configuration File

Drogon supports configuring applications via JSON files:

config.json:

{
  "app": {
    "log_path": "./",
    "log_level": "WARN"
  },
  "listeners": [
    {
      "address": "0.0.0.0",
      "port": 8080,
      "https": false
    }
  ],
  "db_clients": [
    {
      "name": "default",
      "type": "postgresql",
      "host": "localhost",
      "port": 5432,
      "dbname": "mydatabase",
      "user": "myuser",
      "passwd": "mypassword"
    }
  ],
  "thread_num": 16
}

Load the configuration in code:

app().loadConfigFile("../config.json");
app().run();

Conclusion

The Drogon framework provides C++ developers with a powerful tool for building high-performance web services through its asynchronous non-blocking architecture and rich feature set. Whether for simple API endpoints or complex enterprise-level applications, Drogon can deliver excellent performance and development experience.

Leave a Comment