全志H3 - Qt&QtWebApp搭建Http Server(无X11系统)
目前我手里正好有一块NanoPi M1
加法板,NanoPi M1
(以下简称M1
)是友善之臂团队面向创客、嵌入式爱好者,电子艺术家、发烧友等群体推出的又一款完全开源的掌上创客神器,它的大小只有树莓派的大约2/3
,可运行Debian
、Ubuntu-MATE
、Ubuntu-Core
、Android
等操作系统。
NanoPi M1
采用了全志高性能处理器Allwinner H3
,集成以太网、红外接收、视频/音频输出等接口,支持HDMI
、AVOUT
视频输出等功能。
尽管体积很小,设计却紧凑美观。NanoPi M1
引出了相当丰富的接口,包括HDMI
、以太网、USB-Host
、USB-OTG
、DVP camera
和AVOUT
(音频+视频)等。而且集成了板载麦克风,红外接收器,并且兼容树莓派GPIO
口,并且拥有独立的调试串口等。
本机的目的主要是在当前系统上编写Qt
应用程序,因此不会过多的深入了解有关系统底层的知识,更多的内容可以参考《 NanoPi M1/zh
》。
一、目的
本次实现使用的这款开发板已经烧录了:FriendlyCore
系统固件,基于Ubuntu core 22.04
构建,内置Qt4.8
,但是该系统并没有X11
桌面环境。
这里我们需要在该系统上开发一款Qt
应用程序,通过LCD
显示屏显示车牌识别的结果。车牌识别的结果可以从车牌识别系统获取到,而我们要做的仅仅是在显示屏上显示识别到的车牌信息。
1.1 车牌识别系统
车牌识别系统识别到的信息包含以下几种:
1. 入口静止 | 2. 出口静止 | 3. 入口忙碌 | 4. 出口忙碌,不收费 | 5. 出口忙收费 |
---|---|---|---|---|
欢迎光临 | 一路平安 | 粤B12345 | 粤B12345 | 粤B12345 |
logo | logo | 临时车/欢迎光临,剩余8/天/欢迎光临 | 临时车 | 停车:1小时3分钟 |
余位显示 | 余位 | 停车3小时5分钟 | 二维码 | |
一路平安 | ¥100元 | |||
语音 | 语音 | 语音 |
车牌识别系统支持的通信方式有多种:
HTTP
接口:http://IP:8080/API
,数据格式JSON
;UDP
端口6666,协议格式JSON
;MQTT
通讯(互联网,云平台对接);TCP
客户端模式,服务端模式均支持;485
通讯;
1.2 协议内容
无论采用哪种通信方式,车牌识别系统发送的协议内容是固定的,具体如下:
字段 | 说明 | 类型 | 类型 |
---|---|---|---|
requestid | 消息 ID,会返回响应 | String | 是 |
servicename | 业务名称 | String | 是 |
data | 具体的业务 json | json | 是 |
sign | MD5 签名 | 是 |
业务servicename
种类有多种:
welcome
:欢迎光临;payinfo
:缴费二维码;parkNuminfo
:余位显示;byebye
:一路平安;noPass
:禁止通行;waitInfo
:人工确认;volume
:音量;DisplayVoice
: 简化指令;advLoadImage
:广告下载,文件方式(速度较慢),URL 方式(简单易用);cmd485
:485 数据包;UpdateTime
:更新时间;
这里我们要做的就是在显示屏上显示servicename
为welcome
和advLoadImage
的内容。
1.2.1 welcome
字段 | 说明 | 类型 | 必须 |
---|---|---|---|
carmark | 车牌 | String | 是 |
line1 | 欢迎回家 | string | |
line2 | 临时车 | int | |
line3 | 请入场停车 | ||
voice | 粤 B12345,临时车,欢迎光临 | String | |
companyName | 深圳 XX 科技有限公司 | ||
pictureUsed | 0~4,对应servicename 为advLoadImage 业务发送的4张背景图 |
int |
如果显示屏字体,颜色大小不合适,可以增加以下字段调整:
字段 | 说明 | 类型 | 必须 |
---|---|---|---|
line1Size | 字体大小 | int | 否,0 为默认值,其是按大小的 |
line2Size | 字体大小 | int | 否,0 为默认值,其是按大小的 |
line3Size | 字体大小 | int | 否,0 为默认值,其是按大小的 |
carmarkSize | 字体大小 | int | 否,0 为默认值,其是按大小的 |
companyNameSize | 字体大小 | int | 否,0 为默认值,其是按大小的 |
carmarkColor | 颜色 | String | #FFFFFF |
line1Color | 颜色 | String | #FFFFFF |
line2Color | 颜色 | String | #FFFFFF |
line3Color | 颜色 | String | #FFFFFF |
companyNameColor | 颜色 | String | #FFFFFF |
示例1:
{
"requestid": "bff09428-6200-42e0-b7b5-560c5a8f4fdd",
"servicename": "welcome",
"data": {
"carmark": "粤 B12345",
"line1": "欢迎光临",
"line2": "临时车",
"line3": "请入场停车",
"voice": "粤 B12345,临时车,欢迎光临",
"companyName": "深圳市 XX 科技有限公司",
"pictureUsed": "0"
}
}
示例2:
{
"requestid": "65dc4092d6c5f",
"servicename": "welcome",
"data": {
"carmark": "Welcome",
"line1": "",
"line2": "LCD002",
"line3": "",
"voice": "",
"companyName": "JH-LOCAL-PARK",
"line1Size": "118",
"line2Size": "88",
"line3Size": "88",
"carmarkSize": "88",
"companyNameSize": 118,
"carmarkColor": "#FFFFFF",
"line1Color": "#FFFFFF",
"line2Color": "#FFFFFF",
"line3Color": "#FFFFFF",
"companyNameColor": "#FFFFFF",
"pictureUsed": "0"
}
}
1.2.2 advLoadImage
字段 | 说明 | 类型 | 必须 |
---|---|---|---|
ImageName | 广告图片名 | String | 文件下载方式必填 |
ImageUrl | URL | string | 网络连接方式下载必填 |
Data | Base64 数据 | string | 文件下载方式必填 |
AdvName | 广告 | string | bg0,bg1,bg2,bg3,bg4 logo 必填 只有5张广告图片,宽高:9:16,推荐 1920*1080 |
文件下载方式:通常适用于没有外网的方式,就是操作复杂点,数据包比较大。
网络连接方式方式,只需要提供URL
,APP
自动下载广告。
示例1:
{
"requestid": "fcfc7fd0-c4ee-48c7-9e5a-3b368e136907",
"servicename": "advLoadImage",
"data": {
"ImageName": "764f23a1b5022f349541ba509e92b92.jpg",
"ImageUrl": null,
"AdvName": "bg0",
"Data": "/9j/4AAQSkZJRgABAQEAAAAAAAD/7gAOQWRvYmUAZAAAAAAB/+EAQEV4aWYAAE1N ACoAAAAIAA....."
}
}
示例2:
{
"requestid": "65dc4092d6c5f",
"servicename": "BackgroundImage",
"data": {
"ImageName": "",
"AdvName": "bg0",
"ImageUrl": "http://local.whizz-park.com:10180/storage/lcd/bg_blue_20231226103610.jpeg",
"Data": ""
}
}
1.2.3 报文响应
当我们的Qt
应用程序接收到车牌识别系统发送过来的请求,我们需要对此作出响应,响应格式大致如下:
{
"msg": "操作成功",
"requestId": "65dc4092d6c5f",
"status": true,
"version": "231102"
}
1.2.4 注意
车牌识别系统首先会通过advLoadImage
服务将5张背景图片发送到我们Qt
应用程序,我们需要将这些背景图片保存下来,比如以下背景图片:
在后续接收到welcome
服务的时候根据pictureUsed
选择某张图片作为背景图片,并将服务中带有的信息显示出来,显示效果如下:
上面的内容和请求字段对应关系如下:
WWW 2388
:对应carmark
字段;SORRY
:对应line1
字段;No entry record found.
:对应line2
字段;Please press intercom
:对应line3
字段;
二、 QtWebApp
介绍
由于我们要实现在局域网车牌识别系统能够通过Http
协议访问我们的应用程序,因此需要使用Qt
搭建Http
服务器,这里我们使用QtWebApp
来实现。
QtWepApp
是一个C++
中的Http
服务器库,其灵感来自Java Servlet
。适用于Linux
、Windows
、Mac OS
和Qt Framework
支持的许多其他操作系统。QtWebApp
包含以下组件:
HTTP(S)1.0
和1.1
服务器;- 模板引擎;
- 缓冲记录器。
2.1 源码下载
QtWebApp
下载地址:http://www.stefanfrings.de/qtwebapp/QtWebApp.zip
;
我们需要下载QtWebApp
源码,这里我下载到/opt/qt-project
目录;
root@zhengyang:/opt/qt-project# wget http://www.stefanfrings.de/qtwebapp/QtWebApp.zip
2.2 测试
解压QtWebApp.zip
:
root@zhengyang:/opt/qt-project# unzip QtWebApp.zip
root@zhengyang:/opt/qt-project# apt install tree
root@zhengyang:/opt/qt-project# tree -L 2 QtWebApp
QtWebApp
├── Demo1
│ ├── build
│ ├── Demo1
│ ├── Demo1.pro
│ ├── Demo1.pro.user
│ ├── etc
│ ├── logs
│ ├── Makefile
│ └── src
├── Demo2
│ ├── Demo2.pro
│ ├── Demo2.pro.user
│ └── src
└── QtWebApp
├── doc
├── Doxyfile
├── httpserver
├── logging
├── QtWebApp.pro
├── QtWebApp.pro.user
└── templateengine
2.2.1 测试源码
查看测试代码:
root@zhengyang:/opt/qt-project/QtWebApp# cd Demo1/
root@zhengyang:/opt/qt-project/QtWebApp/Demo1#
root@zhengyang:/opt/qt-project/QtWebApp/Demo1# ls -l
总用量 36
-rwxrwxrwx 1 root root 1450 3月 19 2022 Demo1.pro
-rw-r--r-- 1 root root 19374 9月 8 00:23 Demo1.pro.user
drwxr-xr-x 5 root root 4096 1月 17 2023 etc
drwxr-xr-x 2 root root 4096 3月 19 2022 logs
drwxr-xr-x 3 root root 4096 10月 1 2022 src
2.2.2 编译运行
编译:
root@zhengyang:/opt/qt-project/QtWebApp/Demo1# mkdir build
root@zhengyang:/opt/qt-project/QtWebApp/Demo1# cd build
root@zhengyang:/opt/qt-project/QtWebApp/Demo1/build# qmake ..
root@zhengyang:/opt/qt-project/QtWebApp/Demo1/build# make
运行程序:
root@zhengyang:/opt/qt-project/QtWebApp/Demo1/build# ./Demo1
Using config file /opt/qt-project/QtWebApp/Demo1/etc/Demo1.ini
Logging to /dev/stdout
QFile::at: Cannot set file position 0
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 TemplateLoader: path=/opt/qt-project/QtWebApp/Demo1/etc/templates, codec=UTF-8
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 TemplateCache: timeout=60000, size=1000000
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 HttpSessionStore: Sessions expire after 3600000 milliseconds
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 StaticFileController: docroot=/opt/qt-project/QtWebApp/Demo1/etc/docroot, encoding=UTF-8, maxAge=60000
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 StaticFileController: cache timeout=60000, size=1000000
05.03.2024 23:08:11.790 0 DEBUG 0x7f17992ddbc0 RequestMapper: created
05.03.2024 23:08:11.809 0 DEBUG 0x7f17992ddbc0 HttpListener: Listening on port 8080
05.03.2024 23:08:11.809 1 WARNING 0x7f17992ddbc0 Application has started
打开PC Web
浏览器,在浏览器输入http://192.168.0.200:8080/
:
其中192.168.0.200
为程序运行所在宿主机的ip
地址。
三、QtWebApp
使用
由于我们的开发板已经内置了Qt4.8
,因此我们最好在宿主机上搭建Qt4.8
的环境,这样方便将代码直接拷贝到开发板编译运行。
但是这里我们并不打算这么搞了,因此我的ubuntu
宿主机安装的是Qt6.5.0
,具体可以参考《Rockchip RK3588 - linux
下Qt
和opencv
交叉编译环境搭建》。
这里我直接在宿主机上使用Qt6.5.0
进行开发,最后再移植到开发板上编译调整。
3.1 新建项目
在宿主机打开Qt Creator
:
zhengyang@zhengyang:/$ sudo /opt/qtcreator-11.0.0/bin/qtcreator
在Qt Creator
首页点击【创建项目】 ,选中【Application(Qt)
】- 【Qt Widgets Application
】;接着进行如下设置:
Location
:名称PlateLCDDisplay
,创建路径/opt/qt-project
;- 构建系统:选择
qmake
; Details
:跳过;Tranlation
:跳过;- 构建套件:选择桌面;
- 汇总:跳过。
新建的项目目录结构如下:
root@zhengyang:/opt/qt-project# tree -L 2 PlateLCDDisplay/
PlateLCDDisplay/
├── main.cpp
├── mainwindow.cpp
├── mainwindow.h
├── mainwindow.ui
├── PlateLCDDisplay.pro
└── PlateLCDDisplay.pro.user
选择【工具】- 【外部】-【配置】-【文本编辑器】- 【行为】- 【文件编码】, 将文件编码设置为utf-8
,UTF-8 BOM
选择存在则保留,最后选择应用;
这样源码文件将均采用UTF-8
编码。
3.1.1 webapp.ini
配置文件
在项目目录下创建etc
文件夹,在文件夹下添加webapp.ini
配置文件,并通过qt designer
将配置文件添加到项目中;
[listener]
;ip=192.168.0.110
port=80
minThreads=4
maxThreads=100
cleanupInterval=60000
readTimeout=10000
maxRequestSize=6000000
maxMultiPartSize=10000000
其中:
ip
和port
:IP
和端口参数指定web
服务器侦听的IP
地址和端口。如果注释掉IP
(如上所述),则服务器将侦听所有网络接口。公共web
服务器使用端口80
,而内部web
服务器通常在端口8080
上侦听;minThreads
和cleanupInterval
:QtWebApp
可以同时处理多个Http
请求,因此它是多线程的。该参数用于设置空闲时并发工作线程的最小数量;web
服务器总是以一个空线程池开始,线程是在Http
请求传入时按需创建的,空闲线程由计时器缓慢关闭;每个cleanupInterval
(以毫秒为单位),服务器都会关闭一个空闲线程;maxThreads
:指定并发工作线程的最大数量,在进入生产环境之前,应该使用负载生成器工具来了解服务器在不耗尽内存或变得迟缓的情况下可以处理多少负载;使用给定的值,服务器最多可以处理100
个并发Http
连接。它保持4个空闲的工作线程运行,以确保良好的响应时间;readTimeout
:设置可以保护服务器免受简单的拒绝服务攻击,比如打开大量连接但不使用它们;空闲连接在指定的毫秒数后会被关闭,在正常情况下,Web
浏览器负责关闭连接;maxRequestSize
:保护服务器不受非常大的Http
请求造成的内存过载的影响,此值适用于常规请求;maxMultiPartSize
:设置允许上传的最大文件大小,以防止恶意用户上传过大的文件导致服务器负载过重或资源耗尽;
3.1.2 modules
模块
在项目目录下创建modules
文件夹:
root@zhengyang:/opt/qt-project# cd PlateLCDDisplay
root@zhengyang:/opt/qt-project/PlateLCDDisplay# mkdir modules
3.2 httpserver
模块
将/opt/qt-project/QtWebApp/QtWebApp/httpserver
拷贝到modules
目录下;
root@zhengyang:/opt/qt-project/PlateLCDDisplay# cp -r /opt/qt-project/QtWebApp/QtWebApp/httpserver ./modules
将以下行添加到PlateLCDDisplay.pro
;
QT +=network
include ($$PWD/modules/httpserver/httpserver.pri)
3.3 Http
管理模块
在项目modules
文件夹,创建httpServerManager
文件夹,同时在httpServerManager
文件夹下创建如下文件:
root@zhengyang:/opt/qt-project/PlateLCDDisplay# cd modules/
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules# mkdir httpServerManager
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules# cd httpServerManager
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch HttpServerManager.h
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch HttpServerManager.cpp
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch HelloworldRequestHandler.h
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch HelloworldRequestHandler.cpp
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch httpServerManager.pri
其中:
httpServerManager.pri
:子项目文件;HttpServerManager.h
和HttpServerManager.cpp
:Http
管理器实现,用于加载web
配置,并启动Http Server
监听Http
请求;HelloworldRequestHandler.h
和HelloworldRequestHandler.cpp
:Http
请求处理实现;
3.3.1 httpServerManager.pri
httpServerManager.pri
:子模块项目文件,类似Makefile
,用来链接项目文件,比如头文件、源码等;
INCLUDEPATH += $$PWD
DEPENDPATH += $$PWD
# Enable very detailed debug messages when compiling the debug version
CONFIG(debug, debug|release) {
DEFINES += SUPERVERBOSE
}
HEADERS += $$PWD/HttpServerManager.h \
$$PWD/HelloworldRequestHandler.h
SOURCES += $$PWD/HttpServerManager.cpp \
$$PWD/HelloworldRequestHandler.cpp
同时将以下行添加到PlateLCDDisplay.pro
;
include ($$PWD/modules/httpServerManager/httpServerManager.pri)
3.3.2 HttpServerManager.h
#ifndef HTTPSERVERMANAGER_H
#define HTTPSERVERMANAGER_H
#include <QObject>
#include <QMutex>
#include "httplistener.h"
#include "filelogger.h"
#include "HelloworldRequestHandler.h"
#include "logging.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 maxRequestSize); /* 设置最大请求数 */
void setMaxMultiPartSize(int maxMultiPartSize); /* 设置上传文件最大大小(单位,字节)*/
QString searchConfigFile(); /* Find the configuration file */
public slots:
void slot_start();
void slot_stop();
private:
static HttpServerManager *_pInstance;
static QMutex _mutex;
private:
bool _running;
private:
HttpListener *_pHttpListener; /* http服务监听器 */
QSettings *_pHttpListenerSettings; /* http服务器配置文件 */
private:
QString _ip; /* 服务器监听ip,若为空,则表示监听所有ip */
quint16 _port; /* 服务器监听端口 */
int _minThreads; /* 空闲最小线程数 */
int _maxThreads; /* 负载最大线程数 */
int _cleanupInterval; /* 空线程清空间隔(单位,毫秒) */
int _readTimeout; /* 保持连接空载超时时间(单位,毫秒)*/
int _maxRequestSize; /* 最大请求数 */
int _maxMultiPartSize; /* 上传文件最大大小(单位,字节) */
};
#endif // HTTPSERVERMANAGER_H
3.3.3 HttpServerManager.cpp
在HttpServerManager.cpp
文件中主要用于从多个路径查找并加载webapp.ini
配置文件,同时创建HttpListener
监听Http
请求。
#include "HttpServerManager.h"
#include <QApplication>
#include <QDir>
HttpServerManager *HttpServerManager::_pInstance = 0;
QMutex HttpServerManager::_mutex;
HttpServerManager::HttpServerManager(QObject *parent)
: QObject(parent),
_pHttpListener(0),
_pHttpListenerSettings(0),
_running(false),
_port(8088),
_minThreads(2),
_maxThreads(10),
_cleanupInterval(60000),
_readTimeout(60000),
_maxRequestSize(100),
_maxMultiPartSize(1024*1024*1024)
{
}
/**
* 单例模式
*
* @brief HttpServerManager::getInstance
* @return
*/
HttpServerManager *HttpServerManager::getInstance()
{
if(!_pInstance)
{
QMutexLocker lock(&_mutex);
if(!_pInstance)
{
_pInstance = new HttpServerManager();
}
}
return _pInstance;
}
/**
* 启动http的监听
* @brief HttpServerManager::slot_start
*/
void HttpServerManager::slot_start()
{
if(_running)
{
LOG_Debug << "It's running!!!";
return;
}
_running = true;
LOG_Debug << "Succeed to run";
// Find the configuration file
QString httpServerPath = searchConfigFile();
LOG_Debug << httpServerPath << "exit:" << QFile::exists(httpServerPath);
// Configure and start the TCP listener
if(!_pHttpListenerSettings)
{
_pHttpListenerSettings = new QSettings(httpServerPath, QSettings::IniFormat);
_pHttpListenerSettings->beginGroup("listener");
qDebug("config file loaded");
setIp(_pHttpListenerSettings->value("ip").toString());
setPort(_pHttpListenerSettings->value("port").toUInt());
setMinThreads(_pHttpListenerSettings->value("minThreads").toInt());
setMaxThreads(_pHttpListenerSettings->value("maxThreads").toInt());
setCleanupInterval(_pHttpListenerSettings->value("cleanupInterval").toInt());
setReadTimeout(_pHttpListenerSettings->value("readTimeout").toInt());
setMaxRequestSize(_pHttpListenerSettings->value("maxRequestSize").toInt());
setMaxMultiPartSize(_pHttpListenerSettings->value("maxMultiPartSize").toInt());
// 打印读取的属性值
LOG_Info << "ip:" << _ip;
LOG_Info << "port:" << _port;
LOG_Info << "minThreads:" << _minThreads;
LOG_Info << "maxThreads:" << _maxThreads;
LOG_Info << "cleanupInterval:" << _cleanupInterval;
LOG_Info << "readTimeout:" << _readTimeout;
LOG_Info << "maxRequestSize:" << _maxRequestSize;
LOG_Info << "maxMultiPartSize:" << _maxMultiPartSize;
}
_pHttpListener = new HttpListener(_pHttpListenerSettings, new HelloworldRequestHandler);
}
/**
* 停止http的监听
* @brief HttpServerManager::slot_stop
*/
void HttpServerManager::slot_stop()
{
if(!_running)
{
LOG_Debug <<"It's not running!!!";
return;
}
_running = false;
LOG_Debug << "Succeed to stop";
_pHttpListener->close();
}
int HttpServerManager::getMaxMultiPartSize() const
{
return _maxMultiPartSize;
}
void HttpServerManager::setMaxMultiPartSize(int maxMultiPartSize)
{
_maxMultiPartSize = maxMultiPartSize;
}
int HttpServerManager::getMaxRequestSize() const
{
return _maxRequestSize;
}
void HttpServerManager::setMaxRequestSize(int axRequestSize)
{
_maxRequestSize = axRequestSize;
}
int HttpServerManager::getReadTimeout() const
{
return _readTimeout;
}
void HttpServerManager::setReadTimeout(int readTimeout)
{
_readTimeout = readTimeout;
}
int HttpServerManager::getCleanupInterval() const
{
return _cleanupInterval;
}
void HttpServerManager::setCleanupInterval(int cleanupInterval)
{
_cleanupInterval = cleanupInterval;
}
int HttpServerManager::getMaxThreads() const
{
return _maxThreads;
}
void HttpServerManager::setMaxThreads(int maxThreads)
{
_maxThreads = maxThreads;
}
int HttpServerManager::getMinThreads() const
{
return _minThreads;
}
void HttpServerManager::setMinThreads(int minThreads)
{
_minThreads = minThreads;
}
quint16 HttpServerManager::getPort() const
{
return _port;
}
void HttpServerManager::setPort(const quint16 port)
{
_port = port;
}
QString HttpServerManager::getIp() const
{
return _ip;
}
void HttpServerManager::setIp(const QString ip)
{
_ip = ip;
}
/**
* Search the configuration file.
* Abborts the application if not found.
* @brief HttpServerManager::searchConfigFile
* @return
*/
QString HttpServerManager::searchConfigFile()
{
QString binDir = QCoreApplication::applicationDirPath(); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug
QString appName = QCoreApplication::applicationName(); // PlateLCDDisplay
QString fileName = "webapp.ini";
QStringList searchList;
searchList.append(binDir); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug
searchList.append(binDir + "/etc"); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug/etc
searchList.append(binDir + "/../etc"); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug/../etc
searchList.append(binDir + "/../" + appName + "/etc"); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug/../PlateLCDDisplay/etc
searchList.append(binDir + "/../../" + appName + "/etc"); // /opt/qt-project/build-PlateLCDDisplay-unknown-Debug/../../PlateLCDDisplay/etc
searchList.append(QDir::rootPath() + "etc/opt"); // /etc/opt"
searchList.append(QDir::rootPath() + "etc"); // /etc
foreach (QString dir, searchList)
{
QFile file(dir + "/" + fileName);
if (file.exists())
{
// 将相对路径名转换为绝对形式
fileName = QDir(file.fileName()).canonicalPath(); // /opt/qt-project/PlateLCDDisplay/etc/webapp.ini
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;
}
3.3.4 HelloworldRequestHandler.h
#ifndef HELLOWORLDREQUESTHANDLER_H
#define HELLOWORLDREQUESTHANDLER_H
#include "httprequesthandler.h"
#include "logging.h"
using namespace stefanfrings;
class HelloworldRequestHandler : public HttpRequestHandler
{
public:
HelloworldRequestHandler(QObject *parent = 0);
public:
void service(HttpRequest& request, HttpResponse& response);
};
#endif // HELLOWORLDREQUESTHANDLER_H
3.3.5 HelloworldRequestHandler.cpp
该文件用于处理Http
请求,这里我们实现了虚函数service
,在函数内部我们并没有根据请求uri
进行分发请求,也就是说我们只要通过http://ip:port/uri
访问我们的板子,返回的始终全志H3 - Qt & QtWebApp搭建Http Server(无X11系统)Hello world!
;
#include "HelloworldRequestHandler.h"
HelloworldRequestHandler::HelloworldRequestHandler(QObject *parent):HttpRequestHandler(parent)
{
}
void HelloworldRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
QString text = "全志H3 - Qt & QtWebApp搭建Http Server(无X11系统)Hello world!";
// 将字符串转换为本地编码的字节数组
QByteArray data = text.toLocal8Bit();
// 设置HTTP Response的Content-Type和字符编码
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.write(data,true);
}
在这个方法中,首先将一个包含中文和英文字符的字符串转换为本地编码(当前系统的本地编码为utf-8
)的字节数组,然后设置HTTP
响应的Content-Type 为"text/plain; charset=utf-8"
,最后将utf-8
数据写入响应中返回给客户端。
response.write
函数有两个参数:
data
:要写入的响应体数据,以QByteArray
形式传入;lastPart
:一个可选参数,用于指示是否为最后一个数据块,默认值为false
;
如果响应仅包含一个数据块,即设置lastPart=true
,则会自动设置Content-Length
头部。
如果响应没有设置Content-Length
头部,并且也没有设置Connection: close
头部,那么将自动选择分块传输模式(Chunked Transfer Encoding
)。
3.4 日志模块
对于一个web
服务来说,日志是一个非常重要的功能模块,通过记录日志能够定位服务运行中出现各种问题。
QtWebApp
提供了日志模块,将/opt/qt-project/QtWebApp/QtWebApp/logging
拷贝到modules
目录下;
root@zhengyang:/opt/qt-project/PlateLCDDisplay# cp -r /opt/qt-project/QtWebApp/QtWebApp/logging ./modules
要将日志模块的源代码包括到项目中,将以下行添加到PlateLCDDisplay.pro
;
include ($$PWD/modules/logging/logging.pri)
3.4.1 webapp.ini
配置文件
修改webapp.ini
配置文件,添加如下内容:
[logging]
minLevel=INFO
bufferSize=100
fileName=../logs/webapp.log
;fileName=/dev/stdout
maxSize=1000000
maxBackups=2
timestampFormat=dd.MM.yyyy hh:mm:ss.zzz
msgFormat={timestamp} {typeNr} {type} {thread} {msg}
其中:
-
minLevel
:日志级别,只有配置了minLevel
及以上级别的消息才会写入日志文件;日志级别有:DEBUG
,INFO
、WARN
、CRITICAL
(别名ERROR
)、FATAL
,这里配置为INFO
级别; -
bufferSize
:日志缓冲区大小,配置为非0表示启用线程本地缓冲区; -
fileName
:日志文件名,如果没有指定文件名,则日志会输出到控制台,日志文件的路径可以是绝对路径,也可以是相对于配置文件的文件夹的路径; -
maxSize
:限制日志文件的大小(以字节为单位),当超过此限制时,将会创建一个新的日志文件; -
maxBackups
:指定磁盘上应保留多少日志文件; -
timestampFormat
:设置时间戳格式,QDateTime::toString()
的文档以获得对字符的解释,还有更多可用的内容; -
msgFormat
:设置指定每条消息的格式,以下字段可用:{timestamp}
:创建日期和时间;{typeNr}
:数字格式的消息类型或级别(0=DEBUG
,4=INFO
,1=WARNING
,2=CRITICAL
,3=FATAL
);{type}
:字符串格式的消息类型或级别(DEBUG
,INFO
,WARNING
,CRITICAL
,FATAL
);{thread}
:线程的ID
号;{msg}
:消息文本;
在Qt5
及以上版本支持:
msgFormat={timestamp} {typeNr} {type} {thread} {msg}\n in {file} line {line} function {function}
其中:
{file}
:Filename of source code where the message was generated
;{function}
:Function where the message was generated
;{line}
:Line number where the message was generated
;
3.4.2 HttpServerManager.h
修改HttpServerManager.h
添加如下成员:
public:
FileLogger *pFileLogger; /* 日志记录 */
private:
HttpListener *_pHttpListener; /* http服务监听器 */
QSettings *_pHttpListenerSettings; /* http服务器配置文件 */
QSettings *_pFileLoggerSettings; /* 记录配置文件 */
3.4.3 HttpServerManager.cpp
修改HttpServerManager.cpp
的slot_start
函数,添加如下代码:
// Configure logging into a file
if(!_pFileLoggerSettings)
{
_pFileLoggerSettings = new QSettings(httpServerPath,QSettings::IniFormat);
_pFileLoggerSettings->beginGroup("logging");
// 日志不会主动创建文件夹,所以需要我们自己创建
QFileInfo fileinfo(httpServerPath);
QString dirPath = fileinfo.dir().absolutePath();
dirPath = QString("%1/%2")
.arg(dirPath)
.arg(_pFileLoggerSettings->value("fileName").toString());
dirPath = dirPath.mid(0,dirPath.lastIndexOf("/"));
QDir dir;
dir.mkpath(dirPath);
}
pFileLogger = new FileLogger(_pFileLoggerSettings);
pFileLogger->installMsgHandler();
此外,如果我们想日志配置文件修改之后,不用重启服务就能生效,修改_pFileLogger
所在行代码:
pFileLogger = new FileLogger(logSettings,10000);
数字10000
是以毫秒为单位的刷新间隔,记录器使用它来重新加载配置文件。因此,可以在程序运行时编辑任何记录器设置,并且更改在几秒钟后生效,而无需重新启动服务器,如果不希望自动重新加载,请使用值0。
3.4.4 logging.h
我们在项目目录下创建一个logging.h
文件,里面定义一些不同级别日志输出的宏:
#ifndef LOGGING_H
#define LOGGING_H
#define LOG_Debug qDebug()<<__FILE__<<__LINE__
#define LOG_Info qInfo()<<__FILE__<<__LINE__
#define LOG_Warn qWarning()<<__FILE__<<__LINE__
#define LOG_Error qCritical()<<__FILE__<<__LINE__
#define LOG_Fatal qFatal()<<__FILE__<<__LINE__
#endif // LOGGING_H
QtWebApp
三方源码中的qDebug
,qWarn
,QFatal
等相关系统函数是将日志直接输出到控制待的,使用该日志模块可以截断这些函数输出的日志,并将日志输出到有配置文件fileName
指定的位置。
3.4.5 记录请求IP
日志记录器支持我们自定义用户变量,这些变量是线程本地的,类似Java
中的ThreadLocal
,在清除它们之前一直保留在内存中。
对于web
应用程序,在每条消息中记录当前请求用户ip
,向HelloworldRequestHandler.cpp
添加代码以设置记录器变量:
void HelloworldRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
// 获取客户端的 IP 地址
QString clientIp = request.getHeader("x-forwarded-for");
if(clientIp.isEmpty())
{
clientIp = request.getHeader("Proxy-Client-IP");
}
if(clientIp.isEmpty())
{
clientIp = request.getHeader("WL-Proxy-Client-IP");
}
if(clientIp.isEmpty())
{
clientIp = request.getHeader("HTTP_CLIENT_IP");
}
if(clientIp.isEmpty())
{
clientIp = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if(clientIp.isEmpty())
{
clientIp = request.getPeerAddress().toString();
}
if(!clientIp.isEmpty())
{
HttpServerManager::getInstance()->pFileLogger->set("remoteIp",clientIp);
}
// 打印客户端的 IP 地址
LOG_Info << "Client IP Address: " << clientIp;
QString text = "全志H3 - Qt & QtWebApp搭建Http Server(无X11系统)Hello world!";
// 将字符串转换为本地编码的字节数组
QByteArray data = text.toLocal8Bit();
// 设置HTTP Response的Content-Type和字符编码
response.setHeader("Content-Type", "text/plain; charset=utf-8");
response.write(data);
// 清理
HttpServerManager::getInstance()->pFileLogger->clear(true,true);
}
通过这种方式,在请求处理之前我们可以获取到请求客户端的IP
地址。现在可以修改webapp.ini
文件以使用该变量:
msgFormat={timestamp} {typeNr} {type} {thread} Ip:{remoteIp} {msg}
需要注意的是:在代码最后我们对日志进行了清除,为什么要怎么做呢?
主要是由于一个请求处理线程可能会对多个Http
请求复用,为了避免对新的请求造成影响,需要每当Http
请求的处理完成时,都要清理记录器的缓存,当同一个线程处理下一个请求时,它将以空缓冲区开始。
3.5 mainwindow.cpp
我们需要在mainwindow.cpp
中运行Http
管理类启动Http Server
监听用户请求;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
int width = 540;
int heigh = 960;
// 设置无边框 设置窗口置顶,使其始终位于其他窗口的前面
setWindowFlags(Qt::CustomizeWindowHint | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
......
// 开启Http Server Listener
initHttpServer();
}
/**
* 开启Http Server Listener
* @brief MainWindow::initHttpServer
*/
void MainWindow::initHttpServer()
{
// init thread
HttpServerManager *pHttpServerManager = HttpServerManager::getInstance();
QThread *pHttpServerManagerThread = new QThread();
pHttpServerManager->moveToThread(pHttpServerManagerThread);
connect(pHttpServerManagerThread, SIGNAL(started()), pHttpServerManager, SLOT(slot_start()));
// start thread
pHttpServerManagerThread->start();
}
3.6 运行测试
3.6.1 启动日志
在ubuntu
宿主机运行程序,控制台输出如下日志:
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-root'
../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 64 Succeed to run
Using config file /opt/qt-project/PlateLCDDisplay/etc/webapp.ini
../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 68 "/opt/qt-project/PlateLCDDisplay/etc/webapp.ini" exit: true
Logging to /opt/qt-project/PlateLCDDisplay/logs/webapp.log
此时ubuntu
宿主机日志文件logs/webapp.log
中输出配置文件中listener
配置信息:
07.03.2024 23:49:13.652 0 DEBUG 0x7f03a8ecc700 Ip:{remoteIp} config file loaded
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 105 ip: ""
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 106 port: 80
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 107 minThreads: 4
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 108 maxThreads: 100
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 109 cleanupInterval: 60000
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 110 readTimeout: 10000
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 111 maxRequestSize: 6000000
07.03.2024 23:49:13.652 4 INFO 0x7f03a8ecc700 Ip:{remoteIp} ../PlateLCDDisplay/modules/httpServerManager/HttpServerManager.cpp 112 maxMultiPartSize: 10000000
3.6.2 请求日志
在PC
机器Web
浏览器输入ubuntu
宿主机IP
地址:
此时ubuntu
宿主机日志文件logs/webapp.log
中输出:
07.03.2024 23:51:03.453 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpConnectionHandler (0x7f03a000a5a0): handle new connection
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpConnectionHandler (0x7f03a000a5a0): read input
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: read request
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: from ::ffff:192.168.0.110: GET / HTTP/1.1
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header host: 192.168.0.200
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header connection: keep-alive
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header cache-control: max-age=0
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header upgrade-insecure-requests: 1
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header accept-encoding: gzip, deflate
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: received header accept-language: zh-CN,zh;q=0.9
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: headers completed
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: expect no body
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: extract and decode request parameters
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpRequest: extract cookies
07.03.2024 23:51:03.454 0 DEBUG 0x7f038bfff700 Ip:{remoteIp} HttpConnectionHandler (0x7f03a000a5a0): received request
07.03.2024 23:51:03.454 4 INFO 0x7f038bfff700 Ip:::ffff:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/HelloworldRequestHandler.cpp 44 Client IP Address: "::ffff:192.168.0.110"
07.03.2024 23:51:03.455 0 DEBUG 0x7f038bfff700 Ip:::ffff:192.168.0.110 HttpConnectionHandler (0x7f03a000a5a0): finished request
第一次发送Http
请求,服务端连接池会创建一个HttpConnectionHandler
线程来处理当前请求,并将Http
请求信息输出,同时执行我们编写service
方法,执行完成后,readTimeout
超时时间一到,会将当前scocket
连接关闭。
3.6.3 日志优化
这里输出了大量有关HttpRequest
的日志信息,如果想关闭其中一些不必要的日志,我们只需要将构建模式从Debug
更改为Release
即可;
07.03.2024 23:54:26.384 0 DEBUG 0x7f9007c95700 Ip:::ffff:192.168.0.110 HttpRequest: from ::ffff:192.168.0.110: GET / HTTP/1.1
07.03.2024 23:54:26.384 0 DEBUG 0x7f9007c95700 Ip:::ffff:192.168.0.110 HttpConnectionHandler (0x7f901000a600): received request
07.03.2024 23:54:26.384 4 INFO 0x7f9007c95700 Ip:::ffff:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/HelloworldRequestHandler.cpp 44 Client IP Address: "::ffff:192.168.0.110"
07.03.2024 23:54:26.384 0 DEBUG 0x7f9007c95700 Ip:::ffff:192.168.0.110 HttpConnectionHandler (0x7f901000a600): finished request
这里我们注意最后其中一条日志,该日志是我在service
函数中通过如下代码输出的:
LOG_Info << "Client IP Address: " << clientIp;
3.7 前后端分离
实际上QtWebApp
还支持作为静态页面服务器,在当前后端分离盛行的时代;
- 前端负责页面的开发,通过
AJAX
请求来访问后端的数据接口,将Model
展示到View
中即可; - 后端提供数据接口;
因此这里我们只使用QtWebApp
提供数据处理接口即可。然而QtWebApp
提供的功能不止这些,比如:
- 关于静态文件实现可以参考:《
http
服务器html
实现静态相对路径调用第三方js
文件》; - 关于
Session
和Cookie
实现可以参考:《http
服务器使用Session
和Cookie
实现用户密码登录和注销功能》。
四、项目实现
前面我们已经简单的介绍了web
实现,并讲解了如何处理Http
请求,这一节我们将实现http://IP:8080/API
接口的响应。
如果想实现访问不同的url
返回不同的信息应该如何办呢?我们可以在HelloworldRequestHandler
中获取uri
路径,根据路径不同执行不同的处理逻辑。
通常而言,我们会创建一个请求映射器,将不同的请求映射到不同的Controller
层。
4.1 分发请求
我们在httpServerManager
下创建RequestMapper
类,它将实现多个控制器之间切换。
4.1.1 RequestMapper.h
#ifndef REQUESTMAPPER_H
#define REQUESTMAPPER_H
#include "httprequesthandler.h"
#include "logging.h"
using namespace stefanfrings;
/**
The request mapper dispatches incoming HTTP requests to controller classes
depending on the requested path.
*/
class RequestMapper : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(RequestMapper)
public:
/**
Constructor.
@param parent Parent object
*/
RequestMapper(QObject* parent=0);
/**
Destructor.
*/
~RequestMapper();
/**
Dispatch incoming HTTP requests to different controllers depending on the URL.
@param request The received HTTP request
@param response Must be used to return the response
*/
void service(HttpRequest& request, HttpResponse& response);
private:
QString _clientIp;
private:
QString getRemoteIp(HttpRequest& request);
};
#endif // REQUESTMAPPER_H
4.1.2 RequestMapper.cpp
#include <QCoreApplication>
#include "RequestMapper.h"
#include "controller/HelloworldController.h"
#include "controller/PlateController.h"
#include "HttpServerManager.h"
RequestMapper::RequestMapper(QObject* parent)
:HttpRequestHandler(parent)
{
LOG_Debug << "RequestMapper: created";
}
RequestMapper::~RequestMapper()
{
LOG_Debug << "RequestMapper: deleted";
}
/**
* 获取客户端的 IP 地址
* @brief getRemoteIp
* @return
*/
QString RequestMapper::getRemoteIp(HttpRequest& request)
{
_clientIp = request.getHeader("x-forwarded-for");
if(_clientIp.isEmpty())
{
_clientIp = request.getHeader("Proxy-Client-IP");
}
if(_clientIp.isEmpty())
{
_clientIp = request.getHeader("WL-Proxy-Client-IP");
}
if(_clientIp.isEmpty())
{
_clientIp = request.getHeader("HTTP_CLIENT_IP");
}
if(_clientIp.isEmpty())
{
_clientIp = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if(_clientIp.isEmpty())
{
_clientIp = request.getPeerAddress().toString();
}
if(!_clientIp.isEmpty())
{
HttpServerManager::getInstance()->pFileLogger->set("remoteIp",_clientIp);
}
return _clientIp;
}
void RequestMapper::service(HttpRequest& request, HttpResponse& response)
{
QString clientIp = getRemoteIp(request);
// 打印客户端的 IP 地址
LOG_Info << "Client IP Address: " << clientIp;
QByteArray path = request.getPath();
LOG_Info << "RequestMapper: path=" << path.data();
// For the following pathes, each request gets its own new instance of the related controller.
if (path.startsWith("/API"))
{
PlateController().service(request, response);
}
else if (path.startsWith("/hello"))
{
HelloworldController().service(request, response);
}
LOG_Debug << "RequestMapper: finished request";
// Clear the log buffer
HttpServerManager::getInstance()->pFileLogger->clear(true,true);
}
如果多个并发Http
请求同时传入,那么service()
方法将并行执行多次。所以这个方法是多线程的。当访问在service()
方法外部声明的变量时,必须考虑这一点。
请求映射器处于application scope
,这意味着在应用程序的生命周期内只有一个实例存在,并且可以被所有请求共享。这种模式通常被称为单例模式。
这种方式有助于提高性能,避免重复创建映射器实例,并统一管理请求的路由和处理逻辑。同时,也便于维护和扩展应用程序,因为所有请求都共享同一个映射器实例。
两个控制器类位于request scope
中,这意味着每个请求都由该类的新实例处理,这会降低一些性能,但会稍微简化编程。
4.1.3 优化
因此我们可以将两个控制器修改为application scope
,在RequestMapper.h
中添加;
public:
HelloworldController hellowordController;
PlateController plateController;
修改RequestMapper.cpp
中service
方法;
void RequestMapper::service(HttpRequest& request, HttpResponse& response)
{
QString clientIp = getRemoteIp(request);
// 打印客户端的 IP 地址
LOG_Info << "Client IP Address: " << clientIp;
QByteArray path = request.getPath();
LOG_Info << "RequestMapper: path=" << path.data();
// For the following pathes, each request gets its own new instance of the related controller.
if (path.startsWith("/API"))
{
plateController.service(request, response);
}
else if (path.startsWith("/hello"))
{
hellowordController.service(request, response);
}
LOG_Debug << "RequestMapper: finished request";
// Clear the log buffer
HttpServerManager::getInstance()->pFileLogger->clear(true,true);
}
4.2 核心业务代码
这里我们创建两个Controller
控制器,对应的uri
分别为:
/hello
:对应的控制器实现为HelloworldController.cpp
;/API:
对应的控制器实现为PlateController.cpp
;
我们在httpServerManager
下创建controller
文件夹,并将HelloworldController.cpp
、HelloworldController.h
文件移动到该文件夹,同时创建PlateController.h
、PlateController.cpp
文件。
root@zhengyang:/opt/qt-project/PlateLCDDisplay# cd modules/httpServerManager
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# mkdir controller
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# mv HelloworldRequestHandler.h HelloworldController.h
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# mv HelloworldRequestHandler.cpp HelloworldController.cpp
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# mv HelloworldController.* ./controller/
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch ./controller/PlateController.h
root@zhengyang:/opt/qt-project/PlateLCDDisplay/modules/httpServerManager# touch ./controller/PlateController.cpp
4.2.1 PlateController.h
#ifndef PLATECONTROLLER_H
#define PLATECONTROLLER_H
#include "httprequesthandler.h"
#include "logging.h"
#include "plate.h"
using namespace stefanfrings;
class PlateController :public HttpRequestHandler
{
Q_OBJECT
signals:
void welcome(plate_welcome plate);
public:
/** Constructor */
PlateController(QObject *parent = 0);
public:
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // LOGINCONTROLLER_H
4.2.2 PlateController.cpp
#include "PlateController.h"
#include <QJsonParseError>
#include <QJsonDocument>
#include <QJsonObject>
#include <ResponseUtils.h>
#include <HttpServerManager.h>
#include <QImage>
#include <QDir>
#include <QBuffer>
PlateController::PlateController(QObject *parent):HttpRequestHandler(parent)
{
}
void PlateController::service(HttpRequest& request, HttpResponse& response)
{
QByteArray body = request.getBody();
QJsonParseError err;
//解析json对象
QJsonDocument json_recv = QJsonDocument::fromJson(body,&err);
if(json_recv.isNull())
{
ResponseUtils::fail(response, nullptr);
return;
}
QJsonObject object = json_recv.object();
if(object.contains("requestid"))
{
QString requestid = object.value("requestid").toString();
QString servicename = object.value("servicename").toString();
if(object.contains("data"))
{
QJsonValue value = object.value("data");
if(value .isObject() && servicename == "welcome")
{
QJsonObject data = value.toObject();
QString carmark = data.value("carmark").toString();
QString line1 = data.value("line1").toString();
QString line2 = data.value("line2").toString();
QString line3 = data.value("line3").toString();
QString carmarkSize = data.value("carmarkSize").toString();
QString line1Size = data.value("line1Size").toString();
QString line2Size = data.value("line2Size").toString();
QString line3Size = data.value("line3Size").toString();
QString carmarkColor = data.value("carmarkColor").toString();
QString line1Color = data.value("line1Color").toString();
QString line2Color = data.value("line2Color").toString();
QString line3Color = data.value("line3Color").toString();
QString pictureUsed = data.value("pictureUsed").toString();
plate_welcome plate;
plate.carmark = carmark;
plate.line1 = line1;
plate.line2 = line2;
plate.line3 = line3;
plate.carmarkSize = carmarkSize.isEmpty() ? 59 : carmarkSize.toInt();
plate.line1Size = line1Size.isEmpty() ? 44 : line1Size.toInt();
plate.line2Size = line2Size.isEmpty() ? 44 : line2Size.toInt();
plate.line3Size = line3Size.isEmpty() ? 44 : line3Size.toInt();
plate.carmarkColor = carmarkColor.isEmpty() ? "#FFF" : carmarkColor;
plate.line1Color = line1Color.isEmpty() ? "#FFF" : line1Color;
plate.line2Color = line2Color.isEmpty() ? "#FFF" : line2Color;
plate.line3Color = line3Color.isEmpty() ? "#FFF" : line3Color;
plate.pictureUsed = pictureUsed.isEmpty() ? 0 : pictureUsed.toInt();
LOG_Info << "carmark:" << plate.carmark ;
LOG_Info << "line1:" << plate.line1 ;
LOG_Info << "line2:" << plate.line2 ;
LOG_Info << "line3:" << plate.line3 ;
LOG_Info << "carmarkSize:" << plate.carmarkSize ;
LOG_Info << "line1Size:" << plate.line1Size;
LOG_Info << "line2Size:" << plate.line2Size;
LOG_Info << "line3Size:" << plate.line3Size;
LOG_Info << "carmarkColor:" << plate.carmarkColor ;
LOG_Info << "line1Color:" << plate.line1Color ;
LOG_Info << "line2Color:" << plate.line2Color ;
LOG_Info << "line3Color:" << plate.line3Color ;
LOG_Info << "pictureUsed:" << plate.pictureUsed ;
emit welcome(plate);
ResponseUtils::success(response, requestid);
return;
}
if(value .isObject() && servicename == "advLoadImage")
{
QJsonObject data = value.toObject();
QString advName = data.value("AdvName").toString();
QString base64 = data.value("Data").toString();
if(!base64.isEmpty() && !advName.isEmpty())
{
// 解码Base64字符串为字节数组
QByteArray byteArray = QByteArray::fromBase64(base64.toLocal8Bit());
// 将字节数组转换为图像对象
QImage image;
if(image.loadFromData(byteArray,"jpg")) {
LOG_Info << "Image loaded successfully.";
} else {
LOG_Info << "Failed to load image.";
}
// 保存图像为文件
QDir dir(HttpServerManager::getInstance()->getBgPath());
QString filePath = dir.filePath(advName + ".jpg");
bool saved = image.save(filePath);
if (saved) {
LOG_Info << "Image " << filePath << " saved successfully!";
ResponseUtils::success(response, requestid);
return;
} else {
LOG_Info << "Image " << filePath << " saved failed!";
ResponseUtils::fail(response, nullptr);
return;
}
}
}
}
}
ResponseUtils::fail(response, nullptr);
}
如果我们接收到的servername
为advLoadImage
,我们将获取图片base64
编码的字符串,解码后并将其保存到./bg
路径下;
如果我们接收到的servername
为welcome
,我们通过信号与槽机制将获取的内容发送到ui
线程去显示。
4.2.3 HelloworldController.h
#ifndef HELLOWORLDREQUESTHANDLER_H
#define HELLOWORLDREQUESTHANDLER_H
#include "httprequesthandler.h"
#include "logging.h"
using namespace stefanfrings;
class HelloworldController : public HttpRequestHandler
{
public:
HelloworldController(QObject *parent = 0);
public:
void service(HttpRequest& request, HttpResponse& response);
};
#endif // HELLOWORLDREQUESTHANDLER_H
4.2.4 HelloworldController.cpp
#include "HelloworldController.h"
#include "ResponseUtils.h"
HelloworldController::HelloworldController(QObject *parent):HttpRequestHandler(parent)
{
}
void HelloworldController::service(HttpRequest& request, HttpResponse& response)
{
ResponseUtils::success(response,"123456", "Hello world!");
}
4.2.5 其它
由于该项目涉及到的代码比较多,因此这里只对核心代码进行了介绍,关于具体代码实现请参考文章最后给出的链接。
五、 编译运行
Qt
针对不同型号的开发板,提供了不同的Qt
版本,Qt
支持的特性也不同,在《How to Build, Install and Setting Qt Application/zh
》中我们定位到Allwinner H3
:
PU名称 | Qt版本 | 显示驱动 | OpenGL | QtWebEngine | QtMultimedia硬解 | 触摸屏 | 显示屏 | 对应开发板 |
---|---|---|---|---|---|---|---|---|
Allwinner H3 | Qt 4.8.6 | LinuxFB | No | No | No | 单点触摸 | 单屏 | NanoPi-Duo/NanoPi-M1-Plus/NanoPi-M1/NanoPi-NEO-Air/NanoPi-NEO-Core/NanoPi-NEO |
可以看到NanoPi M1
开发板,只能选择LinuxFB
作为平台插件,该插件通过Linux
的fbdev
子系统直接写入帧缓冲区。
5.1 编译
首先将代码拷贝到NanoPi M1
开发板,root
账号密码为fa
;
root@zhengyang:/opt/qt-project# scp -r /opt/qt-project/PlateLCDDisplay root@192.168.100:/opt/
在Allwinner H3
平台的编译:
root@NanoPi-M1-Plus:~# cd /opt/PlateLCDDisplay
root@NanoPi-M1-Plus/opt/PlateLCDDisplay# mkdir build && cd build
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# /usr/local/Trolltech/QtEmbedded-4.8.6-arm/bin/qmake ../PlateLCDDisplay.pro
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# make
在开发板直接编译会出现各种问题,主要是由于Qt6.5
和Qt4.8
之间不兼容,并且我使用了一些高版本的特性,比如:
Qt4.8
没有qInfo()
函数;Qt4.8
不支持QJsonObject
。Qt4.8
中foreach
遍历QStringList
存在错误;
因此我们不得不在ubuntu
宿主机安装Qt4.8
开发环境,并进行代码调整。
5.1.1 安装Qt4.8
这里我在宿主机重新安装Qt4.8
,安装教程参考《Ubuntu
安装qt4.8
》;
root@zhengyang:/opt# sudo add-apt-repository ppa:rock-core/qt4
root@zhengyang:/opt# sudo apt update
root@zhengyang:/opt# sudo apt install libqt4-declarative
root@zhengyang:/opt# sudo apt install qt4*
root@zhengyang:/opt# qmake -v
QMake version 2.01a
Using Qt version 4.8.7 in /usr/lib/x86_64-linux-gnu
qmake
位于/lib/x86_64-linux-gnu/qt4/bin/qmake
。
如果想卸载Qt4.8
:
root@zhengyang:/opt# sudo apt-get remove qtcreator
root@zhengyang:/opt# sudo apt-get remove qt4*
5.1.2 安装Qt Creator4.2
然后去安装Qt Creator4.2
,具体参考《linux
上安装Qt4.8.4
+QtCreator4.2.0
》;
root@zhengyang:/opt# wget https://download.qt.io/archive/qtcreator/4.2/4.2.0/qt-creator-opensource-linux-x86_64-4.2.0.run
需要在ubuntu
桌面环境,打开终端运行如下命令安装Qt Creator
到/opt/qtcreator-4.2.0
:
zhengyang@zhengyang:~/桌面$ cd /opt
zhengyang@zhengyang:/opt# sudo chmod 777 qt-creator-opensource-linux-x86_64-4.2.0.run
zhengyang@zhengyang:/opt# sudo ./qt-creator-opensource-linux-x86_64-4.2.0.run
运行并配置Qt 4.8
桌面开发套件,这里就不过多介绍了;
zhengyang@zhengyang:/opt$ sudo /opt/qtcreator-4.2.0/bin/qtcreator
打开PlateLCDDisplay
,进行代码调整,使其能够编译通过。
5.1.3 错误一(QT_X11_NO_MITSHM
)
如果在ubuntu
宿主机编译后程序运行出现如下错误:
X Error: BadShmSeg (invalid shared segment parameter) 128
Extension: 131 (MIT-SHM)
Minor opcode: 2 (X_ShmDetach)
Resource id: 0x2c0000c
编辑环境:
root@zhengyang:/opt# vim /etc/environment
在最后一行添加:QT_X11_NO_MITSHM=1
,保存后重启编辑器并运行:
root@zhengyang:/opt# source /etc/environment
5.1.4 错误二(ui_mainwindow.h
)
确保程序可以在ubuntu
宿主机正常运行后,我们将代码拷贝到开发板,进行编译测试;
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# /usr/local/Trolltech/QtEmbedded-4.8.6-arm/bin/qmake ../PlateLCDDisplay.pro
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# make
/usr/local/Trolltech/QtEmbedded-4.8.6-arm/bin/uic ../mainwindow.ui -o ui_mainwindow.h
uic: option to generate cpp code not compiled in
File '../mainwindow.ui' is not valid
make: *** [Makefile:439: ui_mainwindow.h] Error 1
这个错误我也不清楚是啥错误,但是看着像是uic
的问题,这里我直接将ubuntu
宿主机编译得到的ui_mainwindow.h
文件拷贝到开发板:
root@zhengyang:/opt/qt-project/build-PlateLCDDisplay-Qt4_8_7-Debug# scp ui_mainwindow.h root@192.168.100:/opt/PlateLCDDisplay/build
或者直接创建该文件:
/********************************************************************************
** Form generated from reading UI file 'mainwindow.ui'
**
** Created by: Qt User Interface Compiler version 4.8.7
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_MAINWINDOW_H
#define UI_MAINWINDOW_H
#include <QtCore/QVariant>
#include <QtGui/QAction>
#include <QtGui/QApplication>
#include <QtGui/QButtonGroup>
#include <QtGui/QHeaderView>
#include <QtGui/QMainWindow>
#include <QtGui/QWidget>
QT_BEGIN_NAMESPACE
class Ui_MainWindow
{
public:
QAction *actionDdd;
QAction *actionDdd_2;
QWidget *centralwidget;
void setupUi(QMainWindow *MainWindow)
{
if (MainWindow->objectName().isEmpty())
MainWindow->setObjectName(QString::fromUtf8("MainWindow"));
MainWindow->resize(359, 557);
actionDdd = new QAction(MainWindow);
actionDdd->setObjectName(QString::fromUtf8("actionDdd"));
actionDdd_2 = new QAction(MainWindow);
actionDdd_2->setObjectName(QString::fromUtf8("actionDdd_2"));
centralwidget = new QWidget(MainWindow);
centralwidget->setObjectName(QString::fromUtf8("centralwidget"));
MainWindow->setCentralWidget(centralwidget);
retranslateUi(MainWindow);
QMetaObject::connectSlotsByName(MainWindow);
} // setupUi
void retranslateUi(QMainWindow *MainWindow)
{
MainWindow->setWindowTitle(QApplication::translate("MainWindow", "MainWindow", 0, QApplication::UnicodeUTF8));
actionDdd->setText(QApplication::translate("MainWindow", "ddd", 0, QApplication::UnicodeUTF8));
actionDdd_2->setText(QApplication::translate("MainWindow", "ddd", 0, QApplication::UnicodeUTF8));
} // retranslateUi
};
namespace Ui {
class MainWindow: public Ui_MainWindow {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_MAINWINDOW_H
5.1.5 错误三(Makfile
)
通过vim
修改Makefile
文件,执行如下命令将arm-linux-
全部替换成空字符串;
:%s/arm-linux-//g
再次编译程序,得到PlateLCDDisplay
可执行文件;
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# make
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# ll PlateLCDDisplay
-rwxr-xr-x 1 root root 346208 Mar 10 10:49 PlateLCDDisplay*
5.2 运行
如果需要将屏幕旋转,则配置:
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# export QWS_DISPLAY='Transformed:Rot90'
root@NanoPi-M1-Plus/opt/PlateLCDDisplay/build# ./PlateLCDDisplay -qws
其中:
QWS_DISPLAY
:可设置角度值为:0, 90, 180, 270
;-qws
:告诉Qt
使用Qt/Embedded
窗口系统,而不是默认的窗口系统(比如X11
)。
注意:如果运行过程出现如下错误,参考下文介绍。
5.2.1 中文乱码
程序运行之后,访问welcome
服务请求,日志文件中中文出现乱码。
(1) 修改main.cpp
文件;
#include "mainwindow.h"
#include <QApplication>
#include <QTextCodec>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置全局的编解码器为UTF-8
QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));
QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));
MainWindow w;
w.show();
return a.exec();
}
前面我们已经将源码文件将均采用UTF-8
编码,这里以使用QTextCodec::setCodecForCStrings
和QTextCodec::setCodecForLocale
来设置全局的编解码器,以便在整个应用程序中统一解决乱码问题。
这样可以确保所有的输入和输出都按照指定的字符编码进行处理,而不需要在每个地方都手动进行编码转换。
(2) 查看系统所有可用的locale
:
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# locale -a
C
C.utf8
POSIX
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IL
en_IL.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
可以看到并没有zh_CN.utf8
,输入以下命令:
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# dpkg-reconfigure locales
执行之后可以使用空格选择,Tab
键跳转光标,这里用空格选中 zh_CN.UTF-8
:
添加环境变量到.bashrc
,这个文件主要用于设置系统的locale
;
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# echo "export LC_ALL=zh_CN.UTF-8" >> ~/.bashrc
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# echo "export LANG=zh_CN.UTF-8" >> ~/.bashrc
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# echo "export LANGUAGE=zh_CN.UTF-8" >> ~/.bashrc
使配置立即生效:
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# source ~/.bashrc
5.2.2 图片无法保存/加载问题
在开发板环境执行如下代码保存图片会失败,但是在ubuntu
宿主机环境并不会失败;比如PlateController.cpp
文件如下代码;
QImage image;
if(image.loadFromData(byteArray,"jpg")) {
LOG_Info << "Image loaded successfully.";
} else {
LOG_Info << "Failed to load image.";
}
// 保存图像为文件
QDir dir(HttpServerManager::getInstance()->getBgPath());
QString filePath = dir.filePath(advName + ".jpg");
bool saved = image.save(filePath);
if (saved) {
LOG_Info << "Image " << filePath << " saved successfully!";
ResponseUtils::success(response, requestid);
return;
} else {
LOG_Info << "Image " << filePath << " saved failed!";
ResponseUtils::fail(response, nullptr);
return;
}
QImage
支持很多图像格式,有些是默认支持的,如png
,bmp
等。有些需要加载插件后才能支持,如jpg
,tif
等。
这里我们在程序中追加如下代码打印出QImage
默认支持png
格式:
LOG_Debug << "support format: " << QImageReader::supportedImageFormats();
ubuntu
宿主机环境打印出QImage
默认支持png
格式:
12.03.2024 22:53:27.916 0 DEBUG 0x7f3f7f21ef80 Ip:{remoteIp} ../PlateLCDDisplay/mainwindow.cpp 82 support format: ("bmp", "gif", "ico", "jpeg", "jpg", "mng", "pbm", "pgm", "png", "ppm", "svg", "svgz", "tga", "tif", "tiff", "xbm", "xpm")
在开发板环境打印出QImage
默认支持png
格式:
12.03.2024 14:46:08.566 0 DEBUG 0xffffffffb6f32020 Ip:{remoteIp} ../PlateLCDDisplay/mainwindow.cpp 82 support format: ("bmp", "gif", "ico", "mng", "pbm", "pgm", "png", "ppm", "svg", "svgz", "tga", "tif", "tiff", "xbm", "xpm")
可以看到在开发板环境并不支持jpeg
、jpg
格式,因此需要加载插件后,才能读写jpg
文件。
5.2.3 指定插件
插件位于/usr/local/Trolltech/QtEmbedded-4.8.6-arm/plugins/imageformats/
目录,在该目录下可以看到有libqjpeg.so
动态库;
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# ll /usr/local/Trolltech/QtEmbedded-4.8.6-arm/plugins/imageformats/
-rwxr-xr-x 1 root root 19996 7月 20 2018 libqgif.so*
-rwxr-xr-x 1 root root 19632 7月 20 2018 libqico.so*
-rwxr-xr-x 1 root root 21052 7月 20 2018 libqjpeg.so*
-rwxr-xr-x 1 root root 283844 7月 20 2018 libqmng.so*
-rwxr-xr-x 1 root root 14724 7月 20 2018 libqsvg.so*
-rwxr-xr-x 1 root root 13576 7月 20 2018 libqtga.so*
-rwxr-xr-x 1 root root 319076 7月 20 2018 libqtiff.so*
设置 LD_LIBRARY_PATH
环境变量,将 /usr/local/Trolltech/QtEmbedded-4.8.6-arm/lib
目录添加到了动态链接库搜索路径中;
设置QT_QPA_PLATFORM_PLUGIN_PATH
环境变量,将将 /usr/local/Trolltech/QtEmbedded-4.8.6-arm/plugins
目录添加到了插件搜索路径中;
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# vim /etc/profile
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/Trolltech/QtEmbedded-4.8.6-arm/lib
export QT_PLUGIN_PATH=$QT_PLUGIN_PATH:/usr/local/Trolltech/QtEmbedded-4.8.6-arm/plugins
root@NanoPi-M1-Plus:/opt/PlateLCDDisplay/build# source /etc/profile
不过在实际测试中发现,这么做并没有任何效果,因此最终不得不将程序中涉及的jpg
相关更换成png
格式。
5.3 测试
这里我们通过IDEA
中的Http
客户端发起请求,这里我们编写了一些Http
请求测试用例,下载地址如下:https://files.cnblogs.com/files/zyly/PlateLCDDisplay.zip?t=1710328134&download=true
。
5.3.1 advLoadImage
服务请求
程序会将请求日志记录在/opt/PlateLCDDisplay/logs/webapp.log
文件下,比如我们通过调用/API
接口上传背景图片(图片必须是png
格式);
POST http://192.168.0.100:8080/API
Content-Type: application/json
{
"requestid": "ba6317c5-f55d-b9af-031e-8d0438416f23",
"servicename": "advLoadImage",
"data": {
"ImageName": "bg0.png",
"ImageUrl": null,
"AdvName": "bg0",
"Data": "iVBORw0KGgoAAAANSUhE......."
}
}
注意:Data
为png
图片的base64
编码字符串,可以通过https://www.lddgo.net/convert/imagebasesix
网站实现转换,需要将转换结果的固定前缀移除。
请求执行会在日志文件记录如下信息;
13.03.2024 11:45:34.634 0 DEBUG 0xffffffffb2881380 Ip:{remoteIp} HttpConnectionHandler (0xb4101ba8): handle new connection
13.03.2024 11:45:34.636 0 DEBUG 0xffffffffb2881380 Ip:{remoteIp} HttpRequest: from 192.168.0.110: POST /API HTTP/1.1
13.03.2024 11:45:34.654 0 DEBUG 0xffffffffb2881380 Ip:{remoteIp} HttpConnectionHandler (0xb4101ba8): received request
13.03.2024 11:45:34.655 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/RequestMapper.cpp 70 Client IP Address: "192.168.0.110"
13.03.2024 11:45:34.655 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/RequestMapper.cpp 73 RequestMapper: path= /API
13.03.2024 11:45:35.292 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/controller/PlateController.cpp 120 Image loaded successfully.
13.03.2024 11:45:36.714 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/controller/PlateController.cpp 132 Image "/opt/PlateLCDDisplay/build/../bg/bg0.png" saved successfully!
13.03.2024 11:45:36.715 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/ResponseUtils.cpp 57 "{"data":"","msg":"success","requestid":"ba6317c5-f55d-b9af-031e-8d0438416f23","status":true,"version":"V1.0.100.0"}"
13.03.2024 11:45:36.719 0 DEBUG 0xffffffffb2881380 Ip:192.168.0.110 ../PlateLCDDisplay/modules/httpServerManager/RequestMapper.cpp 88 RequestMapper: finished request
13.03.2024 11:45:36.720 0 DEBUG 0xffffffffb2881380 Ip:{remoteIp} HttpConnectionHandler (0xb4101ba8): finished request
13.03.2024 11:45:39.063 0 DEBUG 0xffffffffb2881380 Ip:{remoteIp} HttpConnectionHandler (0xb4101ba8): disconnected
该请求会将背景图片保存到"/opt/PlateLCDDisplay/bg
目录下。
5.3.2 welcome
服务请求
我们通过调用/API
接口发送welcome
服务请求;
### Send welcome with backgroup 1
POST http://192.168.0.100:8080/API
Content-Type: application/json
{
"requestid": "d3ab8da1-5300-5cca-05a7-72030da866d4",
"servicename": "welcome",
"data": {
"carmark": "粤 B12345",
"line1": "欢迎光临",
"line2": "临时车",
"line3": "请入场停车",
"voice": "粤 B12345,临时车,欢迎光临",
"companyName": "深圳市 XX 科技有限公司",
"pictureUsed": "1"
}
}
下图是welcome
服务请求界面的显示效果:
5.4 开机自启动
以运行PlateLCDDisplay
程序为例,假设它放在/opt/PlateLCDDisplay/build
目录,则你可以编辑/etc/rc.local
文件,确否有以下内容:
export QWS_DISPLAY='Transformed:Rot90'
/opt/PlateLCDDisplay/build/PlateLCDDisplay -qws&
5.5 支持修改IP
首先我们通过修改/etc/network/interfaces
设置静态IP
信息,比如:
auto eth0
iface eth0 inet static
address 192.168.0.100
netmask 255.255.255.0
gateway 192.168.0.1
在终端中执行以下命令可以重新加载网络服务,使配置文件的更改立即生效:
root@NanoPi-M1-Plus:~# systemctl restart networking
root@NanoPi-M1-Plus:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 02:81:5f:d6:08:90 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.100/24 brd 192.168.0.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::81:5fff:fed6:890/64 scope link
valid_lft forever preferred_lft forever
3: wlan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
link/ether 50:41:1c:e2:24:76 brd ff:ff:ff:ff:ff:ff
接着新增通过Http
接口配置/etc/network/interfaces
,编写IPsettingcontroller.h
、IPsettingcontroller.cpp
源码。
5.5.1 IPsettingcontroller.h
#ifndef IPSETTINGCONTROLLER_H
#define IPSETTINGCONTROLLER_H
#include "httprequesthandler.h"
#include "logging.h"
using namespace stefanfrings;
class IPsettingcontroller: public HttpRequestHandler
{
public:
IPsettingcontroller(QObject *parent = 0);
~IPsettingcontroller();
public:
void service(HttpRequest& request, HttpResponse& response);
};
#endif // IPSETTINGCONTROLLER_H
5.5.2 IPsettingcontroller.cpp
#include "IPsettingcontroller.h"
#include "ResponseUtils.h"
#include <QProcess>
#include <QHostAddress>
#include <QNetworkAddressEntry>
#include <parser.h>
#include <serializer.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <error.h>
#include <net/route.h>
#include <unistd.h>
#include <QTextStream>
#include <QProcess>
using namespace QJson;
bool validateIPAddress(const QString &ip)
{
QHostAddress address(ip);
return address.isNull() ? false : true;
}
bool validateNetmask(const QString &netmask)
{
// 将输入字符串转换为QHostAddress对象
QHostAddress address(netmask);
if (address.isNull()) {
// 如果转换失败,说明不是有效的IP地址格式
return false;
}
// 检查是否是有效的子网掩码
quint32 ipv4Addr = address.toIPv4Address();
bool isValidNetmask = true;
quint32 mask = 0xFFFFFFFF;
// 计算子网掩码的二进制反码
for (int i = 0; i < 32; ++i) {
if (!((mask << i) & ipv4Addr)) {
isValidNetmask = false;
break;
}
}
return isValidNetmask;
}
bool validateGateway(const QString &gateway)
{
QHostAddress address(gateway);
return address.isNull() ? false : true;
}
/**
* @brief 根据网口名称设置IP地址.
* @param *ethName 网卡名字符
* @retval None.
* @notes None.
*/
int setIfAddr(const char *ethName, const char *ipAddr, const char *mask, const char *gateway)
{
int fd;
int ret = 0;
struct ifreq ifr;
struct sockaddr_in *sin;
struct rtentry rt;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd < 0)
{
perror(" socket error");
return fd;
}
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, ethName);
sin = (struct sockaddr_in *)&ifr.ifr_addr;
sin->sin_family = AF_INET;
/* IP地址设置 */
ret = inet_aton(ipAddr, &(sin->sin_addr));
if(ret < 0)
{
LOG_Error << "inet_aton error";
goto error_exit;
}
ret = ioctl(fd, SIOCSIFADDR, &ifr);
if(ret < 0)
{
LOG_Error << "ioctl SIOCSIFADDR error";
goto error_exit;
}
/* 子网掩码设置 */
ret = inet_aton(mask, &(sin->sin_addr));
if(ret < 0)
{
LOG_Error << "inet_aton error";
goto error_exit;
}
ret = ioctl(fd, SIOCSIFNETMASK, &ifr);
if(ret < 0)
{
LOG_Error << "ioctl SIOCSIFNETMASK error";
goto error_exit;
}
/* 网关 */
memset(&rt, 0, sizeof(struct rtentry));
memset(sin, 0, sizeof(struct sockaddr_in));
sin->sin_family = AF_INET;
sin->sin_port = 0;
ret = inet_aton(gateway, &(sin->sin_addr));
if(ret < 0)
{
LOG_Error << "inet_aton gateway error";
}
memcpy(&rt.rt_gateway, sin, sizeof(struct sockaddr_in));
((struct sockaddr_in *)&rt.rt_dst)->sin_family = AF_INET;
((struct sockaddr_in *)&rt.rt_genmask)->sin_family = AF_INET;
rt.rt_flags = RTF_GATEWAY;
/* 设置网关,如果所设置的网关与现有网关一样,会报File exists */
ret = ioctl(fd, SIOCADDRT, &rt);
if(ret < 0)
{
LOG_Error << "ioctl(SIOCADDRT) error in set_default_route\n";
goto error_exit;
}
error_exit:
close(fd);
return ret;
}
IPsettingcontroller::IPsettingcontroller(QObject *parent):HttpRequestHandler(parent)
{
LOG_Debug << "IpSettingController: created";
}
IPsettingcontroller::~IPsettingcontroller()
{
LOG_Debug << "IpSettingController: deleted";
}
void IPsettingcontroller::service(HttpRequest& request, HttpResponse& response)
{
QByteArray body = request.getBody();
Parser parser;
bool ok;
//解析json对象
QVariant var = parser.parse(body, &ok);
if(!ok)
{
ResponseUtils::fail(response, nullptr);
return;
}
if(var.type() == QVariant::Map)
{
QVariantMap result = var.toMap();
QString requestid = result["requestid"].toString();
QString ip = result["ip"].toString();
QString netmask = result["netmask"].toString();
QString gateway = result["gateway"].toString();
LOG_Info << "Setting ip:" << ip;
LOG_Info << "Setting netmask:" << netmask;
LOG_Info << "Setting gateway:" << gateway;
if (validateIPAddress(ip)) {
LOG_Debug << "IP地址合法";
} else {
LOG_Debug << "IP地址不合法";
ResponseUtils::fail(response, requestid, "IP地址不合法");
return;
}
if (validateNetmask(netmask)) {
LOG_Debug << "子网掩码合法";
} else {
LOG_Debug << "子网掩码不合法";
ResponseUtils::fail(response, requestid, "子网掩码不合法");
return;
}
if (validateGateway(gateway)) {
LOG_Debug << "网关合法";
} else {
LOG_Debug << "网关不合法";
ResponseUtils::fail(response, requestid, "网关不合法");
return;
}
// 执行系统命令来修改本机 IP、子网掩码和网关, 重启会失效
// if(setIfAddr("eth0", qPrintable(ip), qPrintable(netmask), qPrintable(gateway)) < 0)
// {
// ResponseUtils::fail(response, requestid, "SetIfAddr fail !");
// return;
// }
// 清空文件内容
QFile file("/etc/network/interfaces");
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
LOG_Debug << "Failed to clear /etc/network/interfaces";
ResponseUtils::fail(response, requestid, "Failed to clear /etc/network/interfaces");
return;
}
file.close();
// 配置网络接口 eth0
QFile configFile("/etc/network/interfaces");
if (!configFile.open(QIODevice::Append | QIODevice::Text))
{
LOG_Debug <<"Failed to open /etc/network/interfaces for appending";
ResponseUtils::fail(response, requestid, "Failed to open /etc/network/interfaces for appending");
return;
}
QTextStream out(&configFile);
out << "auto eth0\n"
"iface eth0 inet static\n"
"address " << ip << "\n"
"netmask " << netmask << "\n"
"gateway " << gateway << "\n";
configFile.close();
// 创建一个QProcess对象
QProcess process;
// 通过QProcess执行重启网络服务的系统命令
process.start("sudo systemctl restart networking");
process.waitForFinished(-1); // 等待进程完成
ResponseUtils::success(response, requestid);
return;
}
ResponseUtils::fail(response, nullptr);
}
5.5.3 测试
比如开发板当前IP
地址为192.168.0.100
,我希望修改为192.168.0.101
,执行如下请求;
POST http://192.168.0.100:8080/ip
Content-Type: application/json
{
"requestid": "d3ab8da1-5300-5cca-05a7-72030da866d4",
"ip":"192.168.0.101",
"netmask": "255.255.255.0",
"gateway":"192.168.0.1"
}
注意:由于网卡IP
被修改了,该Http
执行成功后不会有返回信息。
设置完成后,可以通过以下命令查看当前网络设备信息:
root@NanoPi-M1-Plus:~# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 02:81:5f:d6:08:90 brd ff:ff:ff:ff:ff:ff
inet 192.168.0.101/24 brd 192.168.0.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::81:5fff:fed6:890/64 scope link
valid_lft forever preferred_lft forever
3: wlan0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000
link/ether 50:41:1c:e2:24:76 brd ff:ff:ff:ff:ff:ff
可以看到网络接口eth0 IP
地址已经被修改为192.168.0.101
;我们可以通过Http
请求验证IP
已经修改成功;
POST http://192.168.0.101:8080/API
Content-Type: application/json
{
"requestid": "d3ab8da1-5300-5cca-05a7-72030da866d4",
"servicename": "welcome",
"data": {
"carmark": "粤 B12345",
"line1": "欢迎光临",
"line2": "临时车",
"line3": "请入场停车",
"voice": "粤 B12345,临时车,欢迎光临",
"companyName": "深圳市 XX 科技有限公司",
"pictureUsed": "2"
}
}
六、源码下载
参考文献
[1] 嵌入式Qt
程序启动参数-qws
不需要X11
桌面系统
[2] NanoPi M1/zh
[3] How to Build, Install and Setting Qt Application/zh
[4] QtWebApp
介绍、下载和搭建基础封装http
轻量级服务器Demo