【C++/Qt】Qt 网络工具中的输入校验设计:IP、端口、URL 和空内容判断

在 Qt 网络调试工具中,很多功能都需要用户输入参数,例如 TCP 的 IP 和端口、HTTP 的 URL、MQTT 的 Topic、邮件模块的账号密码等。如果这些输入内容没有经过校验就直接参与连接、发送或读取操作,很容易出现连接失败、程序逻辑混乱,甚至导致一些难以定位的问题。

因此,在实际开发中,比较推荐把输入校验逻辑单独封装成一个工具类,例如 InputValidator。这样按钮函数只负责业务流程,而参数是否合法交给统一的校验类处理,代码会更加清晰,也更方便后期维护。

本文主要围绕 Qt 网络工具中的输入校验设计进行整理,重点讲解 IP、端口、URL、空内容等常见参数的校验方式。

一、为什么要把输入校验单独封装出来

在网络工具中,很多按钮点击函数都会涉及参数判断。例如点击"连接服务器"时,需要判断 IP 和端口;点击"发送数据"时,需要判断发送内容是否为空;点击"发送 HTTP 请求"时,需要判断 URL 是否有效。

如果所有判断都直接写在按钮函数里,代码很容易变成这样:

cpp 复制代码
void Widget::on_btnConnect_clicked()
{
    QString ip = ui->lineEditIp->text();
    QString port = ui->lineEditPort->text();

    if (ip.isEmpty()) {
        QMessageBox::warning(this, "错误", "IP不能为空");
        return;
    }

    if (port.isEmpty()) {
        QMessageBox::warning(this, "错误", "端口不能为空");
        return;
    }

    // 后面还有连接逻辑......
}

这种写法虽然能用,但是问题也比较明显:

cpp 复制代码
1. 每个模块都要重复写类似判断;
2. 按钮函数里既有参数校验,又有网络连接逻辑;
3. 校验规则一旦修改,需要到多个地方改;
4. 错误提示格式不统一;
5. 代码越写越长,不方便维护。

所以更好的方式是:把通用校验逻辑抽出来,封装成一个 InputValidator 类。

这个类不负责连接服务器,也不负责发送数据,只负责判断输入是否合法。

可以先定义一个统一的返回结果结构:

cpp 复制代码
class InputValidator
{
public:
    // 校验结果结构体
    // valid 表示是否校验通过
    // message 表示校验失败时的提示信息
    struct ValidationResult
    {
        bool valid;
        QString message;

        ValidationResult(bool valid = true,
                         const QString &message = QString())
            : valid(valid), message(message)
        {
        }
    };
};

这样每个校验函数都可以返回同一种结果:

cpp 复制代码
ValidationResult(true, "")              // 表示校验通过
ValidationResult(false, "端口号无效")   // 表示校验失败,并给出原因

这种设计的好处是:

调用者不需要关心校验函数内部怎么判断,只需要看 valid 是否为 true,如果失败就把 message 显示出来。

二、端口校验:判断范围是否在 1 到 65535

网络通信中,端口号是最常见的输入参数之一。TCP、UDP、HTTP、WebSocket、MQTT 等模块都离不开端口。

端口本质上是一个整数,合法范围通常是:1 ~ 65535

如果用户输入的是 0、负数、空字符串,或者超过 65535 的数字,都应该认为是不合法的。

端口校验函数可以这样写:

cpp 复制代码
InputValidator::ValidationResult
InputValidator::validatorPort(int port)
{
    // 端口号不能小于等于 0
    // 0 一般不作为用户主动填写的通信端口
    if (port <= 0) {
        return ValidationResult(false, "端口号不能小于或等于 0");
    }

    // 端口号最大不能超过 65535
    // TCP/UDP 端口范围是 0~65535,但用户配置时通常使用 1~65535
    if (port > 65535) {
        return ValidationResult(false, "端口号不能大于 65535");
    }

    // 能走到这里,说明端口范围合法
    return ValidationResult(true);
}

