前言
在arm上做了Qt的應用程序,爲了在局域網實現web頁的訪問方式來配置arm上Qt的程序,局域網輕量級http服務器是很好的實現方式之一,有機會做國產麒麟上Qt的http服務器,正好接觸到了QtWebApp可以實現。
本篇實戰解說QtWebApp的輕量級Demo。
本篇篇幅較長,爲了保持基礎的完整性將必要的東西都放在本篇。
QtWepApp是一個C++中的HTTP服務器庫,其靈感來自Java Servlet。適用於Linux、Windows、Mac OS和Qt Framework支持的許多其他操作系統。
QtWebApp包含以下組件:
- HTTP(S)1.0和1.1服務器
- 模板引擎
- 緩衝記錄器
這些組件可以相互獨立地使用。一個非常小的用法示例:
// The main program starts the HTTP server
int main(int argc, char *argv[])
{
QCoreApplication app(argc,argv);
new HttpListener(
new QSettings("configfile.ini", QSettings::IniFormat, &app),
new MyRequestHandler(&app),
&app);
return app.exec();
}
// The request handler receives and responds HTTP requests
void MyRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
// Get a request parameters
QByteArray username=request.getParameter("username");
// Set a response header
response.setHeader("Content-Type", "text/html; charset=UTF-8");
// Generate the HTML document
response.write("<html><body>");
response.write("Hello ");
response.write(username);
response.write("</body></html>");
}
大約2MB的小內存需求使web服務器有資格用於嵌入式系統(PS:非常符合後續arm產品定位)。對於更大的網絡服務來說,它也足夠強大。
記錄器通過將調試消息保留在內存中直到出現錯誤來提高磁盤空間和性能。只要一切正常,就不會編寫調試消息。
對記錄器配置的更改將自動變爲活動狀態,而無需重新啓動程序。
該庫使用Qt 4.7至6.x版本運行。如果是Qt 6,則需要安裝Qt5Compat庫。它包含對許多8位字符編碼的支持,Qt6默認情況下不再支持這些編碼。可以在LGPL許可證的條件下使用該軟件。
作者項目的起源
多年前,一位經驗豐富的Java開發人員堅持認爲Java是互聯網語言,因爲在其他編程語言中進行互聯網通信要複雜得多。這不正確,但拒絕相信。所以開始挑戰。
任務:與Qt4相比,僅使用Java 6運行時庫對具有一些基本功能的HTTP服務器進行編程。
兩個項目都很相似,實現了任務。同時,Qt/C++程序比Java版本小得多,也快得多。
幾年後,用這個原型製作了一個庫,並將其用於一些私人項目。大學鼓勵發佈代碼。從那以後,再也不用這個項目了,但還是進行了一些改進,讓人們感到高興。
有趣的是,Qt製造商多年來一直在開發標準HTTP服務器,但到2022年,它仍然不包括在Qt庫中。這也許可以解釋爲什麼很多人使用的庫。
官方:http://www.stefanfrings.de/qtwebapp/QtWebApp.zip
鏈接:https://pan.baidu.com/s/1v9DTrajX8Mv-xnhScDhN8g?pwd=1234
使用Qt和QtWebApp在C++中開發HTTP Web服務器應用程序。必須已經瞭解C++和HTML的基本知識。
安裝好Qt,下載QtWebApp源碼;
安裝好Qt,下載QtWebApp源碼,然後對應不同linux安裝一些軟件如下:
- Debian, Ubuntu
sudo apt install build-essential gdb libgl1-mesa-dev
- Fedora, RedHat, CentOS sudo
yum groupDebian, Ubuntunstall "C Development Tools and Libraries"
sudo yum install mesa-libGL-devel
- openSUSE
sudo zypper install -t pattern devel_basis
爲的編程項目創建一個目錄,然後在那裏下載並提取QtWebApp ZIP文件。啓動QT Creator IDE並打開項目文件Demo1/Demo1.pro。這將打開“配置項目”對話框:
只需點擊突出顯示的“配置項目”按鈕即可。然後點擊左邊框中的綠色“Run”按鈕,構建並運行演示程序。當的計算機正忙時,可以通過單擊底部邊框中的相關按鈕來觀看“編譯輸出”窗格。如果一切正常,將打開一個黑色控制檯窗口,告訴演示應用程序正在使用哪個配置文件:
打開URL http://localhost:8080在web瀏覽器中檢查演示web服務器是否正常工作:
如果在屏幕上看到那個網站,所有需要的軟件都能正常工作。現在可以關閉演示應用程序。
如果曾經使用Java Servlet API開發過web服務器應用程序,會感覺像在家一樣。的庫提供了幾乎相同的功能。將向展示如何使用QtWebApp編寫一個最小的web服務器應用程序。
提取編程文件夾中的QtWebApp.zip文件,並創建一個名爲“MyFirstWebApp”的新Qt控制檯項目(如果尚未完成)。然後,應該擁有與相同的文件夾結構:
將以下行添加到MyFirstWebApp項目的項目文件中:
QT += network
include(../QtWebApp/QtWebApp/httpserver/httpserver.pri)
第一行激活Qt的網絡模塊,第二行包括QtWebApp的HTTP服務器模塊的源代碼。因此,當編譯程序時,HTTP服務器將成爲可執行文件的一部分。
作爲替代方案,可以使用共享庫。要生成它,請打開項目QtWebApp/QtWebApp-QtWebApp.pro並構建它。然後查看QtWebApp/Demo2/Demo2.pro,瞭解如何鏈接到共享庫。然而,建議包含如上所示的源代碼,因爲這樣不太容易出錯。
下一步是創建配置文件MyFirstWebApp/etc/webapp1.ini。需要使用操作系統的文件管理器來執行此操作,因爲Qt Creator無法創建新文件夾。文件內容爲:
[listener]
;host=192.168.0.100
port=8080
minThreads=4
maxThreads=100
cleanupInterval=60000
readTimeout=60000
maxRequestSize=16000
maxMultiPartSize=10000000
主機和端口參數指定web服務器偵聽的IP地址和端口。如果註釋掉主機(如上所述),則服務器將偵聽所有網絡接口。公共web服務器使用端口80,而內部web服務器通常在端口8080上偵聽。可以使用任何喜歡的自由端口。
Unix用戶應該記住,1024以下的端口號是爲“root”用戶保留的。Windows用戶可能需要配置Windows防火牆以允許從其他計算機進行訪問。
QtWebApp可以同時處理多個HTTP請求,因此它是多線程的。由於啓動一個新線程需要花費大量時間,QtWebApp會將線程重新用於後續的HTTP請求。
maxThreads值指定併發工作線程的最大數量。在進入生產環境之前,應該使用負載生成器工具來了解服務器在不耗盡內存或變得遲緩的情況下可以處理多少負載。
minThreads空閒時併發工作線程的最小數量。web服務器總是以一個空線程池開始。線程是在HTTP請求傳入時按需創建的。空閒線程由計時器緩慢關閉。每個cleanupInterval(以毫秒爲單位),服務器都會關閉一個空閒線程,但是minThreads的數量總是保持運行。使用給定的值,的服務器最多可以處理100個併發HTTP連接。它保持4個空閒的工作線程運行,以確保良好的響應時間。
readTimeout設置通過打開大量連接而不使用這些連接來保護服務器免受簡單的拒絕服務攻擊的超時時間。空閒連接在該毫秒數之後關閉。在正常情況下,網絡瀏覽器負責關閉連接。
maxRequestSize保護服務器不受非常大的HTTP請求造成的內存過載的影響。此值適用於常規請求。
maxMultiPartSize值適用於web瀏覽器將文件上載到服務器時發生的多部件請求。如果想接收10兆字節的文件,由於HTTP協議開銷,必須將此值設置得稍大一些。
上傳的文件存儲在臨時文件中。臨時文件夾的位置由操作系統定義。
繼續創建的第一個web應用程序。要使此配置文件在Qt Creator中可見,請在項目文件中添加一行:
OTHER_FILES += etc/webapp1.ini
現在添加一些代碼來加載該文件:
#include <QCoreApplication>
#include <QSettings>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QSettings* listenerSettings=
new QSettings("/home/sfrings/programming/MyFirstWebApp/etc/webapp1.ini",QSettings::IniFormat,&app);
qDebug("config file loaded");
return app.exec();
}
但更喜歡在幾個文件夾中自動搜索配置文件,這樣就可以在IDE內外運行應用程序,而無需更改路徑:
#include <QCoreApplication>
#include <QSettings>
#include <QFile>
#include <QDir>
#include <QString>
/**
* Search the configuration file.
* Aborts the application if not found.
* @return The valid filename
*/
QString searchConfigFile() {
QString binDir=QCoreApplication::applicationDirPath();
QString appName=QCoreApplication::applicationName();
QString fileName("Demo1.ini");
QStringList searchList;
searchList.append(binDir);
searchList.append(binDir+"/etc");
searchList.append(binDir+"/../etc");
searchList.append(binDir+"/../"+appName+"/etc"); // for development with shadow build (Linux)
searchList.append(binDir+"/../../"+appName+"/etc"); // for development with shadow build (Windows)
searchList.append(QDir::rootPath()+"etc/opt");
searchList.append(QDir::rootPath()+"etc");
foreach (QString dir, searchList)
{
QFile file(dir+"/"+fileName);
if (file.exists())
{
fileName=QDir(file.fileName()).canonicalPath();
qDebug("Using config file %s",qPrintable(fileName));
return fileName;
}
}
// not found
foreach (QString dir, searchList)
{
qWarning("%s/%s not found",qPrintable(dir),qPrintable(fileName));
}
qFatal("Cannot find config file %s",qPrintable(fileName));
return nullptr;
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// Load the configuration file
QString configFileName=searchConfigFile();
QSettings* listenerSettings=new QSettings(configFileName, QSettings::IniFormat, &app);
qDebug("config file loaded");
return app.exec();
}
過程searchConfigFile()在多個文件夾中搜索文件。
方法**QDir::canonicalPath()將相對路徑名轉換爲絕對形式,這在下面的調試消息中看起來更好。
如果找不到該文件,則應用程序會輸出一條帶有qFatal()**的錯誤消息,這也會中止程序。
一旦加載了配置文件,就可以創建一個HTTP偵聽器對象,它是web服務器的核心:
#include <QCoreApplication>
#include <QSettings>
#include <QFile>
#include <QDir>
#include <QString>
#include "httplistener.h"
#include "httprequesthandler.h"
using namespace stefanfrings;
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
// Load the configuration file
QString configFileName=searchConfigFile();
QSettings* listenerSettings=new QSettings(configFileName, QSettings::IniFormat, &app);
listenerSettings->beginGroup("listener");
// Start the HTTP server
new HttpListener(listenerSettings, new HttpRequestHandler(&app), &app);
return app.exec();
}
方法**QSettings::beginGroup()**從配置文件中選擇組“[listener]”。稍後將添加更多組。
HttpRequestHandler接收所有傳入的HTTP請求,並生成響應。默認情況下,請求處理程序只返回一個錯誤頁面。
在堆上用“new”創建HttpListener是很重要的,否則它將在程序啓動後立即終止。
運行程序並打開URLhttp://localhost:8080在web瀏覽器中。將在控制檯窗口中收到錯誤頁面“501未實現”和調試消息。
這是很多會減慢程序速度的消息,但它們對調試很有幫助。在Qt Creator的左邊框中,可以通過單擊紫色按鈕將構建模式從“調試”更改爲“發佈”。發佈版本不那麼冗長:
因此,對於生產,應該更喜歡發佈版本。
爲了輸出“Hello World”消息,必須編寫自己的請求處理程序。用鼠標右鍵單擊src文件夾,選擇“添加新…”,然後選擇“C++類”。
#ifndef HELLOWORLDCONTROLLER_H
#define HELLOWORLDCONTROLLER_H
#include "httprequesthandler.h"
using namespace stefanfrings;
class HelloWorldController : public HttpRequestHandler {
Q_OBJECT
public:
HelloWorldController(QObject* parent=0);
void service(HttpRequest& request, HttpResponse& response);
};
#endif // HELLOWORLDCONTROLLER_H
#include "helloworldcontroller.h"
HelloWorldController::HelloWorldController(QObject* parent)
: HttpRequestHandler(parent) {
// empty
}
void HelloWorldController::service(HttpRequest &request, HttpResponse &response) {
response.write("Hello World",true);
}
可選參數“true”表示這是當前HTTP請求的最後一次write()調用。
main.cpp中的兩個更改:
#include "helloworldcontroller.h"
new HttpListener(listenerSettings,new HelloWorldController(&app),&app);
運行程序並打開URLhttp://localhost:8080在網絡瀏覽器中。
因爲http很多時候是放在一個Qt界面裏面,所以搭建的是QWidget工程模板,非控制檯,有需要自行切換下。
略;
將QtWebApp中的httpserver,符合模塊化設計準則,如下圖:
添加模塊進入工程:
httpserver模塊,QtWebApp自帶的三方模塊
# httpserver模塊,QtWebApp自帶的三方模塊
include ($$PWD/modules/httpserver/httpserver.pri)
第三方的模塊。
再建立一個http管理類來處理,如下:
再建立基本配置:
至此,基礎模塊搭建好,下面需要開始寫http的消息處理過程。
繼承HttpRequestHandler消息處理類,開始新建一個類:
引入頭文件,命名空間,做一些基礎處理:
然後要實現service服務接口:
如下圖:
這裏已經將http的輕量級服務器已經子線程模塊化融入帶界面的qt應用中(帶不帶界面融入過程都一樣,只是QApplication和QCoreApplication以及在哪初始化的問題了)
測試127.0.0.1移植連接補上,查看 “入坑二”。
然後測試打開成功:
這裏有個字符編碼的問題也要同時解決一下,一般來說都是utf-8,所以要字符編碼修改一下。
我們忽略系統編碼,統一進行utf-8進行轉換,避免因爲系統問題而去單獨處理這個問題:
然後測試網頁:
除了日誌,沒發現三方模塊中有是否監聽成功的反饋,所以日誌就顯得很重要,日誌在下一篇再融於進來
至此,一個基礎子線程模塊化的http服務的qt界面應用Demo就完成了。
#ifndef HTTPSERVERMANAGER_H
#define HTTPSERVERMANAGER_H
#include <QObject>
#include <QMutex>
#include "httplistener.h"
#include "HelloWorldRequestHandler.h"
class HttpServerManager : public QObject
{
Q_OBJECT
private:
explicit HttpServerManager(QObject *parent = 0);
public:
static HttpServerManager *getInstance();
public:
QString getIp() const; // 服務器監聽ip(若爲空,則表示監聽所有ip
quint16 getPort() const; // 服務器監聽端口
int getMinThreads() const; // 空閒最小線程數
int getMaxThreads() const; // 負載最大線程數
int getCleanupInterval() const; // 空線程清空間隔(單位:毫秒)
int getReadTimeout() const; // 保持連接空載超時時間(單位:毫秒)
int getMaxRequestSize() const; // 最大請求數
int getMaxMultiPartSize() const; // 上載文件最大數(單位:字節)
public:
void setIp(const QString &ip); // 服務器監聽ip(若爲空,則表示監聽所有ip
void setPort(const quint16 &port); // 服務器監聽端口
void setMinThreads(int minThreads); // 空閒最小線程數
void setMaxThreads(int maxThreads); // 負載最大線程數
void setCleanupInterval(int cleanupInterval); // 空線程清空間隔(單位:毫秒)
void setReadTimeout(int readTimeout); // 保持連接空載超時時間(單位:毫秒)
void setMaxRequestSize(int value); // 最大請求數
void setMaxMultiPartSize(int value); // 上載文件最大數(單位:字節)
public slots:
void slot_start();
void slot_stop();
private:
static HttpServerManager *_pInstance;
static QMutex _mutex;
private:
bool _running;
private:
HttpListener *_pHttpListener; // http服務監聽器
QSettings *_pSettings; // 配置文件
private:
QString _ip; // 服務器監聽ip(若爲空,則表示監聽所有ip)
quint16 _port; // 服務器監聽端口
int _minThreads; // 空閒最小線程數
int _maxThreads; // 負載最大線程數
int _cleanupInterval; // 空線程清空間隔(單位:毫秒)
int _readTimeout; // 保持連接空載超時時間(單位:毫秒)
int _maxRequestSize; // 最大請求數
int _maxMultiPartSize; // 上載文件最大數(單位:字節)
};
#endif // HTTPSERVERMANAGER_H
#include "HttpServerManager.h"
#include <QApplication>
#include <QDebug>
#include <QDateTime>
//#define LOG qDebug()<<__FILE__<<__LINE__
//#define LOG qDebug()<<__FILE__<<__LINE__<<__FUNCTION__
//#define LOG qDebug()<<__FILE__<<__LINE__<<QThread()::currentThread()
//#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd")
#define LOG qDebug()<<__FILE__<<__LINE__<<QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss:zzz")
HttpServerManager *HttpServerManager::_pInstance = 0;
QMutex HttpServerManager::_mutex;
HttpServerManager::HttpServerManager(QObject *parent)
: QObject(parent),
_pHttpListener(0),
_pSettings(0),
_running(false),
_port(8088),
_minThreads(2),
_maxThreads(10),
_cleanupInterval(60000),
_readTimeout(60000),
_maxRequestSize(100),
_maxMultiPartSize(1024*1024*1024)
{
}
HttpServerManager *HttpServerManager::getInstance()
{
if(!_pInstance)
{
QMutexLocker lock(&_mutex);
if(!_pInstance)
{
_pInstance = new HttpServerManager();
}
}
return _pInstance;
}
void HttpServerManager::slot_start()
{
if(_running)
{
LOG << "It's running!!!";
return;
}
_running = true;
LOG << "Succeed to run";
// 啓動http的監聽
{
QString httpServerPath = QString("%1/etc/httpServer.ini").arg(qApp->applicationDirPath());
if(!_pSettings)
{
LOG << httpServerPath << "exit:" << QFile::exists(httpServerPath);
_pSettings = new QSettings(httpServerPath, QSettings::IniFormat);
}
#if 0
if(!_ip.isEmpty())
{
_pSettings->setValue("host" , _ip); // ;在ini裏面是註釋了
}
_pSettings->setValue("port" , _port);
_pSettings