[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 onehandler
, and it is defined through virtual functions -
Drogon provides us with a macro
PATH_ADD
for path mapping, which should be included betweenPATH_LIST_BEGIN
andPATH_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 ofHttpMethod
, which is used to specify the request method, and the other isHttpFilter
, 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
andADD_METHOD_TO
, where,-
The mapping added by
METHOD_ADD
will automatically add a prefix (namespace + class name). For example, the classHttpTestController
under the namespacedemo
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 thestringstream >>
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. Thereq
in the parameter is the establishment request sent by the user. At this time, the framework has returned the response for us, thenreq
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, andmessage
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 ofWebSocketConnection.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 requestfcb
: 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 isHttpViewData
, 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
andconst 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: