在 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、邮件等多个模块中复用。