61、信号与槽机制在 TCP 编程中的应用---------网络编程

信号与槽机制在 TCP 编程中的应用

信号与槽机制在 TCP 编程中的应用

Qt 的信号与槽机制是实现异步通信的关键。通过连接不同对象的信号与槽,可以在事件发生时自动触发相应的处理函数。例如,当服务器接收到新连接时,QTcpServer 会发出 newConnection 信号,连接到这个信号的槽函数可以处理新连接。

我们可以通过捕获newConnection信号,在槽函数中处理连接请求,但是这种方式

优点:

●简洁易用:通过信号和槽机制,代码结构清晰,易于理解和维护。

●不需要继承:不需要创建服务器类的子类,适合简单和中等复杂度的应用。

缺点:

●灵活性有限:对于需要更细粒度控制新连接处理过程的应用,可能不够灵活。

也可以通过重写incomingConnection处理新来的连接,这种方式和上面类似,但是更加灵活,还可以将新的连接分配到其他线程。

聊天室实战案例

我们实现一个客户端群聊功能,当然要实现客户端和服务器通信,服务器负责将消息转发给所有在线的客户端,客户端收到消息后显示。

服务器显示客户端发送的信息列表

客户端显示如下

实现过程

客户端界面如下图

ui布局

属性布局如下

MainWindow

做网络之前,我们先实现基本的发送响应逻辑, 最初的MainWindow声明如下

cpp 复制代码
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    //点击发送按钮
    void on_send_btn_clicked();
    //收到消息槽函数,添加消息到聊天框
    void slot_append_msg(QString msg);
private:
    Ui::MainWindow *ui;
signals:
};

构造函数实现

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->resize(800,600);
    //设置默认ip
    ui->address_ed->setText("127.0.0.1");
    //设置spinbox范围
    ui->port_spin->setMaximum(20000);
    //设置端口号
    ui->port_spin->setValue(10086);

    //设置textEdit文字大小
    QFont font = ui->chatEdit->font();
    font.setPointSize(16);
    ui->chatEdit->setFont(font);
    //设置textEdit只读
    ui->chatEdit->setReadOnly(true);

    //设置默认名字
    ui->name_ed->setText("疾跑流鲁班");

}

当发送消息的按钮被点击时执行如下槽函数

cpp 复制代码
//发送消息按钮点击后槽函数响应如下
void MainWindow::on_send_btn_clicked()
{
    QString name = ui->name_ed->text();
    QString msg = ui->msg_ed->text();
    if(msg.isEmpty()){
        return;
    }

    if(name.isEmpty()){
        QMessageBox::information(nullptr,"提示信息","用户名为空!");
        return;
    }

    QString msgs = QString("%1 : %2").arg(name).arg(msg);
    //发送消息,todo发送TCPClient信号...
    //emit TcpClient::Inst().sig_send_msg(msgs);
    //清空编辑框
    ui->msg_ed->clear();
}

收到发送消息的信号后,将消息添加到聊天框

cpp 复制代码
//添加消息
void MainWindow::slot_append_msg(QString msg)
{
    ui->chatEdit->append(msg);
}

TcpClient类

接下来我们需要实现TCPClient类,关于网络类,我们在企业中一般以单例模式使用.

为了编写边测试,我们先声明TcpClient类,因为是单例类,所以要去掉拷贝构造和拷贝赋值,并且将构造函数设为私有。

cpp 复制代码
#ifndef TCPCLIENT_H
#define TCPCLIENT_H

#include <QObject>
#include <QTcpSocket>

class TcpClient : public QObject
{
    Q_OBJECT
public:
    //单例模式,返回静态局部变量
    static TcpClient& Inst(){
        static TcpClient client;
        return client;
    }
    ~TcpClient();

    //删除拷贝构造和拷贝赋值
    TcpClient(const TcpClient&) = delete;
    TcpClient& operator=(const TcpClient&) = delete;
    
    //连接到服务器
    void connectToServer(const QString& host, quint16 port);
private:
    //构造函数变为私有
    explicit TcpClient(QObject *parent = nullptr);

    //QTcpSocket类,用来通信的客户端
    QTcpSocket* _socket;

signals:

public slots:

    //连接成功触发的槽函数
    void slot_connected();
    //tcp缓冲区有数据可读,触发读取消息槽函数
    void slot_ready_read();
    //捕获到断开连接触发的槽函数
    void slot_disconnected();
    //捕获到出错信号触发的槽函数
    void slot_error_occured(QAbstractSocket::SocketError socketError);

    //发送消息槽函数
    void slot_send_msg(const QString& msg);

};

#endif // TCPCLIENT_H

在TcpClient的构造函数中添加信号和槽函数的连接处理

cpp 复制代码
TcpClient::TcpClient(QObject *parent) : QObject(parent), _socket(new QTcpSocket(this))
{
    //连接 连接成功信号和槽函数
    connect(_socket, &QTcpSocket::connected, this, &TcpClient::slot_connected);
    //连接 读数据信号和槽函数
    connect(_socket, &QTcpSocket::readyRead, this, &TcpClient::slot_ready_read);
    //连接 断开连接信号和槽函数
    connect(_socket, &QTcpSocket::disconnected, this, &TcpClient::slot_disconnected);
    //连接 出错信号和槽函数
    connect(_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
                    this, &TcpClient::slot_error_occured);
    //5.15之后的高版本才有的信号
//    connect(_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
//                this, &TcpClient::slot_error_occured);
}

注意事项

  • _socket初始化通过new的方式创建指针,指定父窗口为TcpClient
  • errorOccurred信号是高于5.15版本才有的信号,我们使用的是低版本,所以使用error信号。
  • QOverload 是一个模板类,用于解决重载信号的问题。因为 Qt 中可能会有多个同名的信号(例如,error(QAbstractSocket::SocketError)error(QString)),使用 QOverload 可以明确指定我们要连接的信号的版本。

其等价于下面的写法,

cpp 复制代码
//连接 出错信号和槽函数
connect(_socket, static_cast<void(QTcpSocket::*)(QTcpSocket::SocketError)>(&QTcpSocket::error),
                this, &TcpClient::slot_error_occured);

通过static_cast将QTcpSocket::error转化为一个成员函数的指针,这样可以指定参数类型。

我们将其他的成员函数和方法先实现,不写具体内容。

连接服务器

连接主界面连接服务器按钮的点击信号,调用连接功能

cpp 复制代码
//点击连接到服务器后响应的槽函数
void MainWindow::on_conBtn_clicked()
{
    auto address = ui->address_ed->text();
    quint16 port = static_cast<quint16>(ui->port_spin->value());
    TcpClient::Inst().connectToServer(address, port);
}

实现connectToServer函数

cpp 复制代码
void TcpClient::connectToServer(const QString &host, quint16 port)
{
    _socket->connectToHost(host, port);
}

捕获连接信号

我们可以为Client类设置用户名,这样发送消息可以携带用户名

新增成员变量_name,构造函数初始化为空字符串,并且提供设置名字的成员函数

cpp 复制代码
void TcpClient::setName(const QString &msg)
{
     _name = msg;
}

在MainWindow构造函数中添加设置名字逻辑

cpp 复制代码
//设置用户名
TcpClient::Inst().setName(ui->name_ed->text());

在MainWindow构造函数中捕获名字编辑框消失的信号,并且设置名字给客户端

cpp 复制代码
//连接名字编辑框失去焦点信号
connect(ui->name_ed,&QLineEdit::editingFinished, this, [=](){
    //设置用户名
    TcpClient::Inst().setName(ui->name_ed->text());
});

回到TcpClient中, 封装成员函数发送消息,

cpp 复制代码
void TcpClient::sendMsg(const QString &msg)
{
    //发送信号,统一交给槽函数处理,这么做的好处是多线程安全
    emit sig_send_msg(msg);
}

sendMsg中发送信号发消息,这么做的好处是多线程调用sendMsg不会有数据污染

所以TcpClient定义了sig_send_msg信号,以及槽函数slot_send_msg

cpp 复制代码
void TcpClient::slot_send_msg(const QString &msg)
{
    //如果连接异常则直接返回
    if(_socket->state() != QAbstractSocket::ConnectedState){
        QMessageBox::information(nullptr,"提示消息","断开连接无法发送");
        return;
    }
    //发送消息
     _socket->write(msg.toUtf8()+"\n");
}

TcpClient中连接发送信号和槽函数

cpp 复制代码
//连接 发送数据信号和槽函数
connect(this, &TcpClient::sig_send_msg, this, &TcpClient::slot_send_msg);

主窗口点击发送按钮后,需发送客户端的信号, 完善后的发送服务如下

cpp 复制代码
void MainWindow::on_send_btn_clicked()
{
    QString name = ui->name_ed->text();
    QString msg = ui->msg_ed->text();
    if(msg.isEmpty()){
        return;
    }

    if(name.isEmpty()){
        QMessageBox::information(nullptr,"提示信息","用户名为空!");
        return;
    }

    QString msgs = QString("[%1] : %2").arg(name).arg(msg);
    //发送消息
    emit TcpClient::Inst().sig_send_msg(msgs);
    //清空编辑框
    ui->msg_ed->clear();
}

准备工作都做好了,接下来我们在连接成功的槽函数中发送信息给服务器

cpp 复制代码
//连接成功槽函数
void TcpClient::slot_connected()
{
    QMessageBox::information(nullptr,"提示信息","连接服务器成功!");
    //发送消息
    this->sendMsg(QString("[%1] : %2").arg(_name).arg("登录成功"));
}

我们发送给服务器了,主界面也要显示一下自己的信息, 主界面也连接TcpClient的sig_send_msg信号

cpp 复制代码
//连接发送消息信号
connect(&TcpClient::Inst(), &TcpClient::sig_send_msg, this, &MainWindow::slot_append_msg);

捕获连接断开

cpp 复制代码
//捕获连接断开信号
void TcpClient::slot_disconnected()
{
    //连接断开
    QMessageBox::information(nullptr,"提示信息","连接断开!");
    this->sendMsg(QString("[%1] : %2").arg(_name).arg("连接断开"));
}

捕获错误信息

cpp 复制代码
void TcpClient::slot_error_occured(QAbstractSocket::SocketError socketError)
{
    Q_UNUSED(socketError);
    //产生错误
    QMessageBox::information(nullptr,"提示信息",
                             QString("产生错误: %1 !").arg(_socket->errorString()));
}

此时启动程序,点击连接服务器,过一段时间后会出现如下错误

为了防止用户点击了连接服务器后没来的及处理就再次点击,所以点击连接后将按钮置灰, 在产生错误时重置连接为可用。

MainWindow构造函数中

cpp 复制代码
    //设置按钮状态
    ui->conBtn->setEnabled(true);
    ui->disConBtn->setEnabled(false);

点击后

cpp 复制代码
//点击连接到服务器后响应的槽函数
void MainWindow::on_conBtn_clicked()
{
    //设置按钮状态
    ui->conBtn->setEnabled(false);
    auto address = ui->address_ed->text();
    quint16 port = static_cast<quint16>(ui->port_spin->value());
    TcpClient::Inst().connectToServer(address, port);
}

MainWindow增加槽函数响应是否重置按钮初始状态

cpp 复制代码
//重置按钮状态
void MainWindow::slot_reset_btn(bool b_reset)
{
    ui->conBtn->setEnabled(b_reset);
    ui->disConBtn->setEnabled(!b_reset);
}

MainWindow构造函数中添加

cpp 复制代码
//连接重置按钮信号
connect(&TcpClient::Inst(), &TcpClient::sig_reset_btn, this, &MainWindow::slot_reset_btn);

完善错误处理

cpp 复制代码
void TcpClient::slot_error_occured(QAbstractSocket::SocketError socketError)
{
    Q_UNUSED(socketError);
    //产生错误
    QMessageBox::information(nullptr,"提示信息",
                             QString("产生错误: %1 !").arg(_socket->errorString()));

    //出错则判断连接状态并重置
    if(_socket->state() != QAbstractSocket::ConnectedState){
        emit sig_reset_btn(true);
        return;
    }

    //出错就关闭连接
    _socket->close();
    emit sig_reset_btn(true);
}

完善连接成功处理

cpp 复制代码
//连接成功槽函数
void TcpClient::slot_connected()
{
    QMessageBox::information(nullptr,"提示信息","连接服务器成功!");
    //重置按钮
    emit sig_reset_btn(false);
    //发送消息
    this->sendMsg(QString("[%1] : %2").arg(_name).arg("登录成功"));
}

读取对端发送的信息

cpp 复制代码
//接受服务器发送的信息
void TcpClient::slot_ready_read()
{
    //读取所有数据
    QByteArray data = _socket->readAll();
    //将数据转化为字符串
    QString message = QString::fromUtf8(data).trimmed();
    //发送消息通知界面显示
    emit sig_append_msg(message);
}

读取消息后,为了将消息发送到主界面聊天框显示,所以定义了sig_append_msg信号。

在MainWindow中构造函数里连接这个信号

cpp 复制代码
//连接客户端接受信息后显示的消息
connect(&TcpClient::Inst(),&TcpClient::sig_append_msg, this, &MainWindow::slot_append_msg);

接下来实现断开连接函数

cpp 复制代码
//断开连接函数
void TcpClient::disconnectFromServer()
{
    if(_socket->state() != QAbstractSocket::ConnectedState){
        return;
    }

    _socket->disconnectFromHost(); // 优雅地关闭连接

    // 或者使用 socket->close(); 立即关闭连接
    if (_socket->waitForDisconnected(3000)) {
        //重置按钮状态
        emit sig_reset_btn(true);
    } else {
        QMessageBox::information(nullptr, "提示信息",
                                 QString("关闭错误%1").arg(_socket->errorString()));
    }
}

并且在MainWindow的点击关闭连接的槽函数中添加socket断开连接的逻辑

cpp 复制代码
void MainWindow::on_disConBtn_clicked()
{
    //断开连接
    TcpClient::Inst().disconnectFromServer();
}

到此位置我们的客户端开发完成。

接下来我们做服务器

服务器

创建TcpServer项目,记得在pro中添加network

cpp 复制代码
QT       += core gui network

添加Tcpserver

构造函数实现如下:

cpp 复制代码
#ifndef CHATSERVER_H
#define CHATSERVER_H

#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QSet>

class ChatServer : public QTcpServer
{
    Q_OBJECT
public:
    static ChatServer& Inst(){
        static ChatServer server;
        return server;
    }

    //开始服务器
    bool startServer(quint16 port);
    //删除拷贝构造和拷贝赋值
    ChatServer(const ChatServer& cs) = delete;
    ChatServer& operator=(const ChatServer& cs) = delete;
protected:
    //重写虚函数,实现新连接到来管理逻辑
    void incomingConnection(qintptr socketDescriptor) override;
signals:

public slots:
    //服务器读消息的槽函数
    void slot_ready_read();
    //获取客户端断开连接的槽函数
    void slot_disconnect();
private:
    //构造函数
    explicit ChatServer(QObject *parent = nullptr);
    //客户端连接的集合
    QSet<QTcpSocket*> clients;
    //广播消息
    void broadcastMessage(const QString &message, QTcpSocket* sender = nullptr);
};

#endif // CHATSERVER_H

构造函数

cpp 复制代码
ChatServer::ChatServer(QObject *parent) : QTcpServer(parent)
{

}

实现启动服务的逻辑

cpp 复制代码
//启动服务
bool ChatServer::startServer(quint16 port)
{
    if (!this->listen(QHostAddress::Any, port)) {
        QMessageBox::information(nullptr,"提示信息","服务器启动失败!");
        return false;
    } else {
        QMessageBox::information(nullptr,"提示信息","服务器启动成功!");
        return true;
    }
}

获取新的连接

cpp 复制代码
//获取新来的连接
void ChatServer::incomingConnection(qintptr socketDescriptor)
{
    QTcpSocket* clientSocket = new QTcpSocket(this);
    if (!clientSocket->setSocketDescriptor(socketDescriptor)) {
        qDebug() << "Failed to set socket descriptor";
        clientSocket->deleteLater();
        return;
    }

    //将客户端加入集合
    clients.insert(clientSocket);

    //连接读取信息信号
    connect(clientSocket, &QTcpSocket::readyRead, this, &ChatServer::slot_ready_read);
    //连接客户端断开信号
    connect(clientSocket, &QTcpSocket::disconnected, this, &ChatServer::slot_disconnect);

    //打印对端地址
    qDebug() << "New client connected:" << clientSocket->peerAddress().toString();
}

读取客户端发送信息

cpp 复制代码
//读取客户端发送的数据
void ChatServer::slot_ready_read()
{
    //将发送信号的对象转化为QTcpSocket类型
    QTcpSocket* clientSocket = qobject_cast<QTcpSocket*>(sender());
    if (!clientSocket)
        return;

    //读取信息
    QByteArray data = clientSocket->readAll();
    //去除信息前后空格
    QString message = QString::fromUtf8(data).trimmed();

    //广播消息
    broadcastMessage(message,clientSocket);

    qDebug() << "Received:" << message;
    //通知主界面添加消息
    emit sig_append_msg(message);
}

广播消息

