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