程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

全志H3 - Qt&QtWebApp搭建Http Server(无X11系统)

目前我手里正好有一块NanoPi M1加法板,NanoPi M1(以下简称M1)是友善之臂团队面向创客、嵌入式爱好者,电子艺术家、发烧友等群体推出的又一款完全开源的掌上创客神器,它的大小只有树莓派的大约2/3,可运行DebianUbuntu-MATEUbuntu-CoreAndroid等操作系统。

NanoPi M1采用了全志高性能处理器Allwinner H3,集成以太网、红外接收、视频/音频输出等接口,支持HDMIAVOUT视频输出等功能。

尽管体积很小,设计却紧凑美观。NanoPi M1引出了相当丰富的接口,包括HDMI、以太网、USB-HostUSB-OTGDVP cameraAVOUT(音频+视频)等。而且集成了板载麦克风,红外接收器,并且兼容树莓派GPIO口,并且拥有独立的调试串口等。

File:NanoPi M1-1.jpg

本机的目的主要是在当前系统上编写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:更新时间;

这里我们要做的就是在显示屏上显示servicenamewelcomeadvLoadImage的内容。

1.2.1 welcome
字段 说明 类型 必须
carmark 车牌 String
line1 欢迎回家 string
line2 临时车 int
line3 请入场停车
voice 粤 B12345,临时车,欢迎光临 String
companyName 深圳 XX 科技有限公司
pictureUsed 0~4,对应servicenameadvLoadImage业务发送的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

文件下载方式:通常适用于没有外网的方式,就是操作复杂点,数据包比较大。

网络连接方式方式,只需要提供URLAPP自动下载广告。

示例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。适用于LinuxWindowsMac OSQt Framework支持的许多其他操作系统。QtWebApp包含以下组件:

  • HTTP(S)1.01.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 - linuxQtopencv交叉编译环境搭建》。

这里我直接在宿主机上使用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-8UTF-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

其中:

  • ipportIP和端口参数指定web服务器侦听的IP地址和端口。如果注释掉IP(如上所述),则服务器将侦听所有网络接口。公共web服务器使用端口80,而内部web服务器通常在端口8080上侦听;
  • minThreadscleanupIntervalQtWebApp可以同时处理多个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.hHttpServerManager.cppHttp管理器实现,用于加载web配置,并启动Http Server监听Http请求;
  • HelloworldRequestHandler.hHelloworldRequestHandler.cppHttp请求处理实现;
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及以上级别的消息才会写入日志文件;日志级别有:DEBUGINFOWARNCRITICAL(别名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.cppslot_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三方源码中的qDebugqWarnQFatal等相关系统函数是将日志直接输出到控制待的,使用该日志模块可以截断这些函数输出的日志,并将日志输出到有配置文件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提供的功能不止这些,比如:

四、项目实现

前面我们已经简单的介绍了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.cppservice方法;

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.cppHelloworldController.h文件移动到该文件夹,同时创建PlateController.hPlateController.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);
}

如果我们接收到的servernameadvLoadImage,我们将获取图片base64编码的字符串,解码后并将其保存到./bg路径下;

如果我们接收到的servernamewelcome,我们通过信号与槽机制将获取的内容发送到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作为平台插件,该插件通过Linuxfbdev子系统直接写入帧缓冲区。

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.5Qt4.8之间不兼容,并且我使用了一些高版本的特性,比如:

  • Qt4.8没有qInfo()函数;
  • Qt4.8不支持QJsonObject
  • Qt4.8foreach遍历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::setCodecForCStringsQTextCodec::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支持很多图像格式,有些是默认支持的,如pngbmp等。有些需要加载插件后才能支持,如jpgtif等。

这里我们在程序中追加如下代码打印出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")

可以看到在开发板环境并不支持jpegjpg格式,因此需要加载插件后,才能读写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......."
   }
}

注意:Datapng图片的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.hIPsettingcontroller.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"
  }
}

六、源码下载

大奥特曼打小怪兽 / plate-lcd-display

参考文献

[1] 嵌入式Qt程序启动参数-qws 不需要X11桌面系统

[2] NanoPi M1/zh

[3] How to Build, Install and Setting Qt Application/zh

[4] QtWebApp介绍、下载和搭建基础封装http轻量级服务器Demo

[5] http服务器日志系统介绍、添加日志系统至Demo测试

[6] Writing Web Server Applications with QtWebApp

posted @ 2024-03-04 21:10  大奥特曼打小怪兽  阅读(40)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步