cpp 复制代码
//广播消息
void ChatServer::broadcastMessage(const QString &message, QTcpSocket* senderSocket)
{
    QByteArray data = message.toUtf8() + "\n";
    //遍历clients并且判断是否为发送者,如果不是源消息发送者则推送消息
    for(auto & client : clients){
        if(client != senderSocket){
            client->write(data);
        }
    }
}

处理客户端断开连接的消息

cpp 复制代码
//获取客户端断开连接槽函数
void ChatServer::slot_disconnect()
{
    //获取发送者转为QTcpSocket类型
    QTcpSocket* clientSocket = qobject_cast<QTcpSocket*>(sender());
    if (!clientSocket)
        return;

    //获取对端地址
    auto address = clientSocket->peerAddress().toString();
    //从客户端移除
    clients.remove(clientSocket);
    //删除socket
    clientSocket->deleteLater();
    //推广离开信息
    QString leftMessage = QString("%1 left the chat.").arg(address);
    broadcastMessage(leftMessage);
    //发送消息通知主界面显示离开信息
    emit sig_append_msg(leftMessage);
    qDebug() << "Client disconnected:" << address;
}

实现主界面

添加页面布局

层级属性

MainWindow

MainWindow构造函数

cpp 复制代码
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    this->resize(800,600);
    //设置默认ip
    ui->addressEd->setText("0.0.0.0");
    //设置spinbox范围
    ui->portSpin->setMaximum(20000);
    //设置端口号
    ui->portSpin->setValue(10086);

    //设置textEdit文字大小
    QFont font = ui->chatEdit->font();
    font.setPointSize(16);
    ui->chatEdit->setFont(font);
    //设置textEdit只读
    ui->chatEdit->setReadOnly(true);
}

添加启动服务按钮的响应槽函数

cpp 复制代码
//响应启动按钮
void MainWindow::on_startBtn_clicked()
{
    //设置按钮状态
    ui->startBtn->setEnabled(false);
    bool res = ChatServer::Inst().startServer(
                static_cast<quint16>(ui->portSpin->value()));
    //设置按钮状态
    ui->startBtn->setEnabled(!res);
    ui->closeBtn->setEnabled(res);
}

实现关闭服务的函数

cpp 复制代码
//关闭服务
bool ChatServer::stopServer()
{
    //判断服务是否在运行
    if(!(this->isListening())){
        QMessageBox::information(nullptr,"提示信息","服务未启动!");
        return false;
    }

    //关闭服务
    this->close();
    QMessageBox::information(nullptr,"提示信息","服务关闭成功!");
    return true;
}

此时启动服务能看到如下界面

关闭服务能看到如下界面

服务器MainWindow构造函数中连接信号

cpp 复制代码
//连接服务器发送的添加文本的信号
connect(&ChatServer::Inst(),&ChatServer::sig_append_msg,this,&MainWindow::slot_append_msg);

实现添加消息的槽函数

cpp 复制代码
//添加消息槽函数
void MainWindow::slot_append_msg(QString msg){
    ui->chatEdit->append(msg);
}

测试

运行服务程序,启动服务器,并且运行客户端,配置服务器地址连接到服务器。

当不同的客户端登录的时候,服务器都会收到信息

可以设置Qt Creator支持多程序启动

服务器会显示收到的消息,并且广播给其他客户端

多个客户端会看到共享的聊天信息

相关推荐
syagain_zsx1 小时前
STL 之 vector 讲练结合
c++·算法
牛油果子哥q2 小时前
STL set与map底层精讲,红黑树适配原理、有序去重特性、迭代器遍历、API实战与面试核心考点全解
开发语言·数据结构·c++·面试
古德new2 小时前
鸿蒙PC迁移:Photoflare Qt 图片编辑器鸿蒙PC适配全记录
qt·编辑器·harmonyos
奇妙方程式2 小时前
2026年第九届GXCPC广西大学生程序设计大赛(热身赛)题解
c++·编程比赛·编程竞赛·gxcpc
Tian_Hang3 小时前
C++原型模式(Protype)
开发语言·c++·算法
swordbob3 小时前
NIO 的 Channel 里有多个 BIO 吗?
linux·网络·nio
天天讯通3 小时前
OKCC 呼叫中心安全性能全解析:技术防护与管理措施指南
大数据·开发语言·网络·人工智能·安全·语音识别
FL16238631294 小时前
[cmake]基于C++使用纯opencv部署ppocrv5v6的onnx模型
开发语言·c++·opencv
玖玥拾4 小时前
C/C++ 数据结构(六)链表迭代器与底层
c语言·数据结构·c++·链表·stl库