【C++/Drogon框架】二、控制器(controller)、过滤器(filter)与视图(view)
文章目录
一、 控制器(controller)
控制器是Web开发中重要的部分,其可以接收浏览器发来的请求并生成相应的数据进行响应,一般用来为前端提供接口,或结合视图直接向用户提供功能接口。在Drogon中,有关网络传输、http解析等并不需要我们关心,我们要做的只有实现逻辑功能。
在controller中,我们可以定义多个函数(一般叫做handler),用来处理一个模块中的多个逻辑功能,如用户的登录和注册。
Drogon的controller一共有三种:HttpSimpleController
、HttpController
、WebSocketController
,使用时只要继承响应的类模板即可。
1、HttpSimpleController
我们可以使用drogon_ctl工具快捷生成一个名为testController
的控制器:
drogon_ctl create controller TestController
#注意:如果你是用的drogon_ctl创建的项目,这条命令要在controller目录下执行
-
每个
HttpSimpleController
只能有一个handler
,而且通过虚函数定义 -
Drogon为我们提供了一个宏
PATH_ADD
用来进行路径映射,它要被包含在PATH_LIST_BEGIN
和PATH_LIST_END
之间,这个宏的第一个参数为你要映射的路径,然后它还提供两种约束:一种是HttpMethod
的类型,用来规定请求方式,另一种是HttpFilter
,用来过滤请求,详见
下面举一个例子:
TestController.h
#pragma once
#include <drogon/HttpSimpleController.h>
using namespace drogon;
class TestController : public drogon::HttpSimpleController<testController>
{
public:
// 只能有一个handler且用虚函数定义
void asyncHandleHttpRequest(const HttpRequestPtr& req, std::function<void (const HttpResponsePtr &)> &&callback) override;
PATH_LIST_BEGIN
// list path definitions here;
// PATH_ADD("/path", "filter1", "filter2", HttpMethod1, HttpMethod2...);
// 为根路径的POST请求进行映射
PATH_ADD("/", Post);
// 为/test路径的Get请求进行映射
PATH_ADD("/test", Get);
PATH_LIST_END
};
TestController.cc
#include "TestController.h"
void TestController::asyncHandleHttpRequest(const HttpRequestPtr& req, std::function<void (const HttpResponsePtr &)> &&callback)
{
// write your application logic here
// 创建Http响应
auto resp = HttpResponse::newHttpResponse();
// 设置状态码为200
resp->setStatusCode(k200OK);
// 设置contentType
resp->setContentTypeCode(CT_TEXT_HTML);
// 设置响应体
if (req->getMethod() == Get) {
resp->setBody("Get!");
}
else if (req->getMethod() == Post) {
resp->setBody("Post!");
}
// 调用回调函数
callback(resp);
}
结果如下:
2、HttpController
HttpController
也可以通过drogon_ctl快速生成,只需要加伤-h
参数
drogon_ctl create controller -h HttpTestController
-
HttpController
中drogon为我们提供了两种添加映射的宏:METHOD_ADD
和ADD_METHOD_TO
,其中,-
METHOD_ADD
添加的映射会自动添加前缀(命名空间+类名),比如在命名空间demo
下的类HttpTestController
使用该宏添加映射会自动增加/demo/HttpTestController/
的前缀; -
而
ADD_METHOD_TO
宏则不会添加前缀。
两者的参数要求相同,第一个为你要映射到的handler,第二个为要添加映射的路径(支持正则表达式),然后约束条件与
HttpSimpleController
的相同,这里就不多赘述了。 -
-
除了路径,我们还可以对参数进行映射:
-
首先是路径参数和问号后的请求参数,这两种参数的映射有四种方式:
{}
:这种只有大括号的,会将对应参数映射到对应参数位置{1}
,{2}
:大括号中带有数字的,会将对应参数映射到对应数字指定的参数位置{name}
:中间的字符串没有实际作用,但能提高程序的可读性{1:name1}
,{2:name2}
:冒号后的字符串同样没有实际作用,但能提高程序的可读性
例如:
"/{1:username}/test?token={2:token}"
需要注意的是,目标参数类型只能是基本类型,
std:string
类型或任何可以使用stringstream >>
操作符赋值的类型 -
另外,drogon还提供了从HttpRequestPtr对象到任意类型的参数映射机制,当handler要求的参数大于路径中的参数映射时,后面多余的参数会有HttpRequestPtr对象转换而来。用户可以定义任意类型的转换,定义这种转换的方式是特化drogon命名空间的fromRequest模板(定义于HttpRequest.h头文件)。比如我们定义一个user的结构体:
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& 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; } }
-
然后下面是一个例子:
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_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& req,
std::function<void(const HttpResponsePtr&)>&& callback,
User&& user);
void getInfo(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
std::string userId,
const std::string& token) const;
};
HttpTestController.cc
#include "HttpTestController.h"
void HttpTestController::setInfo(const HttpRequestPtr& req, std::function<void(const HttpResponsePtr&)>&& callback, User&& 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& req, std::function<void(const HttpResponsePtr&)>&& callback, std::string userId, const std::string& 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);
}
结果如下:
3、WebSocketController
熟悉计算机网络的读者应该对websocket连接不陌生,这是基于HTTP的一种长连接方案,只有在连接建立时进行一次HTTP格式的请求和应答交换,之后所有的消息传输都在websocket上进行。
同样的,WebSocketController
可以通过drogon_ctl
快速生成:
drogon_ctl create controller -w WebSocketTestController
生成的文件中可以看到drogon
已经给我提供好的三个handler
:
-
handlerNewConnection
是在websocket连接建立后被调用的,参数中的req
是用户发来的建立请求,这时候框架已经帮我们返回了response,那么req
就只能为我们提供一些额外信息了;wsConnPtr
是这个websocket对象的一个智能指针,它有如下常用接口:// 发送websocket消息 void send(const char *msg,uint64_t len); // 消息内容和长度 void send(const std::string &msg); // 消息内容 // 该websocket的本机和远端地址 const trantor::InetAddress &localAddr() const; const trantor::InetAddress &peerAddr() const; // 该websocket的连接状态 bool connected() const; bool disconnected() const; // 关闭该websocket void shutdown(); void forceClose(); // 设置和获取本websocket的上下文,由用户存入一些业务数据, // any类型意味着可以存取任意类型的对象。 void setContext(const any &context); const any &getContext() const; any *getMutableContext();
-
handleNewMessage
是在websocket接收到新消息后被调用的,message
是消息接收到的消息(这个message是完整的消息净荷,框架已经做完了消息的解封包和解码等工作,用户直接处理消息本身即可),type
顾名思义是消息的类型,通过查看WebSocketConnection.h
的源码,我们可以发现它默认为0,对应text格式。 -
handleConnectionClosed
是在websocket连接关闭后被调用的,我们可以在里面做一些收尾工作。
除了handler,生成的文件中还有添加路径映射的宏:
- 我们可以通过
WS_PATH_ADD
宏把这个控制器注册到某个路径上,这个宏的用法与之前的类似,它的参数为要映射的路径和过滤器。
下面是一个例子:
WebSocketTestController.h
#pragma once
#include <drogon/WebSocketController.h>
using namespace drogon;
class WebSocketTestController : public drogon::WebSocketController<WebSocketTestController>
{
public:
void handleNewMessage(const WebSocketConnectionPtr&,
std::string &&,
const WebSocketMessageType &) override;
void handleNewConnection(const HttpRequestPtr &,
const WebSocketConnectionPtr&) override;
void handleConnectionClosed(const WebSocketConnectionPtr&) 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& wsConnPtr, std::string &&message, const WebSocketMessageType &type)
{
// 获取当前时间
time_t timep;
tm* p;
time(&timep);
p = localtime(&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 &req, const WebSocketConnectionPtr& wsConnPtr)
{
wsConnPtr->send("Welcome!");
}
void WebSocketTestController::handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr)
{
}
结果如下:
二、过滤器(filter)
过滤器(filter),顾名思义,能够为我们提供提供过滤用户请求的功能,它可以帮助我们提高编程效率,例如要实现多个业务功能,但这些功能都要用户登录了才能使用,我们就可以在每个功能的请求路径上加上一个用户登录的过滤器。
在drogon中,drogon框架做完URL路径匹配后,会先依次调用注册到该路径上的过滤器,只有当所有过滤器都允许"通过"时,对应的handler才会被调用。
1、内置过滤器
drogon框架本身就内置了几个过滤器,供我们直接使用,常用的内置过滤器有:
-
drogon::IntraneIpFilter
: 只放行内网ip发来的http请求,否则返回404页面 -
drogon::LocalHostFilter
: 只放行本机127.0.0.1发来的http请求,否则返回404页面
2、自定义过滤器
除了内置的过滤器,我们还可以自定义过滤器,只需要继承HttpFilter类模板。
自定义格式如下:
class LoginFilter:public drogon::HttpFilter<LoginFilter>
{
public:
virtual void doFilter(const HttpRequestPtr &req,
FilterCallback &&fcb,
FilterChainCallback &&fccb) override ;
};
当然,我们也可以通过drogon_ctl
快速创建:
drogon_ctl create filter LoginFilter
可以看到,创建的过滤器有一个虚函数,要实现过滤器的逻辑就要重载该函数,该函数一共有三个参数:
req
: http请求fcb
: 过滤器回调函数,当请求被过滤器过滤掉时,通过这个回调来给用户返回特定响应fccb
: 过滤器链回调函数,当请求通过过滤器时,通过这个回调告诉drogon调用下一个过滤器或者最终的handler
创建了过滤器后,我们还需要将其配置到路径上,就和前面提到的一样,只需要在添加路径映射的宏的参数中加上过滤器就行,就像这样:
ADD_METHOD_TO(HttpTestController::getInfo, "/{1:userId}/info?token={2}", Get, "LoginFilter");
注意:如果过滤器在定义的命名空间里,这里还需要再加上命名空间
下面是一个具体的例子,创建一个过滤未登录用户的过滤器:
LoginFilter.h
#pragma once
#include <drogon/HttpFilter.h>
using namespace drogon;
class LoginFilter : public HttpFilter<LoginFilter> {
public:
LoginFilter() {}
void doFilter(const HttpRequestPtr &req,
FilterCallback &&fcb,
FilterChainCallback &&fccb) override;
};
LoginFilter.cc
void LoginFilter::doFilter(const HttpRequestPtr &req,
FilterCallback &&fcb,
FilterChainCallback &&fccb) {
// 检查用户是否已经登录
std::string userId = req->getParameter("token");
if (userId.empty()) {
// 用户未登录,返回提示信息
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k302Found);
resp->setBody("Not logged in, about to jump to the login page...");
fcb(resp);
}
else {
// 用户已登录,继续处理请求
fccb();
}
}
然后将这个过滤器配置到路径上:
ADD_METHOD_TO(HttpTestController::getInfo, "/{1:userId}/info?token={2}", Get, "LoginFilter");
实现效果如下:
未登录
已登录
三、视图(view)
视图是一种常见的软件设计模式中的组件,它常被用于MVC(Model-View-Controller)架构中,它的主要职责是展示数据给用户,并接收用户的输入。虽然最近几年前后端分离的架构模式很流行,但是这并不意味着我们可以不再使用MVC了,因为前后端分离和MVC并不矛盾,我们可以根据项目需求合理搭配使用。
在drogon中,为了实现视图,drogon定义了一种csp(C++ Server Page)描述语言,它与jsp类似,都可以将代码嵌入到HTML页面中,不过csp嵌入的是c++而不是java。
1、CSP
drogon的csp方案很简单,我们用特殊的标记符号把C++代码嵌入到HTML页面里就可以了:
-
<%inc %>
这个标签里的内容会被视为需要引用的头文件部分,这里只能写入
#include
语句,如<%inc#include "xx.h" %>
,不过很多常见的头文件drogon都自动包含了,我们就不需要再添加了。 -
%<c++ %>
这个标签里的内容会被视为C++的代码,如
<c++ std:string name="drogon"; %>
C++的代码一般都会原封不动的转移到目标源文件中,除了下面两种特殊标记:
@@
:表示控制器传过来的data变量,类型是HttpViewData
,可以从中获取需要的内容$$
:表示页面内容的流对象,可以把需要显示的内容通过<<
操作符显示在页面上
-
[[ ]]
这个标签里的内容会被视为变量名,view会以这个变量名为keyword从控制器传过来的数据里找到对应的变量,并把它输出到页面的对应位置,变量名字前后的空格会被省略;同时,出于性能考虑,只支持三种字符串数据类型
const char *
,std::string
和const std::string
。另外,这对标签不要分行写。 -
{% %}
这个标签里的内容会被直接视为C++程序里的变量名或表达式,而不是keyword,view会把该变量的内容或表达式的值输出到页面的对应位置。因此,
{% val.xx %}
等效于<%c++ $$<<val.xx; %>
。另外,这对标签不要分行写。 -
<%view %>
这个标签里的内容会被视为子视图的名字,drogon会找到相应的子视图并把它的内容填充到该标签所在位置;子视图和父视图共用控制器的数据, 可以多级嵌套但不要循环嵌套。另外,这对标签不要分行写。
-
<%layout %>
这个标签里的内容会被视为布局的名字,,框架会找到相应的布局并把本视图的内容填充到该布局的某个位置。另外,这对标签不要分行写,可以多级嵌套但不要循环嵌套。
2、视图的使用
首先要将写好的CSP文件转换成C++源文件,只需使用drogon_ctl
即可:
drogon_ctl create view xxx.csp
然后使用视图渲染响应,只需调用如下接口:
static HttpResponsePtr newHttpViewResponse(const std::string &viewName,
const HttpViewData &data);
它有两个参数:
- viewName:视图的名字,扩展名.csp可省略
- data:控制器的handler传给视图的数据,类型是
HttpViewData
,这是个特殊的map,可以存入和取出任意类型的对象
可以看到,控制器不需要引用视图的头文件,控制器和视图实现了很好的解耦;他们唯一的联系是data变量,对data的内容,控制器和视图要有一致的约定。
我们以之前注册的/{userId}/info
路径为例,为其创建一个视图:
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& req, std::function<void(const HttpResponsePtr&)>&& callback, const std::string& userId, const std::string& 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);
}
然后使用drogon_ctl
将csp文件转换成c++源文件:
drogon_ctl create view UserInfo.csp
cmake构建一下,运行,结果如下: