[C++/Drogon Framework] 2. Controller, filter and view

[C++/Drogon Framework] 2. Controller, filter and view

Article directory

  • [C++/Drogon Framework] 2. Controller, filter and view
  • 1. Controller
    • 1.HttpSimpleController
    • 2.HttpController
    • 3. WebSocketController
  • 2. Filter
    • 1. Built-in filter
    • 2. Custom filters
  • 3. View
    • 1.CSP
    • 2. Use of views

1. Controller

The controller is an important part of Web development. It can receive requests from the browser and generate corresponding data to respond. It is generally used to provide an interface for the front end, or to directly provide functional interfaces to users in combination with views. In Drogon, we don’t need to care about network transmission, http parsing, etc. All we have to do is implement logical functions.

In the controller, we can define multiple functions (generally called handlers) to handle multiple logical functions in a module, such as user login and registration.

There are three types of Drogon controllers: HttpSimpleController, HttpController, WebSocketController. When using them, just inherit the corresponding class template.

1. HttpSimpleController

We can use the drogon_ctl tool to quickly generate a controller named testController:

drogon_ctl create controller TestController
#Note: If you created the project using drogon_ctl, this command must be executed in the controller directory.
  • Each HttpSimpleController can only have one handler, and it is defined through virtual functions

  • Drogon provides us with a macro PATH_ADD for path mapping, which should be included between PATH_LIST_BEGIN and PATH_LIST_END. The first part of this macro One parameter is the path you want to map, and it also provides two constraints: one is the type of HttpMethod, which is used to specify the request method, and the other is HttpFilter , used to filter requests, see for details

Here’s an example:

TestController.h

#pragma once

#include <drogon/HttpSimpleController.h>

using namespace drogon;

class TestController : public drogon::HttpSimpleController<testController>
{<!-- -->
  public:
    // There can only be one handler and it is defined with a virtual function
    void asyncHandleHttpRequest(const HttpRequestPtr & amp; req, std::function<void (const HttpResponsePtr & amp;)> & amp; & amp;callback) override;
    PATH_LIST_BEGIN
        // list path definitions here;
        // PATH_ADD("/path", "filter1", "filter2", HttpMethod1, HttpMethod2...);
    //Map the POST request for the root path
    PATH_ADD("/", Post);
    //Map the Get request for the /test path
    PATH_ADD("/test", Get);
    PATH_LIST_END
};

TestController.cc

#include "TestController.h"

void TestController::asyncHandleHttpRequest(const HttpRequestPtr & amp; req, std::function<void (const HttpResponsePtr & amp;)> & amp; & amp;callback)
{<!-- -->
    // write your application logic here
    //Create Http response
    auto resp = HttpResponse::newHttpResponse();
    //Set status code to 200
    resp->setStatusCode(k200OK);
    //Set contentType
    resp->setContentTypeCode(CT_TEXT_HTML);
    //Set the response body
    if (req->getMethod() == Get) {<!-- -->
        resp->setBody("Get!");
    }
    else if (req->getMethod() == Post) {<!-- -->
        resp->setBody("Post!");
    }
    // Call the callback function
    callback(resp);
}

The result is as follows:


2. HttpController

HttpController can also be quickly generated through drogon_ctl, just add the -h parameter

drogon_ctl create controller -h HttpTestController
  • In HttpController, drogon provides us with two macros for adding mapping: METHOD_ADD and ADD_METHOD_TO, where,

    • The mapping added by METHOD_ADD will automatically add a prefix (namespace + class name). For example, the class HttpTestController under the namespace demo is added using this macro. The mapping will automatically add the prefix of /demo/HttpTestController/;

    • The ADD_METHOD_TO macro does not add a prefix.

    The parameter requirements of both are the same. The first is the handler you want to map to, the second is the path to add mapping (regular expressions are supported), and the constraints are the same as those of HttpSimpleController, here I won’t go into details.

  • In addition to paths, we can also map parameters:

    • The first is the path parameter and the request parameter after the question mark. There are four ways to map these two parameters:

      • {}: This type only has braces, which will map the corresponding parameters to the corresponding parameter positions.
      • {1}, {2}: If there are numbers in the curly brackets, the corresponding parameters will be mapped to the parameter positions specified by the corresponding numbers.
      • {name}: The string in the middle has no practical effect, but it can improve the readability of the program.
      • {1:name1}, {2:name2}: The string after the colon also has no practical effect, but it can improve the readability of the program.

      For example: "/{1:username}/test?token={2:token}"

      It should be noted that the target parameter type can only be a basic type, std:string type or any type that can be assigned using the stringstream >> operator

    • In addition, drogon also provides a parameter mapping mechanism from HttpRequestPtr objects to any type. When the parameters required by the handler are greater than the parameter mapping in the path, the remaining parameters will be converted from HttpRequestPtr objects. Users can define any type of conversion. The way to define this conversion is to specialize the fromRequest template of the drogon namespace (defined in the HttpRequest.h header file). For example, we define a user structure:

      User.h

      #pragma once
      #include <iostream>
      #include <drogon/HttpRequest.h>
      
      struct User {<!-- -->
          std::string userId;
          std::string account;
          std::string passwd;
      };
      
      namespace drogon {<!-- -->
          template <>
          inline User fromRequest(const HttpRequest & amp; req) {<!-- -->
              auto json = req.getJsonObject();
              User user;
              if (json) {<!-- -->
                  user.userId = (*json)["userId"].asString();
                  user.account = (*json)["account"].asString();
                  user.passwd = (*json)["passwd"].asString();
              }
              return user;
          }
      }
      

Then here is an example:

HttpTestController.h

#pragma once

#include <drogon/HttpController.h>
#include "User.h"

using namespace drogon;

class HttpTestController : public drogon::HttpController<HttpTestController>
{<!-- -->
  public:
      METHOD_LIST_BEGIN
          //Add two path mappings
          ADD_METHOD_TO(HttpTestController::setInfo, "/user/set", Post);
          ADD_METHOD_TO(HttpTestController::getInfo, "/{1:userId}/info?token={2}", Get);
      METHOD_LIST_END
      void setInfo(const HttpRequestPtr & amp; req,
          std::function<void(const HttpResponsePtr & amp;)> & amp; & amp; callback,
          User & amp; & amp; user);
      void getInfo(const HttpRequestPtr & amp; req,
          std::function<void(const HttpResponsePtr & amp;)> & amp; & amp; callback,
          std::string userId,
          const std::string & amp; token) const;
};

HttpTestController.cc

#include "HttpTestController.h"

void HttpTestController::setInfo(const HttpRequestPtr & amp; req, std::function<void(const HttpResponsePtr & amp;)> & amp; & amp; callback, User & amp; & amp; user) {<!-- - ->
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k200OK);
resp->setContentTypeCode(ContentType::CT_TEXT_HTML);
if (user.userId.empty() || user.account.empty() || user.passwd.empty()) {<!-- -->
resp->setBody("Error!");
}
else {<!-- -->
resp->setBody("User " + user.userId + " saved!");
}
callback(resp);
}

void HttpTestController::getInfo(const HttpRequestPtr & amp; req, std::function<void(const HttpResponsePtr & amp;)> & amp; & amp; callback, std::string userId, const std::string & amp; token ) const {<!-- -->
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k200OK);
resp->setContentTypeCode(ContentType::CT_TEXT_HTML);
resp->setBody("id: " + userId + "<br/>account: " + "123456" + "<br/>passwd: " + "******" + "<br/>token: " + token);
callback(resp);
}

The result is as follows:

3. WebSocketController

Readers who are familiar with computer networks should be familiar with websocket connections. This is a long connection solution based on HTTP. Only an HTTP format request and response exchange is performed when the connection is established, and all subsequent message transmissions are performed on websockets.

Similarly, WebSocketController can be quickly generated through drogon_ctl:

drogon_ctl create controller -w WebSocketTestController

In the generated file, you can see that drogon has provided me with three handler:

  • handlerNewConnection is called after the websocket connection is established. The req in the parameter is the establishment request sent by the user. At this time, the framework has returned the response for us, then req can only provide us with some additional information; wsConnPtr is a smart pointer of this websocket object, which has the following common interfaces:

    //Send websocket message
    void send(const char *msg,uint64_t len); // Message content and length
    void send(const std::string & amp;msg); // Message content
    
    // The local and remote addresses of the websocket
    const trantor::InetAddress & amp;localAddr() const;
    const trantor::InetAddress & amp;peerAddr() const;
    
    //The connection status of the websocket
    bool connected() const;
    bool disconnected() const;
    
    //Close the websocket
    void shutdown();
    void forceClose();
    
    //Set and get the context of this websocket, and store some business data by the user.
    // The any type means that any type of object can be accessed.
    void setContext(const any & amp;context);
    const any &getContext() const;
    any *getMutableContext();
    
  • handleNewMessage is called after the websocket receives a new message, and message is the message received (this message is the complete message payload, and the framework has completed the message For work such as unpacking and decoding, the user can directly process the message itself). type, as the name suggests, is the type of message. By looking at the source code of WebSocketConnection.h, we can find that it defaults is 0, corresponding to text format.

  • handleConnectionClosed is called after the websocket connection is closed, and we can do some finishing work inside.

In addition to the handler, the generated file also contains macros for adding path mapping:

  • We can register this controller to a certain path through the WS_PATH_ADD macro. The usage of this macro is similar to the previous one. Its parameters are the path to be mapped and the filter.

