Tufão(以下称之为tufao或Tufao)是GitHub上的一个开源C++11异步网络库,依赖于QT和Boost.Http开发。它很酷的一点就是利用QT插件的特性实现了业务处理模块的动态加载和动态更新。这条特性加上其简洁的API让我决定使用它。
一,安装
tufao的安装相对而言还是比较简单的,README的信息基本足以正确安装它。
编译时唯一要注意的一点是:tufao依赖于QT的Qt5Network库和Qt5Core库,以及boost库的头文件。前者需要根据编译工具链在项目里正确地设置库地址(如msvc2015 x64环境下lib库在X:\Qt\Qt5.10.0\5.10.0\msvc2015_64\lib
),后者需要引入boost库的头文件目录(如X:\boost_1_70_0\bin\include\boost-1_70
)。我用minGW和MSVC都编译了一遍,基本没有太大问题。
tufao在make install
后会将其自动生成的设置(pkg\tufao1.prf
)插入到QT的features
中,这样你就可以直接通过在.pro
文件里添加CONFIG += C++11 TUFAO1
的方式直接使用tufao库。在我的电脑上,这个地址是C:\Qt\Qt5.11.0\5.11.0\mingw53_32\mkspecs\features\tufao1.prf
。
tufao1.prf
的内容(作者注:以下代码均额外添加了部分注释)如下:
# 引入QT5Network库;Qt5Core作为QT基本库不需要特意设置引用
QT += network
# 定义tufao的版本
DEFINES += TUFAO_VERSION_MAJOR=1
# 引入tufao库头文件
INCLUDEPATH += "C:\Program Files (x86)\tufao\include\tufao-1"
# 引入tufao库文件
win32 {
CONFIG(debug, debug|release): LIBS += -L"C:/Program Files (x86)/tufao/lib" -ltufao1d
CONFIG(release, debug|release): LIBS += -L"C:/Program Files (x86)/tufao/lib" -ltufao1
} else {
LIBS += -L"C:/Program Files (x86)/tufao/lib" -ltufao1
}
除此之外,在安装了Doxygen后,tufao的帮助文件也可以一并生成并插入到QT的本地帮助文档中。不过我装这个装失败了,tufao的源码注释写得相当棒,直接跳转到相应的声明看注释部分就可以解决使用上的问题了[TODO: 以后可能会再试试装这个吧]。
二,例子
在安装好tufao1.prf
后tufao的项目设置就非常轻松了:
(tufaoserver.pro
):
QT += core
TARGET = tufaoServer
TEMPLATE = app
# C++11 TUFAO1是使用Tufao的必须设置
CONFIG += C++11 TUFAO1
# 由于示例没有使用界面,所以gui库被取消了
QT -= gui
SOURCES += main.cpp
简单一步,就完成了tufao的项目设置。
此外,tufao的Hello World
例子也相当简洁(main.cpp
):
#include
#include
#include
using namespace Tufao;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
HttpServer server;
QObject::connect(&server, &HttpServer::requestReady,
[](HttpServerRequest &, HttpServerResponse &res){ // 业务代码
res.writeHead(HttpResponseStatus::OK);
res.end("Hello World");
});
server.listen(QHostAddress::Any, 8080);
return a.exec();
}
完整的服务器模板也是如此(main.cpp
):
#include
#include
#include
#include
#include
#include
using namespace Tufao;
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
HttpPluginServer plugins{"routes.json"/* 插件设置文件Path */}; // 插件系统
HttpServerRequestRouter router{ // 路由系统
{QRegularExpression{""}, plugins},
{QRegularExpression{""}, HttpFileServer::handler("public"/* 静态资源文件夹Path */)}, // 静态资源文件系统
{QRegularExpression{""}, NotFoundHandler::handler()} // 404错误消息
};
HttpServer server;
QObject::connect(&server, &HttpServer::requestReady,
&router, &HttpServerRequestRouter::handleRequest); // 将HttpServer的接收请求信号连接到路由系统的槽函数上
server.listen(QHostAddress::Any, 8080); // 开始监听8080端口。tufao不会堵塞主线程,这让服务器程序的开发变得轻松了很多。
return a.exec();
}
相较而言,tufao的Plugin库模板就稍微有些难记了
(myplugin.pro
):
TARGET = myPlugin
# 生成类型是 库 而不是app
TEMPLATE = lib
# plugin是QT的插件库,这是tufao能够实现动态加载插件的主要原因。C++11 TUFAO1则用于加载TUFAO1库,想用Tufao库就必须设置
CONFIG += plugin C++11 TUFAO1
SOURCES += plugin.cpp
HEADERS += plugin.h
(plugin.h
):
#ifndef PLUGIN_H
#define PLUGIN_H
#include
class Plugin: public QObject, Tufao::HttpServerPlugin
{
Q_OBJECT
/*
* 作为QT插件,以下两行宏使其能被主程序的QPluginLoader动态加载
*/
Q_PLUGIN_METADATA(IID TUFAO_HTTPSERVERPLUGIN_IID) // QT插件元信息
Q_INTERFACES(Tufao::HttpServerPlugin) // QT插件接口
public:
std::function
createHandler(const QHash &dependencies,
const QVariant &customData = QVariant()) override;
};
#endif // PLUGIN_H
(plugin.cpp
):
#include "plugin.h"
#include
#include
using namespace Tufao;
std::function
Plugin::createHandler(const QHash &,
const QVariant &)
{
return [](HttpServerRequest &, HttpServerResponse &res){ // 业务代码
res.writeHead(HttpResponseStatus::OK);
res.end("Hello World, I am a Tufao Plugin\n");
return true;
};
}
如myplugin.pro
所见,插件的类型是lib(MSVC编译后会生成一个.lib
、一个.dll
和一个.pdb
),并且要引入qt的plugin
库。qt插件库需要一些独特的用法,使得tufao的Plugin库模板略显复杂。不过读懂了之后就还算很容易理解的。
三,使用
tufao的一般使用没有什么难点。插件系统和文件系统的路径部分却还是有个坑的。QT在使用默认编译路径时,程序(通过QT Creator运行时,直接运行.exe
文件反而没这个坑)的应用所在目录
并不是源码所在目录,也不是.exe
所在的debug
或release
文件夹,而是.exe
所在目录的上一级。插件系统所需的routes.json
,如果也使用相对路径(相对于应用所在目录
的相对路径)的话,也必须注意这一点。(如果你对插件加载实在摸不着头脑,可以通过qInstallMessageHandler
把qWarning
消息输出到外部文件里,HttpPluginServer
对象会将和加载插件失败相关的警告通过QtWarningMsg
传递出来。消息示例Warning: Tufao::HttpPluginServer: Couldn't load plugin "plugin/myPlugin.lib"
)。
关于HttpPluginServer
类的行为模式,在httppluginserver.h
第102行(文章更新日当前版本)开始有个很好的解释。原文:
The HttpPluginServer behaviour
==============================
An simplified use case to describing how HttpPluginServer reacts to
changes follows:
1. You start with a default-constructed HttpPluginServer
2. You use setConfig with an inexistent file
1. The HttpPluginServer do not find the file
2. HttpPluginServer::setConfig returns false
3. HttpPluginServer object remains in the previous state
3. You use setConfig with a invalid file
1. HttpPluginServer starts to monitor the config file
2. HttpPluginServer::setConfig returns true
3. HttpPluginServer reads the invalid file and remains in the previous
state.
4. You fill the config file with a valid config.
1. HttpPluginServer object load the new contents
2. HttpPluginServer try to load every plugin and fill the router. If a
plugin cannot be loaded, it will be skipped and a warning message is
sent through qWarning. If you need to load this plugin, make any
modification to the config file and HttpPluginServer will try again.
5. You fill the config file with an invalid config.
1. HttpPluginServer see and ignores the changes, remaining with the
previous settings.
6. You remove the config file.
1. HttpPluginServer object come back to the default-constructed state.
version: 0
If the last config had "version: 0", then it means no more monitoring
either (this is what default-constructed state means).
version: 1
If the last config had "version: 1", then HttpPluginServer will (after
the cleanup) start to monitor the containing folder, waiting until a
config file with the same name is available again to resume its
operation.
\note
A later call to HttpPluginServer::setConfig can be used to stop the
monitoring.
\note
If the containing dir is also erased, HttpPluginServer can do nothing
and the monitoring will stop.
我自己翻译一下:
HttpPluginServer
行为一个简单的用例来描述
HttpPluginServer
是怎样对下述变化起反应的:
- 你从一个使用默认构造的
HttpPluginServer
开始- 你调用了
setConfig(<一个不存在的文件>)
HttpPluginServer
无法找到此文件HttpPluginServer::setConfig
函数返回false
HttpPluginServer
对象保持之前的状态- 你调用了
setConfig(<一个无效的文件>)
HttpPluginServer
开始监控这个配置文件HttpPluginServer::setConfig
函数返回true
HttpPluginServer
读取了这个无效的文件,然后仍保持之前的状态- 你用一个有效的配置填充了这个配置文件
HttpPluginServer
对象加载这个新内容HttpPluginServer
尝试加载每个插件并填充路由。如果有插件没法被加载,它会忽略这个插件并通过qWarning
输出一个警告信息。如果你需要加载此插件,可对这个配置文件作出任何修改,随后HttpPluginServer
将会再次尝试- 你用一个无效的配置填充了这个配置文件
HttpPluginServer
对象看到并忽略了此次变更,继续保持之前的状态- 你移除了这个配置文件
HttpPluginServer
对象回到默认构造的状态下。
version: 0
如果最后的配置使用的是"version: 0
",这意味着之后不会再继续监控了(这也是默认构造状态的情况)version: 1
如果最后的配置使用的是"version: 0
",HttpPluginServer
将会(在这次移除后)开始监控这个曾包含配置文件的文件夹,直到监控到有一个同名的配置文件可以获得以再次继续它的操作。
注意:
随后调用一次HttpPluginServer::setConfig
可用于停止这种监控。
注意:
如果这个包含文件夹也被删除了,HttpPluginServer
没法做任何事,随后将会停止这种监控
在httppluginserver.h
第154行(文章更新日当前版本)开始则对routes.json
进行了规则解释。这里我也简单翻译一下。
原文:
The file format
===============
The configuration file format is json-based. If you aren't used to JSON,
read the [json specification](http://json.org/).
\note
The old Tufão 0.x releases used a file with the syntax based on the
QSettings ini format and forced you to use the _tufao-routes-editor_
application to edit this file.
The file must have a root json object with 3 attributes:
- _version_: It must indicate the version of the configuration file. The
list of acceptable values are:
- _0_: Version recognizable by Tufão 1.x, starting from 1.0
- _1_: Version recognizable by Tufão 1.x, starting from 1.2. The only
difference is the autoreloading behaviour. If you delete the config
file, Tufão will start to monitor the containing folder and resume the
normal operation as soon as the file is added to the folder again.
- _plugins_: This attribute stores metadata about the plugins. All plugins
specified here will be loaded, even if they aren't used in the request
router. The value of this field must be an array and each element of
this array must be an object with the following attributes:
- _name_: This is the name of the plugin and defines how you will refer
to this plugin later. You can't have two plugins with the same name.
This attribute is **required**.
- _path_: This is the path of the plugin in the filesystem. Relative
paths are supported, and are relative to the configuration file. This
attribute is **required**.
- _dependencies_: This field specifies a list of plugins that must be
loaded before this plugin. This plugin will be capable of access
plugins listed here. This attribute is **optional**.
- _customData_: It's a field whose value is converted to a QVariant and
passed to the plugin. It can be used to pass arbitrary data, like
application name or whatever. This attribute is **optional**.
- _requests_: This attribute stores metadata about the requests handled by
this object. The value of this field is an array and each element of
this array describes a handler and is an object with the following
attributes:
- _path_: Defines the regex pattern used to filter requests based on the
url's path component. The regex is processed through
QRegularExpression. This attribute is **required** and **must** be an
valid regex.
- _plugin_: Defines what plugin is used to handle request matching the
rules defined in this containing block. This attribute is **required**.
- _method_: Define what HTTP method is accepted by this handler. This
field is **optional** and, if it's not defined, it won't be used to
filter the requests.
译文:
文件格式
配置文件格式是基于JSON的。如果你没用过JSON,阅读json specification。
注意:旧版的Tufao(0.x release)使用了一个基于QSetting ini格式的文件,并强制要求你使用
tufao-routes-editor
应用来编辑此文件。这个文件必须包含一个有三个属性的根JSON对象:
version
:它必须标识这个配置文件的版本。可接纳的值列表如下:
0
:被Tufão 1.x
识别,自1.0
版本开始1
:被Tufão 1.x
识别,自1.2
版本开始。唯一的不同点就是自动加载行为。如果你删除了这个配置文件,Tufao将会开始监控曾包含配置文件的文件夹,一旦配置文件再次加入到这个文件夹中,Tufao就会重新开始一般地操作。plugins
:这个属性储存了关于插件们的元信息。所有在这里指定的插件均将被加载(即使它们并没有在请求路由中使用)。这个字段必须是一个数组,且其每一个元素都必须是包含以下属性的对象:
name
:这是此插件的名字,且定义了你一会儿将如何引用这个插件。你不能有两个名字一样的插件。这个属性是必需的。path
:这是此插件在文件系统中的路径。支持相对路径(相对于这个配置文件而言的相对路径)。这个属性是必需的。dependencies
:这个字段指定了一个必须在此插件前加载的插件列表。此插件将能够访问到列在此处的插件们。这个属性是可选的。customData
:这个字段的值会被转换成QVariant
并传递给此插件。它可以用于传递任意数据,比如应用名称或其他什么数据。这个属性是可选的。requests
:这个属性储存了关于被这个对象处理的请求们的元信息。这个字段的值是一个数组,而且这个数组的每个元素都描述了一个处理机且是一个包含下列属性的对象:
path
:定义一个基于URLpath
部分的正则表达式,用于筛选请求。这里的正则会被QRegularExpression
处理。这个属性是必需的,且必须是一个有效的正则表达式。plugin
:定义了哪个插件会被用于处理已适配被定义在这个包含块内的规则们的请求。这个属性是必需的。method
:定义了哪个HTTP方法是被这个处理机认可的。这个属性是可选的,且如果它没有被定义,它就不会用来筛选请求。
routes.json
的样例在源码httppluginserver.h
中也很全面:
{
version: 1,
plugins: [
{
name: "home",
path: "/home/vinipsmaker/Projetos/tufao-project42/build/plugins/libhome.so",
customData: {appName: "Hello World", root: "/"}
},
{
name: "user",
path: "show_user.so",
dependencies: ["home"]
},
{
name: "404",
path: "/usr/lib/tufao/plugins/notfound.so",
customData: "Not Found
I'm sorry, but it's your fault
"
}
],
requests: [
{
path: "^/$",
plugin: "home",
method: "GET"
},
{
path: "^/user/(\w*)$",
plugin: "user"
},
{
path: "",
plugin: "404"
}
]
}
可以看出例子给的是linux环境下的,插件的path
都是绝对路径下的.so
文件。在MSVC环境下,插件的path
需要填写.lib
文件的地址(当然,.dll
文件也需要和.lib
文件处于同一目录下。至于.pdb
文件如果不是Debug环境,就不要放进去了)。这个地址可以是相对配置文件路径
的相对路径。插件的name
可以与插件库文件的名字甚至是插件库文件里的主class
的名字都不同,其主要是用于在requests
部分区分插件用的。
基本上,懂得这些就可以做出基于tufao的web服务器了。
四,总结
作为轻量级的web服务器,tufao简洁易用的特性真是太棒了。不过,其性能到底如何,我并没有具体测试。以后,还是要对此做具体测试。