1. 程式人生 > >用C++Qt 與libfcgi快速開發後臺 WebService

用C++Qt 與libfcgi快速開發後臺 WebService

在與APP介面的後臺WebService開發方面,估計很少有人直接使用C介面的libfcgi-dev進行開發的了。但是,這不代表此方法是不可行的。在強大的Qt庫的支援下,原來使用C++開發webService也是非常方便的。這裡我們以獲取OpenStreetMap資料庫中的地理資訊為例子,看看現代C++的威力。
專案地址:
https://code.csdn.net/goldenhawking/query_osm/tree/master

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介面框架了。