用C++Qt 与libfcgi快速开发后台 WebService

栏目: 编程工具 · 发布时间: 7年前

内容简介:用C++Qt 与libfcgi快速开发后台 WebService

在与APP接口的后台WebService开发方面,估计很少有人直接使用C接口的libfcgi-dev进行开发的了。但是,这不代表此方法是不可行的。在强大的Qt库的支持下,原来使用C++开发webService也是非常方便的。这里我们以获取OpenStreetMap数据库中的地理信息为例子,看看现代C++的威力。

1 需求

我们有一个OpenStreetMap瓦片服务器数据库,现在希望在提供瓦片服务的基础上,提供根据地理位置获取附近物体、根据物体名称查询位置、根据地理位置获取高程海拔等功能,输出采用JSON格式。

数据库是这样的:

1、瓦片服务器位于postgresql 数据库gis里,包括四个表,planet_osm_line, planet_osm_point, planet_osm_polygon与planet_osm_roads;

2、高程数据位于postgresql数据库contour里,包括陆地海拔等高线 planet_osm_line 表、海洋深度等值线 contour表。

希望提供简单的URL接口(?&传值),输出JSON格式的数据。我们直接在OpenStreetMap宿主服务器上开发,操作系统为ArchLinux,工具链为 apache2 + libfcgi + Qt5

2 FCGI框架搭建

我们希望在几个独立的线程中响应用户的请求。因此,采用异步FCGI模式,设计几个QThread派生类的对象负责具体的事物处理。

2.1 Qt-Pro文件

Qt的工程文件如下,为控制台程序。

QT += core sql
QT -= gui
CONFIG += c++11
TARGET = query_osm.fcgi
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp \
    listenthread.cpp
LIBS += -lfcgi
HEADERS += \
    listenthread.h

工程总共就3个问价,一个主函数入口main.cpp,外加事物线程类listenthread的声明与实现。

2.2 主函数

主函数负责启动事物线程,并等待程序结束:

#include <QCoreApplication>
#include <QList>
#include <fcgi_stdio.h>
#include "listenthread.h"
using namespace std;
const int thread_count = 4; //根据电脑性能自行调整
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    //初始化CGI库
    FCGX_Init();
    //初始化事物线程
    QList<listenThread *> threadpool;
    for (int i=0;i<thread_count;++i)
        threadpool.push_back(new listenThread(&a));
    //开始处理事物
    foreach (listenThread * t, threadpool)
        t->start();
    //循环、等待结束。
    int alives = 0;
    do
    {
        alives = 0;//统计目前仍然活跃的事物线程
        foreach (listenThread * t, threadpool)
        {
            if (t->isRunning())
            {
                ++alives;
                t->wait(200);
            }
            else
                QThread::msleep(200);
            a.processEvents();//维护主线程消息循环
        }
    }while (alives>0);
    a.quit();
    return 0;
}

2.3 事物线程基本结构

事物线程为一个QThread派生类,声明listenthread.h如下:

#ifndef LISTENTHREAD_H
#define LISTENTHREAD_H
#include <QHash>
#include <QThread>
#include <QJsonObject>
#include <functional>
#include <fcgi_stdio.h>
class listenThread : public QThread
{
    Q_OBJECT
public:
    explicit listenThread(QObject *parent = 0);
private:
    QHash <QString, std::function< void (const QHash < QString, QString> query_paras, QJsonObject & jsonObj) > >
    m_functions;
    QString m_threadDBName;
protected:
    void run();
    void deal_client(FCGX_Request * request);
    //各个功能处理函数
    void func_help(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );
};

#endif // LISTENTHREAD_H

说明:

1. 我们的一个fcgi入口可以提供很多种功能,这个框架仅包含一个“帮助”功能。

2. 每增加一个功能,只要增加一个功能处理函数即可。

3. 功能处理函数的接口有两个。一个是输入的变量query_paras,代表了用户URL里包含的内容。另一个是输出变量 jsonObj ,用于存储输出的内容。

4. 成员变量 m_threadDBName 用来存储和线程对应的数据库连接名称。Qt中,每个线程必须使用自己的数据库连接。

该类的实现如下:

#include "listenthread.h"
#include <QByteArray>
#include <QJsonArray>
#include <QJsonDocument>
#include <QRegExp>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QSqlError>
#include <QUrl>
#include <QVariant>
listenThread::listenThread(QObject *parent)
    : QThread(parent)
    , m_threadDBName(QString("TDB%1").arg((quint64(this))))
{
    //注册方法,这里是“帮助”方法
    m_functions["help"] = std::bind(&listenThread::func_help,this,std::placeholders::_1,std::placeholders::_2);
}
//重载的QThread::run(),用于事务处理的总接口 
void listenThread::run()
{
    //1.连接数据库,这里是一个,可以多个。
    {
        QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL",m_threadDBName+"_gis");
        if (db.isValid()==false)
            return;
        db.setHostName("127.0.0.1");
        db.setPort(5432);
        db.setUserName("XXX");
        db.setPassword("XXX");
        db.setDatabaseName("gis");
        if (db.open()==false)
            return;
    }
    //2.开始不断接受请求
    FCGX_Request request;
    FCGX_InitRequest(&request, 0, 0);
    int rc = FCGX_Accept_r(&request);
    while (rc >=0)
    {
        //2.1调用处理客户端的方法listenThread::deal_client
        deal_client(&request);
        FCGX_Finish_r(&request);
        rc = FCGX_Accept_r(&request);
    }
    //3.退出
    QSqlDatabase::removeDatabase(m_threadDBName+"_gis");
    QSqlDatabase::removeDatabase(m_threadDBName+"_contours");
    quit();
}