在界面按钮函数里使用时,可以这样写:

cpp 复制代码
void FormTcpClient::on_btnConnect_clicked()
{
    // 从界面获取端口文本
    QString portText = ui->lineEditPort->text().trimmed();

    // 先判断是否为空
    if (portText.isEmpty()) {
        QMessageBox::warning(this, "输入错误", "端口号不能为空");
        return;
    }

    // 将 QString 转成 int
    int port = portText.toInt();

    // 调用统一的端口校验函数
    auto result = InputValidator::validatorPort(port);

    // 如果校验失败,直接提示并结束函数
    if (!result.valid) {
        QMessageBox::warning(this, "输入错误", result.message);
        return;
    }

    // 校验通过后,再执行真正的连接逻辑
    // socket->connectToHost(ip, port);
}

这里的重点是:

cpp 复制代码
auto result = InputValidator::validatorPort(port);

按钮函数不用再关心端口的具体范围判断,只要把端口传给校验类即可。

这样写以后,TCP、UDP、MQTT、邮件模块都可以复用这一套端口校验逻辑。

三、IP 地址校验:判断格式是否符合 IPv4 规则

IP 地址也是网络工具中非常常见的输入。比如:

cpp 复制代码
127.0.0.1
192.168.1.10
8.8.8.8

一个简单的 IPv4 地址通常由四段数字组成,每一段范围是 0~255,中间用英文点号连接。

因此 IP 校验可以使用正则表达式完成:

cpp 复制代码
InputValidator::ValidationResult
InputValidator::validatorIP(const QString &ip)
{
    // 去掉首尾空格,避免用户多输入空格导致误判
    QString value = ip.trimmed();

    // 先判断是否为空
    if (value.isEmpty()) {
        return ValidationResult(false, "IP 地址不能为空");
    }

    // IPv4 正则表达式:
    // 每一段允许 0~255
    // 四段之间使用英文点号连接
    QRegularExpression regExp(
        R"(^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}"
        R"(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$)"
    );

    // 使用正则表达式匹配 IP
    QRegularExpressionMatch match = regExp.match(value);

    // 如果没有匹配成功,说明 IP 格式不正确
    if (!match.hasMatch()) {
        return ValidationResult(false, "IP 地址格式不正确");
    }

    // 匹配成功,说明 IP 合法
    return ValidationResult(true);
}

这段代码里面最核心的是正则表达式:

cpp 复制代码
25[0-5]      // 匹配 250~255
2[0-4]\d     // 匹配 200~249
1\d\d        // 匹配 100~199
[1-9]?\d     // 匹配 0~99

组合起来以后,就可以限制每一段 IP 的范围不能超过 255

在 TCP 或 UDP 连接按钮中,可以这样调用:

cpp 复制代码
void FormUdpClient::on_btnSend_clicked()
{
    QString ip = ui->lineEditIp->text().trimmed();

    // 校验目标 IP
    auto ipResult = InputValidator::validatorIP(ip);

    if (!ipResult.valid) {
        QMessageBox::warning(this, "输入错误", ipResult.message);
        return;
    }

    // IP 校验通过后,再继续执行端口校验和发送逻辑
}

这样做的好处是:

不管是 TCP 客户端、UDP 客户端,还是服务器监听地址,只要涉及 IP 判断,都可以使用同一个函数,避免每个模块重复写正则。

四、URL、主机名和内容为空的校验

除了 IP 和端口,网络工具中还经常需要校验 URL、主机名、发送内容等参数。

HTTP 模块一般需要校验 URL,例如:

cpp 复制代码
http://example.com
https://www.baidu.com

URL 校验可以借助 Qt 自带的 QUrl

cpp 复制代码
InputValidator::ValidationResult
InputValidator::validatorUrl(const QString &urlText)
{
    // 去掉首尾空格
    QString value = urlText.trimmed();

    // 判断 URL 是否为空
    if (value.isEmpty()) {
        return ValidationResult(false, "URL 不能为空");
    }

    // 使用 QUrl 解析 URL
    QUrl url(value);

    // 判断 URL 是否合法,并且必须包含协议和主机名
    // 例如 http://example.com 才算完整 URL
    if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) {
        return ValidationResult(false, "URL 格式不正确");
    }

    return ValidationResult(true);
}

这里为什么要判断 scheme()

因为用户可能只输入:

cpp 复制代码
www.baidu.com

这虽然看起来像网址,但对程序来说并不是一个完整 URL。更标准的写法应该是:

cpp 复制代码
https://www.baidu.com

所以在 HTTP 请求中,最好要求用户输入完整地址。

主机名校验可以用于 MQTT Broker、邮箱服务器等场景,例如:

cpp 复制代码
broker.emqx.io
smtp.qq.com
imap.qq.com

主机名不一定是 IP,所以不能直接套用 IP 校验函数。可以设计一个单独的主机名校验:

cpp 复制代码
InputValidator::ValidationResult
InputValidator::validatorHostName(const QString &host)
{
    QString value = host.trimmed();

    if (value.isEmpty()) {
        return ValidationResult(false, "服务器地址不能为空");
    }

    // 主机名可以是 IP,也可以是域名
    // 如果是合法 IP,直接通过
    if (validatorIP(value).valid) {
        return ValidationResult(true);
    }

    // 简单域名格式校验
    // 支持 example.com、broker.emqx.io 这类形式
    QRegularExpression regExp(
        R"(^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"
    );

    if (!regExp.match(value).hasMatch()) {
        return ValidationResult(false, "服务器地址格式不正确");
    }

    return ValidationResult(true);
}

除了地址类参数,发送内容也需要判断。例如 TCP、UDP、WebSocket、MQTT 发布消息时,如果发送框为空,就没有必要继续执行发送逻辑。

可以封装一个通用的非空判断函数

cpp 复制代码
InputValidator::ValidationResult
InputValidator::validatorNotEmpty(const QString &text,
                                  const QString &fieldName)
{
    // trim 后再判断,避免用户只输入空格
    if (text.trimmed().isEmpty()) {
        return ValidationResult(false, fieldName + "不能为空");
    }

    return ValidationResult(true);
}

使用时可以这样写:

cpp 复制代码
QString sendText = ui->textEditSend->toPlainText();

auto contentResult =
    InputValidator::validatorNotEmpty(sendText, "发送内容");

if (!contentResult.valid) {
    QMessageBox::warning(this, "输入错误", contentResult.message);
    return;
}

这种写法比直接写:

cpp 复制代码
if (sendText.isEmpty())

更规范一些,因为它可以统一返回错误信息,也可以避免只输入空格的情况。

五、在业务模块中的使用方式

封装 InputValidator 的最终目的,不是为了让校验类本身多复杂,而是让业务模块的代码更清楚。

例如一个 UDP 发送按钮,正常流程应该是:

cpp 复制代码
读取界面参数
        ↓
校验 IP
        ↓
校验端口
        ↓
校验发送内容
        ↓
发送数据
        ↓
输出日志

代码可以整理成下面这样:

cpp 复制代码
void FormUdpClient::on_btnSend_clicked()
{
    // 1. 从界面获取参数
    QString targetIp = ui->lineEditIp->text().trimmed();
    QString portText = ui->lineEditPort->text().trimmed();
    QString sendText = ui->textEditSend->toPlainText();

    // 2. 校验 IP 地址
    auto ipResult = InputValidator::validatorIP(targetIp);
    if (!ipResult.valid) {
        QMessageBox::warning(this, "输入错误", ipResult.message);
        return;
    }

    // 3. 校验端口是否为空
    auto portEmptyResult =
        InputValidator::validatorNotEmpty(portText, "端口号");
    if (!portEmptyResult.valid) {
        QMessageBox::warning(this, "输入错误", portEmptyResult.message);
        return;
    }

    // 4. 校验端口范围
    int port = portText.toInt();
    auto portResult = InputValidator::validatorPort(port);
    if (!portResult.valid) {
        QMessageBox::warning(this, "输入错误", portResult.message);
        return;
    }

    // 5. 校验发送内容
    auto textResult =
        InputValidator::validatorNotEmpty(sendText, "发送内容");
    if (!textResult.valid) {
        QMessageBox::warning(this, "输入错误", textResult.message);
        return;
    }

    // 6. 所有参数校验通过后,再执行真正的 UDP 发送
    QByteArray data = sendText.toUtf8();

    qint64 len = udpSocket->writeDatagram(data,
                                          QHostAddress(targetIp),
                                          static_cast<quint16>(port));

    if (len == -1) {
        QMessageBox::warning(this, "发送失败",
                             udpSocket->errorString());
        return;
    }

    // 7. 发送成功后输出日志
    ui->textEditLog->append(
        QString("发送到 [%1:%2]:%3")
            .arg(targetIp)
            .arg(port)
            .arg(sendText)
    );
}

这样写以后,按钮函数的结构就比较清楚:

cpp 复制代码
前半部分:负责参数校验
后半部分:负责真正的发送逻辑

而且每一种校验都可以复用:

cpp 复制代码
InputValidator::validatorIP(ip);
InputValidator::validatorPort(port);
InputValidator::validatorUrl(url);
InputValidator::validatorHostName(host);
InputValidator::validatorNotEmpty(text, "发送内容");

如果后期发现端口规则、URL 规则、主机名规则需要调整,只需要修改 InputValidator 类,不需要到每个按钮函数里一个一个改。

总结

在 Qt 网络工具开发中,输入校验虽然不是最复杂的功能,但它直接影响程序的稳定性和用户体验。IP、端口、URL、主机名、发送内容这些参数如果不提前判断,就可能导致连接失败、发送失败,或者出现不明确的错误提示。

通过封装 InputValidator 工具类,可以把分散在各个按钮函数中的判断逻辑统一管理起来。按钮函数只需要负责读取界面参数、调用校验函数、执行业务逻辑即可。

整体流程可以总结为:

cpp 复制代码
界面输入参数
        ↓
InputValidator 统一校验
        ↓
返回 ValidationResult
        ↓
校验失败:显示错误信息
        ↓
校验成功:继续执行连接、监听、发送等操作

这种写法的核心价值不是代码量减少多少,而是让网络工具的结构更加清晰。参数校验、业务处理、错误提示各自负责自己的部分,代码后期维护起来会更方便,也更适合在 TCP、UDP、HTTP、WebSocket、MQTT、邮件等多个模块中复用。

0voice · GitHub

相关推荐
liu****1 小时前
第16届国赛蓝桥杯大赛C/C++大学B组
c语言·数据结构·c++·算法·蓝桥杯
nazisami2 小时前
红黑树详解
数据结构·c++·面向对象·红黑树
liulilittle2 小时前
TCP UCP v1.0:当 BBRv1 遇上卡尔曼滤波
网络·网络协议·tcp/ip
kyle~2 小时前
RTPS(Real-Time Publish-Subscribe)---DDS的传输协议
c++·机器人·ros2
189228048612 小时前
NV301固态MT29F32T08GWLBHD6-QJES:B
大数据·服务器·人工智能·科技·缓存
实心儿儿2 小时前
Linux —— 进程间通信 - system V进程间通信 - 共享内存(2)
linux·服务器
TIEM_692 小时前
C++ vector容器全面解析:从入门到精通
开发语言·c++
Irissgwe2 小时前
c++多态
开发语言·c++·多态
lingran__2 小时前
C++_类和对象(上)
开发语言·c++