web开发框架--drogon
# drogon概述
Drogon是一个基于C++14/17的Http应用框架,使用Drogon可以方便的使用C++构建各种类型的Web应用服务端程序。
Drogon的主要应用平台是Linux,也支持Mac OS、FreeBSD和Windows。它的主要特点如下:
- 网络层使用基于epoll(macOS/FreeBSD下是kqueue)的非阻塞IO框架,提供高并发、高性能的网络IO。详细请见TFB Tests Results;
- 全异步编程模式;
- 支持Http1.0/1.1(server端和client端);
- 基于template实现了简单的反射机制,使主程序框架、控制器(controller)和视图(view)完全解耦;
- 支持cookies和内建的session;
- 支持后端渲染,把控制器生成的数据交给视图生成Html页面,视图由CSP模板文件描述,通过CSP标签把C++代码嵌入到Html页面,由drogon的命令行工具在编译阶段自动生成C++代码并编译;
- 支持运行期的视图页面动态加载(动态编译和加载so文件);
- 非常方便灵活的路径(path)到控制器处理函数(handler)的映射方案;
- 支持过滤器(filter)链,方便在控制器之前执行统一的逻辑(如登录验证、Http Method约束验证等);
- 支持https(基于OpenSSL实现);
- 支持websocket(server端和client端);
- 支持Json格式请求和应答, 对Restful API应用开发非常友好;
- 支持文件下载和上传,支持sendfile系统调用;
- 支持gzip/brotli压缩传输;
- 支持pipelining;
- 提供一个轻量的命令行工具drogon_ctl,帮助简化各种类的创建和视图代码的生成过程;
- 基于非阻塞IO实现的异步数据库读写,目前支持PostgreSQL和MySQL(MariaDB)数据库;
- 基于线程池实现sqlite3数据库的异步读写,提供与上文数据库相同的接口;
- 支持ARM架构;
- 方便的轻量级ORM实现,支持常规的对象到数据库的双向映射操作;
- 支持插件,可通过配置文件在加载期动态拆装;
- 支持内建插入点的AOP
# drogon安装
# 准备工作
# 环境
sudo apt install git
sudo apt install gcc
sudo apt install g++
sudo apt install cmake
# jsoncpp
sudo apt install libjsoncpp-dev
# uuid
sudo apt install uuid-dev
# OpenSSL
sudo apt install openssl
sudo apt install libssl-dev
# zlib
sudo apt install zlib1g-dev
# 安装drogon
cd $WORK_PATH
git clone https://github.com/an-tao/drogon
cd drogon
sudo git submodule update --init
mkdir build
cd build
cmake ..
make && sudo make install
默认是编译debug版本,如果想编译release版本,cmake命令要带如下参数:
cmake -DCMAKE_BUILD_TYPE=Release ..
安装结束后,将有如下文件被安装在系统中(CMAKE_INSTALL_PREFIX可以改变安装位置):
- drogon的头文件被安装到/usr/local/include/drogon中;
- drogon的库文件libdrogon.a被安装到/usr/local/lib中;
- drogon的命令行工具drogon_ctl被安装到/usr/local/bin中;
- trantor的头文件被安装到/usr/local/include/trantor中;
- trantor的库文件libtrantor.a被安装到/usr/local/lib中;
# drogon简单使用
# 静态网站
我们从一个最简单的例子开始介绍drogon的使用,在这个例子中我们使用命令行工具drogon_ctl创建一个工程:
drogon_ctl create project your_project_name
进入工程目录,可以看到如下文件:
├── build 构建文件夹
├── CMakeLists.txt 工程的cmake配置文件
├── config.json drogon应用的配置文件
├── controllers 存放控制器文件的目录
├── filters 存放过滤器文件的目录
├── main.cc 主程序
├── models 数据库模型文件的目录
│ └── model.json
└── views 存放视图csp文件的目录
main.cc文件,内容如下:
#include <drogon/HttpAppFramework.h>
int main() {
//Set HTTP listener address and port
drogon::app().addListener("0.0.0.0",80);
//Load config file
//drogon::app().loadConfigFile("../config.json");
//Run HTTP framework,the method will block in the internal event loop
drogon::app().run();
return 0;
}
然后构建项目:
cd build
cmake ..
make
编译完成后,运行目标程序./your_project_name
.
现在,我们在Http根目录添加一个最简单的静态文件index.html:
echo '<h1>Hello Drogon!</h1>' >>index.html
Http根目录默认值是"./"
, 也就是webapp程序运行的当前路径, Http根目录也可在config.json配置文件中进行更改,可参见配置文件, 然后在地址栏输入http://localhost
或http://localhost/index.html
(或者你的webapp所在服务器的ip)可以访问到这个页面,如果服务器找不到浏览器访问的页面,将返回404页面。
注意:请确认服务器的防火墙已经打开80端口,否则你看不到这些页面。
我们可以把一个静态网站的目录和文件复制到这个webapp的运行目录,然后通过浏览器就可以访问到它们,drogon默认支持的文件类型有"html","js","css","xml","xsl","txt","svg","ttf","otf","woff2","woff","eot","png","jpg","jpeg","gif","bmp","ico","icns"等等。
# 动态网站
下面我们看看怎么给这个应用添加控制器(controller),并使用控制器(controller)输出内容。
在controller
目录下运行drogon_ctl命令行工具生成控制器(controller)源文件:
drogon_ctl create controller TestCtrl
可以看到,目录下新增加了两个文件,TestCtrl.h和TestCtrl.cc:
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
//list path definitions here;
//PATH_ADD("/path","filter1","filter2",HttpMethod1,HttpMethod2...);
PATH_LIST_END
};
TestCtrl.cc如下:
#include "TestCtrl.h"
void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback)
{
//write your application logic here
}
我们编辑一下这两个文件,让这个控制器处理函数回应一个简单的“Hello World!”。
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
//list path definitions here;
//PATH_ADD("/path","filter1","filter2",HttpMethod1,HttpMethod2...);
PATH_ADD("/",Get,Post);
PATH_ADD("/test",Get);
PATH_LIST_END
};
使用PATH_ADD添加路径到处理函数的映射,这里映射了两个路径'/'和'/test',并在路径后面添加了对这个路径的约束。
TestCtrl.cc如下:
#include "TestCtrl.h"
void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback)
{
//write your application logic here
auto resp=HttpResponse::newHttpResponse();
resp->setStatusCode(k200OK);
resp->setContentTypeCode(CT_TEXT_HTML);
resp->setBody("Hello World!");
callback(resp);
}
重新用cmake编译这个工程,然后运行目标程序./your_project_name
:
cd ../build
cmake ..
make
./your_project_name
在浏览器地址栏输入http://localhost/
或者http://localhost/test
,你就可以在浏览器看到Hello World!
了。
注意: 同时存在静态和动态资源的情况下,框架优先使用控制器响应请求,此例中http://localhost/
响应的是TestCtrl
控制器的输出Hello Word!
而不是静态网页index.html
的Hello Drogon!
我们看到,在应用中添加controller非常简单,只需要添加对应的源文件即可,甚至main文件不用做任何修改,这种低耦合度的设计对web应用开发是非常有效的。
注意: Drogon没有限制控制器(controller)源文件的位置,也可以放在工程目录下,甚至可以在CMakeLists.txt
中指定到新的目录中,为了方便管理,建议将控制器源文件放在controllers目录。
# 控制器(controller)
控制器(controller)在web应用开发中处于相当重要的地位,它处理浏览器发来的请求,然后生成响应发送给浏览器;drogon框架已经帮我们处理好网络传输、Http协议的解析等等细节,我们只需要关注控制器的逻辑即可;每一个控制器对象可以有一个或者多个处理函数(一般称为handler),函数的接口,一般定义成如下形式:
void handlerName(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
...);
其中,req
是Http请求的对象(被智能指针包裹),callback
是框架传给控制器的回调函数对象,控制器生成应答对象(也通过智能指针包裹)后,通过callback把该对象传给drogon,然后框架会帮你把响应内容发送给浏览器,最后面的是若干参数列表...
,由drogon根据映射规则把Http请求中的参数映射到对应的handler的形参上,这是对应用开发是非常方便的。
很明显,这是个异步接口,用户可以在其它线程完成耗时操作后再调用callback;
drogon的控制器分为三种类型,HttpSimpleController
,HttpController
和WebSocketController
,用户使用时,需要继承相应的类模板,比如,一个HttpSimpleController的自定义类"MyClass"声明如下:
class MyClass:public drogon::HttpSimpleController<MyClass>
{
public:
//TestController(){}
virtual void asyncHandleHttpRequest(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback) override;
PATH_LIST_BEGIN
PATH_ADD("/json");
PATH_LIST_END
};
# 控制器的生命周期
注册到drogon框架的控制器最多只会有一个实例,在整个应用运行期间都不会销毁,所以,用户可以在控制器类中声明和使用成员变量。注意,控制器的handler被调用时,是在多线程环境下的(当框架的IO线程数配置成大于1的值时),如果需要访问非临时变量,请做好并发保护工作。
# HttpSimpleController
可以由drogon_ctl
命令行工具快速生成基于HttpSimpleController
的自定义类的源文件,命令格式如下:
drogon_ctl create controller <[namespace::]class_name>
我们创建一个名称为TestCtrl
的控制器:
drogon_ctl create controller TestCtrl
可以看到,目录下新增加了两个文件,TestCtrl.h和TestCtrl.cc,下面阐述一下这两个文件。
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
//list path definitions here;
//PATH_ADD("/path","filter1","filter2",HttpMethod1,HttpMethod2...);
PATH_LIST_END
};
TestCtrl.cc如下:
#include "TestCtrl.h"
void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback)
{
//write your application logic here
}
每个HttpSimpleController类只能定义一个Http请求处理函数(handler),而且通过虚函数重载定义。
从URL路径到处理函数的路由(或称映射)由宏完成,可以用PATH_ADD
宏添加多重路径映射,所有PATH_ADD
语句应夹在PATH_LIST_BEGIN
和PATH_LIST_END
宏语句之间。
第一个参数是映射的路径,路径后面的参数是对这个路径的约束,目前支持两种约束,一种是HttpMethod
类型,表示该路径允许使用的Http方法,可以配置零个或多个,一种是HttpFilter
类的名字,这种对象执行特定的过滤操作,也可以配置0个或多个,两种类型没有顺序要求,框架会处理好类型的匹配。
用户可以把同一个Simple Controller注册到多个路径上,也可以在同一个路径上注册多个Simple Controller(通过HTTP method区分)。
你可以定义一个HttpResponse类的变量,然后使用callback()返回这个变量即可:
//write your application logic here
auto resp=HttpResponse::newHttpResponse();
resp->setStatusCode(k200OK);
resp->setContentTypeCode(CT_TEXT_HTML);
resp->setBody("Your Page Contents");
callback(resp);
上述路径到处理函数的映射是在编译期完成的,事实上,drogon框架也提供了运行期完成映射的接口,运行期映射可以让用户通过配置文件或其它用户接口完成映射或修改映射关系而无需重新编译这个程序(出于性能的考虑,禁止在运行app().run()之后再注册任何映射)。
# HttpController
# 生成
可以由drogon_ctl
命令行工具快速生成基于HttpController
的自定义类的源文件,命令格式如下:
drogon_ctl create controller -h <[namespace::]class_name>
我们创建一个位于demo v1
名称空间内且名称为User
的控制器:
drogon_ctl create controller -h demo::v1::User
可以看到,目录下新增加了两个文件,demo_v1_User.h和demo_v1_User.cc:
demo_v1_User.h如下:
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
namespace demo
{
namespace v1
{
class User:public drogon::HttpController<User>
{
public:
METHOD_LIST_BEGIN
//use METHOD_ADD to add your custom processing function here;
METHOD_LIST_END
//your declaration of processing function maybe like this:
};
}
}
demo_v1_User.cc如下:
#include "demo_v1_User.h"
using namespace demo::v1;
//add definition of your processing function here
# 使用
我们编辑一下这两个文件,然后再阐述它们。
demo_v1_User.h如下:
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
namespace demo
{
namespace v1
{
class User:public drogon::HttpController<User>
{
public:
METHOD_LIST_BEGIN
//use METHOD_ADD to add your custom processing function here;
METHOD_ADD(User::login,"/token?userId={1}&passwd={2}",Post);
METHOD_ADD(User::getInfo,"/{1}/info?token={2}",Get);
METHOD_LIST_END
//your declaration of processing function maybe like this:
void login(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
std::string &&userId,
const std::string &password);
void getInfo(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
std::string userId,
const std::string &token) const;
};
}
}
demo_v1_User.cc如下:
#include "demo_v1_User.h"
using namespace demo::v1;
//add definition of your processing function here
void User::login(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
std::string &&userId,
const std::string &password)
{
LOG_DEBUG<<"User "<<userId<<" login";
//认证算法,读数据库,验证身份等...
//...
Json::Value ret;
ret["result"]="ok";
ret["token"]=drogon::utils::getUuid();
auto resp=HttpResponse::newHttpJsonResponse(ret);
callback(resp);
}
void User::getInfo(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
std::string userId,
const std::string &token) const
{
LOG_DEBUG<<"User "<<userId<<" get his information";
//验证token有效性等
//读数据库或缓存获取用户信息
Json::Value ret;
ret["result"]="ok";
ret["user_name"]="Jack";
ret["user_id"]=userId;
ret["gender"]=1;
auto resp=HttpResponse::newHttpJsonResponse(ret);
callback(resp);
}
每个HttpController
类可以定义多个Http请求处理函数(handler),由于函数数目可以任意多,所以通过虚函数重载是不现实的,我们需要把处理函数本身(而不是类)注册到框架里去。
从URL路径到处理函数的映射由宏完成,可以用METHOD_ADD
宏或ADD_METHOD_TO
宏添加多重路径映射,所有METHOD_ADD
和ADD_METHOD_TO
语句应夹在METHOD_LIST_BEGIN
和METHOD_LIST_END
宏语句之间。
METHOD_ADD
宏会在路径映射中自动把名字空间和类名作为路径的前缀,所以,本例子中,login函数,被注册到了/demo/v1/user/token
路径上,getInfo函数被注册到了/demo/v1/user/xxx/info
路径上。后面的约束跟HttpSimpleController的PATH_ADD宏类似,不再赘述。
如果使用了自动的前缀,访问地址要包含命名空间和类名,此例中要使用http://localhost/demo/v1/user/token?userid=xxx&passwd=xxx
或者http://localhost/demo/v1/user/xxxxx/info?token=xxxx
来访问。
ADD_METHOD_TO
宏的作用与前者几乎一样,除了它不会自动添加任何前缀,即这个宏注册的路径是一个绝对路径。
我们看到,HttpController
提供了更为灵活的路径映射功能,并且可以注册多个处理函数,我们可以把一类功能放在一个类里。
另外可以看到,METHOD_ADD
宏提供了参数映射的方法,我们可以把路径上的参数映射到函数的参数表里,由参数的数码对应形参的位置,非常方便,常见的可以由字符串类型转换的类型都可以作为参数(如std::string,int,float,double等等),框架基于模板的类型推断会自动帮你转换类型,非常方便。注意左值引用必须是const类型。
同一个路径还可以注册多次,相互之间通过Http Method区分,这是合法的,并且是Restful API的通常做法,比如
METHOD_LIST_BEGIN
METHOD_ADD(Book::getInfo,"/{1}?detail={2}",Get);
METHOD_ADD(Book::newBook,"/{1}",Post);
METHOD_ADD(Book::deleteOne,"/{1}",Delete);
METHOD_LIST_END
路径参数的占位符有多种写法:
- {}: 表示这个路径参数映射到处理函数的对应位置上,路径上的位置就是函数参数的位置。
- {1},{2}: 中间有个数字的,表示映射到数字指定的处理函数参数上。
- {anystring}: 中间的字符串没有实际作用,但可以提高程序的可读性,与
{}
等价。 - {1:anystring},{2:xxx}: 冒号前的数字表示位置,后面的字符串没有实际作用,但可以提高程序的可读性,与
{1}
,{2}
等价。
推荐使用后两种写法,如果路径参数和函数参数顺序一直,使用第三种写法即可。容易知道,以下几种写法是等价的:
- "/users/{}/books/{}"
- "/users/{}/books/{2}"
- "/users/{user_id}/books/{book_id}"
- "/users/{1:user_id}/books/{2}"
注意:路径匹配大小写不敏感,参数名字大小写敏感,参数值大小写保持原貌
# 参数映射
通过前面的叙述我们知道,路径上的参数和问号后面的请求参数都可以映射到处理函数的参数列表里,目标参数的类型需要满足如下条件:
必须是值类型、常左值引用或非const右值引用中的一种,不能是非const的左值引用,推荐使用右值引用,这样用户可以随意处置它;
int, long, long long, unsigned long, unsigned long long, float, double, long double等基础类型都可以作为参数类型;
std::string类型;
任何可以使用
stringstream >>
操作符赋值的类型;
另外,drogon框架还提供了从HttpRequestPtr对象到任意类型的参数的映射机制,当你的handler参数列表中映射参数的数量多于路径上的参数时,后面多余的参数将由HttpRequestPtr对象转换得到,用户可以定义任意类型的转换,定义这种转换的方式是特化drogon命名空间的fromRequest模板(定义于HttpRequest.h头文件)),比如我们需要做一个创建新用户的RESTful的接口,我们定义用户的结构体如下:
namespace myapp{
struct User{
std::string userName;
std::string email;
std::string address;
};
}
namespace drogon
{
template <>
inline myapp::User fromRequest(const HttpRequest &req)
{
auto json = req.getJsonObject();
myapp::User user;
if(json)
{
user.userName = (*json)["name"].asString();
user.email = (*json)["email"].asString();
user.address = (*json)["address"].asString();
}
return user;
}
}
有了上面的定义和模板特化,我们就可以向下面这样定义路径和handler:
class UserController:public drogon::HttpController<UserController>
{
public:
METHOD_LIST_BEGIN
//use METHOD_ADD to add your custom processing function here;
ADD_METHOD_TO(UserController::newUser,"/users",Post);
METHOD_LIST_END
//your declaration of processing function maybe like this:
void newUser(const HttpRequestPtr &req,
std::function<void (const HttpResponsePtr &)> &&callback,
myapp::User &&pNewUser) const;
};
可以看到,第三个myapp::User
类型的参数在映射路径上没有对应的占位符,框架会将它视为由req
对象转换的参数,通过用户特化的函数模板得到这个参数,这都是drogon通过模板推导自动在编译期完成的,为用户的开发提供了极大便利。
更进一步,有些用户除了他们自定义类型的数据外,并不需要访问HttpRequestPtr对象,那么他可以把这个自定义的对象放在第一个参数的位置,框架也能正确完成映射,比如上面的例子也可以写成下面这样:
class UserController:public drogon::HttpController<UserController>
{
public:
METHOD_LIST_BEGIN
//use METHOD_ADD to add your custom processing function here;
ADD_METHOD_TO(UserController::newUser,"/users",Post);
METHOD_LIST_END
//your declaration of processing function maybe like this:
void newUser(myapp::User &&pNewUser,
std::function<void (const HttpResponsePtr &)> &&callback) const;
};
# 多路径映射
drogon支持在路径映射中使用正则表达式,在{}
花括号以外的部分可以有限制的使用,比如
ADD_METHOD_TO(UserController::handler1,"/users/.*",Post); /// Match any path prefixed with `/users/`
ADD_METHOD_TO(UserController::handler2,"/{name}/[0-9]+",Post); ///Match any path composed with a name string and a number.
这种方法不支持子表达式,负向匹配等正则表达式,如果想使用他们,请用如下的方案。
# 正则表达式
上面的方法对正则表达式的支持比较有限,如果用户想自由使用正则表达式,drogon提供了ADD_METHOD_VIA_REGEX
宏来实现这一点,比如
ADD_METHOD_VIA_REGEX(UserController::handler1,"/users/(.*)",Post); /// Match any path prefixed with `/users/` and map the rest of the path to a parameter of the handler1.
ADD_METHOD_VIA_REGEX(UserController::handler2,"/.*([0-9]*)",Post); /// Matche any path that ends in a number and map that number to a parameter of the handler2.
ADD_METHOD_VIA_REGEX(UserController::handler3,"/(?!data).*",Post); /// Matches any path that does not start with '/data'
可以看到,使用正则表达式也可以完成参数映射,所有子表达式匹配的字符串都会按顺序映射到handler的参数上。
需要注意的是,使用正则表达式要注意匹配冲突(多个不同的handler都匹配),当冲突发生在同一个controller内部时,drogon只会执行第一个handler(先注册进框架的那个handler),当冲突发生在不同controller之间时,执行哪个handler是不确定的,因此用户需要避免这种冲突发生。
# 附录
# get和post请求的区别
- get可传的参数少;post可传数据大
- get只能简单的在url上传参数;post有消息体,可存放大量数据