//具体处理客户端的逻辑, 这个函数无需改动
void listenThread::deal_client(FCGX_Request * request)
{
    QHash < QString, QString> query_paras;
    //1. 获得输入变量,存储在 FCGX_Request 里
    const char * const query_string=FCGX_GetParam("QUERY_STRING",request->envp);
    QString str = QString::fromUtf8(query_string) ;
    QStringList lst = str.split("&",QString::SkipEmptyParts);
    //2. 生成输入变量字典
    foreach (QString pai, lst)
    {
        int pd = pai.indexOf("=");
        if (pd>0 && pd < pai.length())
        {
            QString key = pai.left(pd);
            QString v = pai.mid(pd+1);
            query_paras[key.trimmed()]  = v;
        }
    }
    //3. 获得公共变量
    //3.1. indented参数控制结果显示时,是否缩进Json
    const bool bJsonIndented = query_paras["indented"].toInt()?true:false;
    //3.2. function参数指定具体功能
    const QString functionStr = query_paras["function"];
    //4. 生成结果对象
    QJsonObject root;
    //4.1 根据功能继续操作,查找给定的具体功能有没有对应的接口,有的话就调用
    if (m_functions.contains(functionStr))
        m_functions[functionStr](query_paras,root);
    //4.2 没有的话显示帮助
    else
        func_help(query_paras,root);
    //5. 输出到客户端
    QJsonDocument doc(root);
    QByteArray arrJson = doc.toJson(bJsonIndented?QJsonDocument::Indented:QJsonDocument::Compact);
    //Output
    FCGX_PutS("Content-type: text/plain; charset=UTF-8\n\n",request->out);
    FCGX_PutStr(arrJson.constData(),arrJson.length(),request->out);
}
//框架提供的帮助接口
void listenThread::func_help(const QHash < QString, QString> query_paras, QJsonObject & jsonObj )
{
    foreach (QString s, query_paras.keys())
        jsonObj[s] = query_paras[s];
    jsonObj["usage"] = "Please put your help message here.";
}

说明:

1. 核心思想是使用了std::functional 的函数绑定,使得接口可以存储在字典中,便于扩展。

2. 该框架理论上可以扩展任意功能。增加新的功能只需要三步:

(1) 在头文件里添加一个接口处理入口

void func_foo(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );

(2) 在CPP里注册接口

m_functions["foo"] = std::bind(&listenThread::func_foo,this,std::placeholders::_1,std::placeholders::_2);

(3) 实现具体功能

void listenThread::func_foo(const QHash < QString, QString> query_paras, QJsonObject & jsonObj )
{

}

2.4 测试调用效果

把fcgi拷贝到apache文件夹下,

$sudo systemctl  restart httpd
$sudo cp ./query_osm.fcgi /var/www/html/cgi-bin

而后访问

http://192.168.1.10:8088/cgi-bin/query_osm.fcgi?function=help&indented=1

返回:

{
    "function": "help",
    "indented": "1",
    "usage": "Please put your help message here."
}

3 具体实现功能

有了框架,我们来具体实现三个功能。

3.1 增加接口声明

我们向listenthread.h增加三个接口,分别为altitude、object_by_pos, object_by_name

//各个功能函数
    void func_help(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );
    //新增的
    void func_altitude(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );
    void func_object_by_pos(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );
    void func_object_by_name(const QHash < QString, QString> query_paras, QJsonObject & jsonObj );

3.2 注册接口

我们在listenthread.cpp中注册接口:

//注册方法
    m_functions["help"] = std::bind(&listenThread::func_help,this,std::placeholders::_1,std::placeholders::_2);
    //新增加的
    m_functions["altitude"] = std::bind(&listenThread::func_altitude,this,std::placeholders::_1,std::placeholders::_2);
    m_functions["object_by_pos"] = std::bind(&listenThread::func_object_by_pos,this,std::placeholders::_1,std::placeholders::_2);
    m_functions["object_by_name"] = std::bind(&listenThread::func_object_by_name,this,std::placeholders::_1,std::placeholders::_2);

3.3 实现接口

以func_object_by_name为例:

void listenThread::func_object_by_name(const QHash < QString, QString> query_paras, QJsonObject & jsonObj )
{
    //1. 首先产生运行结果字段
    jsonObj["result"] = "error";
    //2. 把输入参数原本不懂地作为输出
    foreach (QString s, query_paras.keys())
        jsonObj[s] = query_paras[s];
    //3. 检查是否给定了待查名称字段"name"
    if (query_paras.contains("name")==false)
    {
        jsonObj["reason"] = "need name element.";
        return;
    }
    //4. 获得待查字段,如果有中文,会是封装格式(%Hex),直接调用QUrl解码
    QString rawnamestr = jsonObj["name"].toString();
    jsonObj["raw_name"] = rawnamestr;
    QUrl url(rawnamestr);
    QString namestring = url.toDisplayString();
    //5. 清除非法字符,防止注入
    namestring.remove(QRegExp("[\\pP‘’“”,\\+\\-()[\\]\\^%~`\\!]"));
    namestring = namestring.trimmed();
    jsonObj["name"] = namestring;
    //6. 长度限制
    if (namestring.length()<2)
    {
        jsonObj["reason"] = "name must contain more than 1 characters.";
        return;
    }
    //7.开始查询数据库
    QSqlDatabase db = QSqlDatabase::database(m_threadDBName+"_gis");
    if (db.isOpen()==false)
    {
        jsonObj["reason"] = "Database connection is not ok.";
        jsonObj["error_msg"] = db.lastError().text();
        return;
    }
    //7.1 生成Sql
    QSqlQuery query(db);
    QString str = QString("select * from ... where name like '%1%%';")
                        .arg(namestring);
    //7.2执行
    if (query.exec(str)==false)
    {
        jsonObj["reason"] = "database query error.";
        jsonObj["error_msg"] = query.lastError().text();
        return;
    }
    //7.3 返回结果,直接利用数据库字段名作为json键
    int nItems = 0;
    while (query.next())
    {
        QJsonObject objitem;
        int cols = query.record().count();
        for (int i=0;i<cols;++i)
            objitem[query.record().fieldName(i)] = query.value(i).toString();
        jsonObj[QString("result%1").arg(nItems)] = objitem;
        ++nItems;
    }
    //8.返回总结果数。
    jsonObj["items"] = nItems;
    jsonObj["result"] = "succeeded";

}

3.4 测试接口

输入

http://192.168.1.10:8088/cgi-bin/query_osm.fcgi?function=object_by_name&name=%E4%B8%AD%E5%9B%BD%E5%9C%B0%E8%B4%A8%E5%A4%A7%E5%AD%A6&indented=1

输出

{
    "function": "object_by_name",
    "indented": "1",
    "items": 17,
    "name": "中国地质大学",
    "raw_name": "%E4%B8%AD%E5%9B%BD%E5%9C%B0%E8%B4%A8%E5%A4%A7%E5%AD%A6",
    "result": "succeeded",
    "result0": {
        "center_pos": "POINT(113.940106140169 22.5320149618608)",
        "geotype": "ST_Polygon",
        "name": "中国地质大学产学研基地",
        "osm_id": "220880942",
        "trans_name_chs": ""
    },
    "result1": {
        "center_pos": "POINT(116.33961555325 39.9909730291633)",
        "geotype": "ST_Polygon",
        "name": "中国地质大学校医院",
        "osm_id": "436059504",
        "trans_name_chs": ""
    },
    ...
    "result17": {
        "center_pos": "POINT(114.251718450378 30.5881765656673)",
        "geotype": "ST_Polygon",
        "name": "中国地质大学(汉口校区)",
        "osm_id": "132730572",
        "trans_name_chs": ""
    }
}

4 体会

其实,webService 可以理解为基于字符串的输出输出处理。理论上,只要一种语言的字符串处理能力很强,就适合做WebService。

以前,C++/FCGI比较麻烦,是因为C++本身的字符串处理实在有点那啥,而C++的JSON类也良莠不齐。

不过,有了Qt后,C++对字符串、JSON、XML可就不瘸腿啦!主要有:

1. 正则表达式与QString的原生契合;

2. QUrl以及內建的QLocale对字符编码的转换;

3. QByteArray及Base-64编码解码;

4. QJson基于类似map的键值操作、无限嵌套;

5. QJson\QVariant的“伪”动态语言特性,使得对类型转换有了保证;

6. Qt库的强大能力,包括对硬件、媒体的控制,使得Webservice可以完成几乎所有的事情!

有了这些,再加上现代C++的functional/bind特性,使得可以一劳永逸的制作WebService接口框架了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithms

Data Structures and Algorithms

Alfred V. Aho、Jeffrey D. Ullman、John E. Hopcroft / Addison Wesley / 1983-1-11 / USD 74.20

The authors' treatment of data structures in Data Structures and Algorithms is unified by an informal notion of "abstract data types," allowing readers to compare different implementations of the same......一起来看看 《Data Structures and Algorithms》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试