目录
前言
这是我学习Qt做的第三个项目,之前做了记事本和串口调试助手,如果是第一次接触Qt的小白,并且懂点C++语法的,可以从记事本项目看起,步骤非常详细:Qt记事本项目(零基础入门)。本次项目源码在我资源里免费下载!
项目概述
使用QT封装好的类:QTcpServer和QTcpClient,和Linux网络编程类似,写一个服务端和客户端,程序运行结果如下:
(还有一点点小瑕疵,但是主要的功能都实现了,直接调用封装好的函数即可)
开发流程
QT的tcp服务端/客户端的关键步骤
工程建立后,需在.pro文件加入网络权限:
服务端
服务端UI界面设计
如下:
最后全选这些控件和水平布局,再为它们选用垂直布局,效果就和上图一样了。包含关系如下:
为需要的控件命名,写代码的时候根据名字就知道是哪个控件,而不需要一遍遍去看名字。
服务端代码实现
1.构造函数和基础初始化
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setLayout(ui->verticalLayout);
// 初始化TCP服务器
server = new QTcpServer(this);
// 初始化按钮状态:停止监听和发送按键默认不可用
ui->btnStopListen->setEnabled(false);
ui->btnSend->setEnabled(false);
// 为server句柄绑定信号与槽,当有新的连接可用时,会触发绑定的这个槽函数
connect(server, SIGNAL(newConnection()),
this, SLOT(on_newClient_connect())); // 新连接通知
/* 自定义Combo Box绑定鼠标点击事件和Combo Box刷新槽函数
* mcomboBox_refresh 函数后面会讲
* 当鼠标点击自定义Combo Box时会刷新下拉选择框,选择要发送数据的对象
*/
connect(ui->comboBoxChildren, &MyComboBox::on_comboBox_clicked,
this, &Widget::mcomboBox_refresh);
// 获取本机IPv4地址(基础网络功能)
QList<QHostAddress> addresses = QNetworkInterface::allAddresses();
for(QHostAddress address : addresses) {
if(address.protocol() == QAbstractSocket::IPv4Protocol) {
ui->comboBoxAddr->addItem(address.toString()); // 添加到地址选择框
}
}
}
在帮助文档搜索QTcpServer,往下找到 newConnection() 信号:
但我们还没有开启监听,当我们开始监听某个IP地址的某个端口,然后客户端请求连接这个IP地址的这个端口时,才会发来 newConnection() 信号,所以等开启监听后再来看 on_newClient_connect 这个槽函数。
如何获得本机的IP地址?在帮助文档搜索 QNetworkInterface 类,往下找到它的静态共有函数 allAddresses() :
这个函数返回一个 QHostAddress 的列表,之后我们遍历这个列表,将列表里IPV4的地址提取出来加入Combo Box中:
在帮助文档里搜索 QHostAddress 往下找到一个公有函数 protocol() :
再点击进入它的返回值 QAbstractSocket::NetworkLayerProtocol :
它返回一个枚举类型,我们筛选其中IPV4的地址即可。
2.服务器开启/停止监听相关操作
// 启动服务器监听
void Widget::on_btnListen_clicked()
{
int port = ui->lineEditPort->text().toInt();
QHostAddress selectedAddr(ui->comboBoxAddr->currentText());
// 尝试监听指定地址和端口
if(!server->listen(selectedAddr, port)) {
QMessageBox::critical(this, "监听失败", "端口号被占用");
return;
}
// 更新UI状态
ui->btnListen->setEnabled(false);
ui->btnStopListen->setEnabled(true);
ui->comboBoxAddr->setEnabled(false);
ui->comboBoxProt->setEnabled(false);
}
// 停止服务器监听
void Widget::on_btnStopListen_clicked()
{
// 关闭所有客户端连接
QList<QTcpSocket*> clients = server->findChildren<QTcpSocket*>();
for(QTcpSocket* client : clients) {
client->close();
}
server->close(); // 关闭服务器
// 恢复UI状态
ui->btnListen->setEnabled(true);
ui->btnStopListen->setEnabled(false);
ui->btnSend->setEnabled(false);
ui->comboBoxAddr->setEnabled(true);
ui->comboBoxProt->setEnabled(true);
}
帮助文档搜索 QTcpServer 往下找到 listen 函数:
第一个参数是 QHostAddress 类型,帮助文档里搜索可以找到它的一系列构造函数:
因此代码里我们直接取出 Combo Box 的当前文本,它的返回值就是 QString 类型。
再来看到停止监听按键对应的槽函数,步骤是找到连接了该服务端的所有客户端,然后一一关闭,最后再关闭服务端,找到所有客户端需要用到 findChildren 函数,帮助文档搜索 QTcpServer 并没有找到,这个函数在它的基类 QObject 里实现:
我们代码里寻找 server 服务端的类型为 QTcpSocket* 的子对象,这个函数返回一个列表,至于为什么要找类型为 QTcpSocket* 的子对象?在我们的流程图里,server端开始监听后,等待客户端程序发起连接,接收到 newConnection 信号后,再调用 server 端的 nextPendingConnection 函数(后面会讲),它返回一个 QTcpSocket 的指针,并且该套接字是作为 server 的子对象创建的:
3.客户端连接操作
// 处理新客户端连接(核心网络功能)
void Widget::on_newClient_connect()
{
//如果服务端存在“挂起”的客户端连接
if(server->hasPendingConnections()) {
QTcpSocket* connection = server->nextPendingConnection(); // 获取新连接
// 显示客户端信息在接收框上
ui->textEditRx->append(
QString("客户端地址:%1, 端口:%2")
.arg(connection->peerAddress().toString())
.arg(connection->peerPort())
);
// 将数据接收和客户端断开连接的信号与槽函数进行绑定
connect(connection, &QTcpSocket::readyRead,
this, &Widget::on_readyRead_handler);
connect(connection, &QTcpSocket::disconnected,
this, &Widget::mdisconnected);
// 将当前连接到的客户端的端口号添加到下拉选择框中并设置当前索引
ui->comboBoxChildren->addItem(QString::number(connection->peerPort()));
ui->comboBoxChildren->setCurrentIndex(ui->comboBoxChildren->count()-1);
// 激活发送按钮
if(!ui->btnSend->isEnabled())
ui->btnSend->setEnabled(true);
}
}
// 客户端断开处理
void Widget::mdisconnected()
{
QTcpSocket* client = qobject_cast<QTcpSocket*>(sender());
ui->textEditRx->append("客户端断开!");
// 从下拉选择框中移除断开连接的客户端
int index = ui->comboBoxChildren->findText(
QString::number(client->peerPort())
);
if(index != -1) {
ui->comboBoxChildren->removeItem(index);
}
client->deleteLater(); // 安全删除对象
// 当没有客户端时禁用发送按钮
if(ui->comboBoxChildren->count() == 0)
ui->btnSend->setEnabled(false);
}
在构造函数里,我们为 newConnection 信号绑定了槽函数 on_newClient_connect ,直接调用 nextPendingConnection 函数就能创建一个 socket 套接字了,学过网络编程的对这个再熟悉不过了,把他理解成对话的“窗口”吧。在这个槽函数里再为这个套接字绑定接收数据和断开连接的信号与槽函数,这两行代码:
主要是实现选择和哪个客户端对话的功能,当有多个客户端连接到服务端时,我们不想每一次发送数据都是发送给所有客户端,因此我们加上了选择客户端端口这个功能:
因为我是在同一台电脑上运行服务端和客户端,因此IP地址都是相同的,通过端口号来区分不同的客户端。
再来看到客户端断开连接的槽函数,捕获的是 disconnected 信号,在 QTcpSocket 的基类 QAbstractSocket 中定义:
在 mdisconnected 函数里,有一个关键的地方,那就是套接字——socket句柄是在 on_newClient_connect 槽函数里定义的,是一个局部变量,在断开连接的槽函数里,我们怎么获得这个句柄?
因此要用到 sender 函数:
返回触发当前槽函数的信号发送者的指针,然后使用 qobject_cast 进行类型转换,转换为 QTcpSocket* 类型就可以操作客户端了,然后进行一系列操作。
4.数据收发操作
// 接收客户端数据(核心数据交换功能)
void Widget::on_readyRead_handler()
{
QTcpSocket* client = qobject_cast<QTcpSocket*>(sender());
QByteArray data = client->readAll(); // 读取全部数据
// 在UI显示接收内容
ui->textEditRx->append("客户端:" + data);
ui->textEditRx->moveCursor(QTextCursor::End); // 自动滚动到底部
}
// 发送数据给客户端(复杂功能:支持单选/广播)
void Widget::on_btnSend_clicked()
{
QList<QTcpSocket*> clients = server->findChildren<QTcpSocket*>();
if(clients.isEmpty()) {
QMessageBox::warning(this, "发送错误", "当前无连接!");
ui->btnSend->setEnabled(false);
return;
}
QString message = ui->textEditTx->toPlainText();
QString target = ui->comboBoxChildren->currentText();
if(target != "all") { // 发送给指定客户端
for(QTcpSocket* client : clients) {
if(QString::number(client->peerPort()) == target) {
client->write(message.toUtf8());
break;
}
}
} else { // 广播给所有客户端
for(QTcpSocket* client : clients) {
client->write(message.toUtf8());
}
}
}
数据收发就没啥好讲的了,都是 QIODevice 这个类的函数,本质上还是对文件进行操作。
客户端
客户端UI界面设计
如下:
客户端的UI界面比服务端还要简单,只用到了Push Button(按键)、Text Edit(文本编辑框)、Label(标签)和Line Eidt(行编辑框),包含关系如下:
客户端代码实现
1.构造函数与初始化
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
// UI初始化
ui->setupUi(this);
//自动适应缩放
this->setLayout(ui->verticalLayout);
// 客户端初始化(client在头文件中定义)
client = new QTcpSocket(this); // 创建TCP客户端对象
/* 信号槽连接(旧式语法,建议改用新式connect)
* 创建客户端对象后连接服务端就可以开始通信了
* 等待readyRead信号,跳转到数据接收槽函数
*/
connect(client, SIGNAL(readyRead()),
this, SLOT(mRead_Data_From_Server()));
// 初始UI状态设置
ui->btnDisconnect->setEnabled(false); // 禁用断开按钮
ui->btnSend->setEnabled(false); // 禁用发送按钮
}
还记得开发流程图吗,客户端直接实例化 QTcpSocket 对象,我们通过调用 socket 的读写函数来实现数据收发。
2.连接服务器功能
void Widget::on_btnConnect_clicked()
{
// 获取服务器地址和端口
QString ip = ui->lineEditIPAddr->text();
quint16 port = ui->lineEditPort->text().toInt();
// 发起连接请求
client->connectToHost(ip, port);
// 创建超时计时器(建议改为成员变量避免重复创建)
timer = new QTimer(this);
timer->setSingleShot(true); // 单次触发
timer->setInterval(5000); // 5秒超时
// 信号连接
connect(timer, SIGNAL(timeout()), this, SLOT(on_timeout()));
connect(client, SIGNAL(connected()), this, SLOT(on_connected()));
connect(client, SIGNAL(error(QAbstractSocket::SocketError)),
this, SLOT(on_error(QAbstractSocket::SocketError)));
// 禁用UI防止重复操作
this->setEnabled(false);
timer->start();
}
/*------------------------------------------
* 连接成功处理
* 功能:更新连接成功后的UI状态
*----------------------------------------*/
void Widget::on_connected()
{
timer->stop(); // 停止超时计时
this->setEnabled(true); // 启用窗口
// 更新UI组件状态
ui->textEditRx->setText("连接成功!\r\n");
ui->btnConnect->setEnabled(false); // 禁用连接按钮
ui->lineEditPort->setEnabled(false); // 禁止修改端口
ui->lineEditIPAddr->setEnabled(false); // 禁止修改IP
ui->btnDisconnect->setEnabled(true); // 启用断开按钮
ui->btnSend->setEnabled(true); // 启用发送按钮
}
/*------------------------------------------
* 错误处理
* 功能:显示具体的错误信息
* 改进:可添加更多错误类型处理
*----------------------------------------*/
void Widget::on_error(QAbstractSocket::SocketError error)
{
// 获取错误枚举的元信息
QMetaEnum metaEnum = QMetaEnum::fromType<QAbstractSocket::SocketError>();
// 转换为可读字符串
const char* errorStr = metaEnum.valueToKey(error);
// 显示错误信息
QString msg = errorStr ?
"连接出错: " + QString(errorStr) :
"连接出错: 未知错误 (" + QString::number(error) + ")";
ui->textEditRx->append(msg);
// 恢复UI状态
this->setEnabled(true);
on_btnDisconnect_clicked(); // 触发断开流程
}
/*------------------------------------------
* 超时处理
* 功能:处理连接超时情况
*----------------------------------------*/
void Widget::on_timeout()
{
ui->textEditRx->append("连接超时\n");
client->abort(); // 中止连接
this->setEnabled(true); // 恢复UI
}
主要是看第一个函数,如果是自己写服务端和客户端的话,后面这些错误处理可要可不要,连接成功的时候打印一下信息就好了。使用函数 connectToHost 连接服务端:
3.断开连接功能
void Widget::on_btnDisconnect_clicked()
{
// 关闭套接字
client->close();
// 更新UI
ui->textEditRx->append("断开连接!");
ui->btnConnect->setEnabled(true);
ui->lineEditPort->setEnabled(true); // 允许修改端口
ui->lineEditIPAddr->setEnabled(true); // 允许修改IP
ui->btnDisconnect->setEnabled(false); // 禁用断开按钮
ui->btnSend->setEnabled(false); // 禁用发送按钮
}
直接调用 close 函数。
4.数据收发功能
//发送按键绑定的槽函数
void Widget::on_btnSend_clicked()
{
// 获取待发送文本
QString text = ui->textEditTx->toPlainText();
// 转换数据
// QByteArray sendData = text.toUtf8(); // 更安全的转换方式
// 发送数据
client->write(sendData);
// 在接收框显示发送内容(红色)
mInsertTextByColor(Qt::red, text);
}
//接收数据槽函数
void Widget::mRead_Data_From_Server()
{
// 滚动条自动到底部
ui->textEditRx->moveCursor(QTextCursor::End);
ui->textEditRx->ensureCursorVisible();
// 读取并显示数据(建议添加编码转换)
QByteArray data = client->readAll();
mInsertTextByColor(Qt::black, QString(data)); // 黑色显示接收数据
}
mInsertTextByColor 是自己定义的一个函数,原本想让服务端和客户端将接收的数据显示为黑色,自己发送的数据显示为红色,后面只在客户端做了这个功能,有需要的自己移植到服务端即可。
5.自定义改变文本颜色函数
void Widget::mInsertTextByColor(const QColor &color, const QString &str)
{
// 获取文本编辑框的光标并移动到末尾
QTextCursor cursor = ui->textEditRx->textCursor();
cursor.movePosition(QTextCursor::End);
// 设置文本颜色格式
QTextCharFormat format;
format.setForeground(QBrush(color));
cursor.setCharFormat(format);
// 插入文本并换行(兼容不同平台)
cursor.insertText(str + "\n");
// 更新文本编辑框的光标状态
ui->textEditRx->setTextCursor(cursor);
// 自动滚动到最新内容
ui->textEditRx->ensureCursorVisible();
}
结尾
至此基于QT5编写的网络调试助手就结束了,基于这个项目,我们可以扩展出很多项目,后续可能会更新基于QT5和Linux服务端加STM32的无人超市项目,请大家点个关注支持一下。