Tufão——QT轻量级Web服务器库使用笔记

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所在的debugrelease文件夹,而是.exe所在目录的上一级。插件系统所需的routes.json,如果也使用相对路径(相对于应用所在目录的相对路径)的话,也必须注意这一点。(如果你对插件加载实在摸不着头脑,可以通过qInstallMessageHandlerqWarning消息输出到外部文件里,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是怎样对下述变化起反应的:

  1. 你从一个使用默认构造的HttpPluginServer开始
  2. 你调用了setConfig(<一个不存在的文件>)
    1. HttpPluginServer无法找到此文件
    2. HttpPluginServer::setConfig函数返回false
    3. HttpPluginServer对象保持之前的状态
  3. 你调用了setConfig(<一个无效的文件>)
    1. HttpPluginServer开始监控这个配置文件
    2. HttpPluginServer::setConfig函数返回true
    3. HttpPluginServer读取了这个无效的文件,然后仍保持之前的状态
  4. 你用一个有效的配置填充了这个配置文件
    1. HttpPluginServer对象加载这个新内容
    2. HttpPluginServer尝试加载每个插件并填充路由。如果有插件没法被加载,它会忽略这个插件并通过qWarning输出一个警告信息。如果你需要加载此插件,可对这个配置文件作出任何修改,随后HttpPluginServer将会再次尝试
  5. 你用一个无效的配置填充了这个配置文件
    1. HttpPluginServer对象看到并忽略了此次变更,继续保持之前的状态
  6. 你移除了这个配置文件
    1. 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简洁易用的特性真是太棒了。不过,其性能到底如何,我并没有具体测试。以后,还是要对此做具体测试。

你可能感兴趣的:(Tufão——QT轻量级Web服务器库使用笔记)