信号与槽机制在 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支持多程序启动

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

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