Below is an example:

WebSocketTestController.h

#pragma once

#include <drogon/WebSocketController.h>

using namespace drogon;

class WebSocketTestController : public drogon::WebSocketController<WebSocketTestController>
{<!-- -->
  public:
     void handleNewMessage(const WebSocketConnectionPtr & amp;,
                                  std::string & amp; & amp;,
                                  const WebSocketMessageType & amp;) override;
    void handleNewConnection(const HttpRequestPtr & amp;,
                                     const WebSocketConnectionPtr & amp;) override;
    void handleConnectionClosed(const WebSocketConnectionPtr & amp;) override;
    WS_PATH_LIST_BEGIN
        WS_PATH_ADD("/chat");
    WS_PATH_LIST_END
};

WebSocketTestController.cc

#include "WebSocketTestController.h"
#include <time.h>

void WebSocketTestController::handleNewMessage(const WebSocketConnectionPtr & amp; wsConnPtr, std::string & amp; & amp;message, const WebSocketMessageType & amp;type)
{<!-- -->
    // Get the current time
    time_t timep;
    tm*p;
    time(&timep);
    p = localtime( & amp;timep);
    std::string strTime = std::format("[{}/{}/{} {}:{}] ", 1900 + p->tm_year, 1 + p->tm_mon, p->tm_mday, p- >tm_hour, p->tm_min);

    wsConnPtr->send(strTime + message);
}

void WebSocketTestController::handleNewConnection(const HttpRequestPtr & amp;req, const WebSocketConnectionPtr & amp; wsConnPtr)
{<!-- -->
    wsConnPtr->send("Welcome!");
}

void WebSocketTestController::handleConnectionClosed(const WebSocketConnectionPtr & amp; wsConnPtr)
{<!-- -->
}

The result is as follows:

2. Filter

Filter, as the name suggests, can provide us with the function of filtering user requests. It can help us improve programming efficiency. For example, if we want to implement multiple business functions, but these functions must be logged in by the user before they can be used, we can Add a user login filter to the request path of each function.

In drogon, after the drogon framework completes URL path matching, it will first call the filters registered on the path in sequence. Only when all filters are allowed to “pass”, the corresponding handler will be called.

1. Built-in filter

The drogon framework itself has several built-in filters for us to use directly. Commonly used built-in filters include:

  • drogon::IntraneIpFilter: Only allow http requests from intranet IP, otherwise return to 404 page

  • drogon::LocalHostFilter: Only allow http requests from the local machine 127.0.0.1, otherwise return to the 404 page

2. Custom filters

In addition to the built-in filters, we can also customize filters by inheriting the HttpFilter class template.

The custom format is as follows:

class LoginFilter:public drogon::HttpFilter<LoginFilter>
{<!-- -->
public:
    virtual void doFilter(const HttpRequestPtr & amp;req,
                          FilterCallback & amp; & amp;fcb,
                          FilterChainCallback & amp; & amp;fccb) override;
};

Of course, we can also quickly create it through drogon_ctl:

drogon_ctl create filter LoginFilter

As you can see, the created filter has a virtual function. To implement the logic of the filter, you must overload this function. This function has a total of three parameters:

  • req: http request
  • fcb: Filter callback function. When the request is filtered out by the filter, this callback is used to return a specific response to the user.
  • fccb: Filter chain callback function. When the request passes the filter, this callback tells drogon to call the next filter or the final handler.

After creating the filter, we also need to configure it on the path. As mentioned before, we only need to add the filter to the parameters of the macro that adds path mapping, like this:

ADD_METHOD_TO(HttpTestController::getInfo, "/{1:userId}/info?token={2}", Get, "LoginFilter");

Note: If the filter is in the defined namespace, you need to add the namespace here

Here is a concrete example of creating a filter for users who are not logged in:

LoginFilter.h

#pragma once

#include <drogon/HttpFilter.h>
using namespace drogon;

class LoginFilter : public HttpFilter<LoginFilter> {<!-- -->
  public:
    LoginFilter() {<!-- -->}
    void doFilter(const HttpRequestPtr & amp;req,
                  FilterCallback & amp; & amp;fcb,
                  FilterChainCallback & amp; & amp;fccb) override;
};

LoginFilter.cc

void LoginFilter::doFilter(const HttpRequestPtr & amp;req,
                          FilterCallback & amp; & amp;fcb,
                          FilterChainCallback & amp; & amp;fccb) {<!-- -->
    // Check if the user is logged in
    std::string userId = req->getParameter("token");
    if (userId.empty()) {<!-- -->
    //The user is not logged in, return prompt information
    auto resp = HttpResponse::newHttpResponse();
    resp->setStatusCode(k302Found);
    resp->setBody("Not logged in, about to jump to the login page...");
    fcb(resp);
}
else {<!-- -->
    // The user is logged in, continue processing the request
    fccb();
}
}

Then configure this filter on the path:

ADD_METHOD_TO(HttpTestController::getInfo, "/{1:userId}/info?token={2}", Get, "LoginFilter");

The effect is as follows:

Not logged in

Logged in

3. View

View is a component in a common software design pattern. It is often used in MVC (Model-View-Controller) architecture. Its main responsibility is to display data to users and receive user input. Although the architectural pattern of front-end and back-end separation has become very popular in recent years, this does not mean that we can no longer use MVC, because front-end and back-end separation and MVC are not inconsistent, and we can use them in a reasonable combination according to project needs.

In drogon, in order to implement views, drogon defines a csp (C++ Server Page) description language, which is similar to jsp and can embed code into HTML pages, but csp embeds c++ instead of java. .

1. CSP

Drogon’s csp solution is very simple. We use special markup symbols to embed the C++ code into the HTML page:

  • <%inc %>

    The content in this tag will be regarded as the part of the header file that needs to be quoted. Only #include statements can be written here, such as <%inc#include "xx.h" %>, but many common header files are automatically included by drogon, so we don’t need to add them.

  • %

    The content in this tag will be regarded as C++ code, such as

    C++ code is generally transferred to the target source file intact, except for the following two special markers:

    • @@: Represents the data variable passed from the controller. The type is HttpViewData, from which the required content can be obtained.
    • $$: A stream object that represents page content. The content that needs to be displayed can be displayed on the page through the << operator.
  • [[ ]]

    The content in this tag will be regarded as a variable name. The view will use this variable name keyword to find the corresponding variable from the data passed by the controller, and output it to the corresponding position on the page. The spaces before and after the variable name will be Omitted; at the same time, for performance reasons, only three string data types are supported: const char *, std::string and const std::string code>. In addition, do not write these tags on separate lines.

  • {% %}

    The content in this tag will be directly regarded as a variable name or expression in the C++ program instead of a keyword. The view will output the content of the variable or the value of the expression to the corresponding location on the page. Therefore, {% val.xx %} is equivalent to <%c + + $$<. In addition, do not write these tags on separate lines.

  • <%view %>

    The content in this tag will be regarded as the name of the subview, and drogon will find the corresponding subview and fill its content into the location of the tag; the subview and the parent view share the data of the controller, and can be nested at multiple levels, but Don't nest loops. In addition, do not write these tags on separate lines.

  • <%layout %>

    The content in this tag will be regarded as the name of the layout, and the framework will find the corresponding layout and fill the content of this view into a certain position in the layout. In addition, do not write this pair of tags in separate lines. They can be nested at multiple levels but do not nest in a loop.

2. Use of views

First, convert the written CSP file into a C++ source file, just use drogon_ctl:

drogon_ctl create view xxx.csp

Then use the view to render the response, just call the following interface:

static HttpResponsePtr newHttpViewResponse(const std::string & amp;viewName,
                                           const HttpViewData &data);

It has two parameters:

  • viewName: The name of the view, the extension .csp can be omitted
  • data: The data passed to the view by the controller's handler. The type is HttpViewData. This is a special map that can store and retrieve any type of object.

It can be seen that the controller does not need to reference the header file of the view, and the controller and the view are well decoupled; their only connection is the data variable, and the controller and the view must have a consistent agreement on the content of data.

Let's take the previously registered /{userId}/info path as an example and create a view for it:

UserInfo.csp

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>[[ title ]]</title>
</head>
<body>
    <H1>UserInfo</H1>
    <table>
        <tr>
            <th>userId</th>
            <td>[[ userId ]]</td>
        </tr>
        <tr>
            <th>account</th>
            <td>[[ account ]]</td>
        </tr>
        <tr>
            <th>passwd</th>
            <td>[[ passwd ]]</td>
        </tr>
        <tr>
            <th>token</th>
            <td>[[ token ]]</td>
        </tr>
    </table>
</body>
</html>

HttpTestController::getInfo

void HttpTestController::getInfo(const HttpRequestPtr & amp; req, std::function<void(const HttpResponsePtr & amp;)> & amp; & amp; callback, const std::string & amp; userId, const std ::string & amp; token) const {<!-- -->
HttpViewData data;
data.insert("title", "UserInfo");
data.insert("account", "admin");
data.insert("passwd", "admin");
data.insert("userId", userId);
data.insert("token", token);
auto resp = HttpResponse::newHttpViewResponse("UserInfo.csp", data);
callback(resp);
}

Then use drogon_ctl to convert the csp file into a c++ source file:

drogon_ctl create view UserInfo.csp

Build and run cmake, the results are as follows: