前言:
转载请联系作者,本帖原创请勿照抄。
环境QT 5.14.1,本文实现了 1、客户端和服务器端收发消息、自动获取IP信息、两种不同的方案发送数据;2、解决了客户端和服务端断开之后程序异常的问题;3、解决了多台客户端连接服务器的客户端显示以及TCPSockets连接保存与断开;4、服务器端可以给某个客户端发送消息,也支持给所有的客户端发送消息;5、在客户端给服务器发送消息的时候会有客户端IP显示,在切换客户端给服务器发送消息的时候会自动显示客户端的IP。
基础点对点功能其实十月初就实现了,之后一直在加功能多个客户端连接服务器,解决过程中出现的问题,优化代码;添加发送文件功能,解决BUG和研究其他问题也占了不少功夫,其实今天将客户端与服务器部分的功能彻底解决完了。唯一的BUG就剩服务器断开监听之后,再次监听会出问题,这个也留给下一篇博客 TCP发送文件部分解决吧,在文章结尾会提供exe测试程序给大家,完整版程序将在下篇博客中发出来。
关于下篇博客TCP发送文件部分,客户端给服务器发送文件也在十月份就完成了,但是涉及到多个客户端给服务器发送文件,和服务器给多个客户端发送文件部分还是得解决一些关键性的地方,所以只能在稍后发了,也不想再拖下去了。
接下来博客更新文章:QT 多线程操作、QT 连接MYSQL进行分页、SQL 优化以及一些语句操作、QT QML、TCP发送文件、UDP发送消息、UDP发送文件、QT之深入了解TCP结构(主要会通过几种不同的连接方式来展开)、等功能在接下来的博客中会陆续更新,有条件的话PHP也会同时进行更新。也希望大家能点个关注,点个赞。
在文章开头先推荐一篇自己写的TCP、UDP、IP的文章,个人感觉挺不错的,有兴趣的伙伴们可以点开看看:https://blog.csdn.net/qq_37529913/article/details/106765916
一、软件界面
1. 服务器界面截图
1.1 服务器启动监听,可以看到有提示信息,启动监听;
1.2 给所有的客户端发送消息;
1.3 当有新的客户端接入的时候也会有相应的提示;
1.4 不同客户端发送信息的时候会有IP显示。
1.5 给单个的客户端发送消息这里就不演示了,感觉篇幅超长了,样式也不好看,有兴趣可以下载文章底部的EXE来测试一下。
2. 客户端A界面截图
2.1 可以看到根据IP和端口来连接到服务器;
2.2 可以看到接受到服务器发送的消息;
2.3 可以看到发送给服务器三条消息;
3. 客户端B界面截图
3.1 可以看到根据IP和端口来连接到服务器;
3.2 可以看到接受到服务器发送的消息;
3.3 可以看到发送给服务器三条消息;
4.项目界面截图
二、引入抬头 (以下部分为正文部分)
1. 在.pro文件中添加QT += network,否则是访问不到<QTcpSocket>
//项目的.pro文件中添加QT += network
//添加完network才能使用<QTcpSocket>
QT += network
三、客户端编程
1. dialog_c.h
#ifndef DIALOG_C_H
#define DIALOG_C_H
#include <QDialog>
#include <QMessageBox>
#include <QButtonGroup>
#include <QString>
#include <QHostInfo>
#include <QtNetwork>
#include <QFileDialog>
//发送消息,上面有些是发送文件使用的
#include <QTcpSocket>
#include <QHostAddress>
#include <QMessageBox>
//加入UTF-8申明,否则中文乱码
#pragma execution_character_set("utf-8")
namespace Ui {
class Dialog_C;
}
class Dialog_C : public QDialog
{
Q_OBJECT
public:
explicit Dialog_C(QWidget *parent = nullptr);
~Dialog_C();
QButtonGroup* bgGroup = new QButtonGroup(this);
QString ExamineQStr(QString Str);
private slots:
//这块是项目里面的其他控件,因为不是正式项目所以控件的名字有的没有改,难记
void on_But_Select_File_clicked();
void on_But_Send_File_clicked();
void on_radioButton_clicked();
void on_radioButton_2_clicked();
void on_But_conn_clicked();
void on_pushButton_2_clicked();
void on_But_Send_Message_clicked();
void readMessage(); //接收数据,这块需要在.cpp文件里面绑定信号
void displayError(QAbstractSocket::SocketError); //显示错误,这块需要在.cpp文件里面绑定信号
void deleteLater();
private:
Ui::Dialog_C *ui;
//private私有变量
QTcpSocket *tcpSocket; //嵌套字使用
};
#endif // DIALOG_C_H
2. dialog_c.cpp
2.1 构造函数代码
//控件默认值
ui->lineEdit_Port->setText("6666");
ui->lineEdit_IP->setText("127.0.0.1");
//获取本地IP
ui->lineEdit_LIP->setText(QNetworkInterface().allAddresses().at(1).toString());
//TCP连接嵌套字和关联信号槽操作
tcpSocket = new QTcpSocket(this); //实例化
tcpSocket->abort(); //取消链接
//这里关联了tcpSocket的两个信号,当有数据到来时发出readyRead()信号,我们执行读取数据的readMessage()函数。
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage()));
//当出现错误时发出error()信号,我们执行displayError()槽函数。connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(displayError(QAbstractSocket::SocketError)));
//这里实现了异常监听和error信号类似
connect(tcpSocket, SIGNAL(disconnected()),this,SLOT(deleteLater()));
2.2 连接服务器代码
tcpSocket->abort(); //取消已有的连接
//连接到主机,这里从界面获取主机地址和端口号
tcpSocket->connectToHost(ui->lineEdit_IP->text(),ui->lineEdit_Port->text().toInt());
if (tcpSocket->waitForConnected(1000)) // 连接成功则进入if{}
{
ui->But_conn->setEnabled(false);
ui->lineEdit_IP->setEnabled(false);
ui->lineEdit_Port->setEnabled(false);
ui->lineEdit_LIP->setEnabled(false);
ui->pushButton_2->setEnabled(true);
ui->textEdit->insertPlainText("连接到服务器!");
ui->textEdit->insertPlainText("\n");
}else{
//出现异常就弹窗,可以加 return;
MessageBox::information(this, "aaa", "连接失败...", QMessageBox::Ok);
}
2.3 断开服务器代码
tcpSocket->disconnectFromHost();
if (tcpSocket->state() == QAbstractSocket::UnconnectedState||tcpSocket->waitForDisconnected(1000)) //已断开连接则进入if{}
{
ui->But_conn->setEnabled(true);
ui->lineEdit_IP->setEnabled(true);
ui->lineEdit_Port->setEnabled(true);
ui->lineEdit_LIP->setEnabled(true);
ui->pushButton_2->setEnabled(false);
}
2.4 读取服务器发送数据代码
//这个模块首先需要查看自己绑定的信号和槽函数,readMessage为槽函数,readyRead为服务器发送数据触发的信号
//这里关联了tcpSocket的两个信号,当有数据到来时发出readyRead()信号,我们执行读取数据的readMessage()函数。
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage()));
QByteArray buffer = tcpSocket->readAll();
if(!buffer.isEmpty())
{
ui->textEdit->insertPlainText("接收到消息:");
ui->textEdit->insertPlainText(buffer);
ui->textEdit->insertPlainText("\n");
}
2.5 异常槽代码
//这是两个异常处理槽,当服务器异常这两个信号和槽都会触发,这块因为涉及到文件发送所以这里都保留下来了,如果只是实现发送消息,可以去除disconnected信号。
//当出现错误时发出error()信号,我们执行displayError()槽函数。
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(displayError(QAbstractSocket::SocketError)));
connect(tcpSocket, SIGNAL(disconnected()),this,SLOT(deleteLater()));
displayError(QAbstractSocket::SocketError)
//tcpSocket->errorString();
tcpSocket->disconnectFromHost();
if (tcpSocket->state() == tcpSocket->waitForDisconnected(1000)){ //已断开连接则进入if{}
ui->textEdit->insertPlainText("服务器断开连接!");
ui->textEdit->insertPlainText("\n");
ui->But_conn->setEnabled(true);
ui->lineEdit_IP->setEnabled(true);
ui->lineEdit_Port->setEnabled(true);
ui->lineEdit_LIP->setEnabled(true);
ui->pushButton_2->setEnabled(false);
}else{
ui->textEdit->insertPlainText("断开监听状态失败");
ui->textEdit->insertPlainText("\n");
}
deleteLater()
deleteLater 函数代码和 displayError一样
2.6 向服务器发送代码
//转换类型
QString QStr=ui->lineEdit_3->text();
std::string str = QStr.toStdString();
const char* ch = str.c_str();
if(QStr != "")
{
if(tcpSocket->write(ch)){
//发送成功代码
}else{
//发送失败代码
}
}else{
QMessageBox::information(this, "on_But_Send_Message_clicked", "发送值为空", QMessageBox::Ok);
}
ui->lineEdit_3->setText("");
QStr="发送消息:"+QStr;
ui->textEdit->insertPlainText(QStr);
ui->textEdit->insertPlainText("\n");
四、服务器编程
在编程当中客户端改动和调整相对较少,只是微调,解决服务器端的BUG或者自己不注意挖的坑比较多。因为客户端只需要面对服务器一个,而服务器则需要面多多个客户端所涉及到的一些功能和一些模块很容易就需要重新解决。(先给自己的懒惰找个合理的借口)
这里有两种方法获取客户端的信息:
通过重写[virtual protected] void QTcpServer::incomingConnection(qintptr socketDescriptor),获取soketDescriptor。自定义TcpClient类继承QTcpSocket,并将获得的soketDescriptor作为类成员。 这个方法的优点是:可以获取到soketDescriptor,灵活性高。缺点是:需要重写函数、自定义类。
在newConnection()信号对应的槽函数中,通过QTcpSocket *QTcpServer::nextPendingConnection()函数获取 新连接的客户端:Returns the next pending connection as a connected QTcpSocket object. 虽然仍然得不到soketDescriptor,但可以通过QTcpSocket类的peerAddress()和peerPort()成员函数获取客户端的IP和端口号,同样是唯一标识。 优点:无需重写函数和自定义类,代码简洁。缺点:无法获得SocketDecriptor,灵活性差。
本文使用的是第二种。
1. dialog_s.h
#ifndef DIALOG_S_H
#define DIALOG_S_H
#include <QDialog>
#include <QtNetWork>
#include <QButtonGroup>
//发送消息
#include <QTcpServer>
#include <QTcpSocket>
#include <QNetworkInterface>
#include <QMessageBox>
//加入UTF-8申明,否则中文乱码
#pragma execution_character_set("utf-8")
namespace Ui {
class Dialog_S;
}
class Dialog_S : public QDialog
{
Q_OBJECT
public:
explicit Dialog_S(QWidget *parent = nullptr);
~Dialog_S();
QButtonGroup* bgGroup = new QButtonGroup(this);
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
void sendMessage();
void on_radioButton_clicked();
void on_radioButton_2_clicked();
void on_textEdit_copyAvailable(bool b);
void displayError(QAbstractSocket::SocketError);//异常处理
void on_pushButton_3_clicked();
void ReadData();
void Link();//监听
void Break();//断开
void on_pushButton_5_clicked();
void deleteLater();
void on_pushButton_6_clicked();
void on_pushButton_7_clicked();
private:
Ui::Dialog_S *ui;
//qstr 通用消息字符串 message 存放从服务器接收到的字符串
QString qstr="",message="";
//发送消息初始化
QTcpServer *tcpServer;//信号监听
QList<QTcpSocket*> tcpClient;//IP数组集合
QTcpSocket *tcpSocket;//网络通信
};
#endif // DIALOG_S_H
2. dialog_s.cpp
2.1 构造函数代码
//获取本地IP
ui->lineEdit_3->setText(QNetworkInterface().allAddresses().at(1).toString());
//本地端口号
ui->lineEdit_2->setText("6666");
//初始化
ui->comboBox->addItem("全部连接");
tcpServer = new QTcpServer(this);
2.2 开始监听代码
//监听本地IP地址和端口
QHostAddress address(QNetworkInterface().allAddresses().at(1).toString());
//QHostAddress::LocalHost = address
if(!tcpServer->listen(address,6666))
{
//本地主机的6666端口,如果出错就输出错误信息,并关闭
qstr="连接错误:"+tcpServer->errorString();
ui->textEdit->insertPlainText(qstr);
ui->textEdit->insertPlainText("\n");
return;
}
ui->textEdit->insertPlainText("启动监听!");
ui->textEdit->insertPlainText("\n");
//每当新的客户端连接到服务器时,newConncetion()信号触发
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(sendMessage()));
2.3 槽函数sendMessage代码
// tcpClient.append 将信息填入List数组内
tcpSocket = tcpServer->nextPendingConnection();
tcpClient.append(tcpSocket);
//将连接进来的IP填入comboBox控件
ui->comboBox->addItem(QString("[%1:%2]:").arg(tcpSocket->peerAddress().toString()).arg(tcpSocket->peerPort()));
//readyRead检测新连接信号 error出错信号
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(ReadData()));
// "0" 为发送消息 "1" 为发送文件 deleteLater 连接异常信号
if(QString::number(bgGroup->checkedId())=="0")
{
connect(tcpSocket, SIGNAL(disconnected()),this,SLOT(deleteLater()));
//发送数据成功后,显示提示
qstr="新客户端接入!";
ui->textEdit->insertPlainText(qstr);
ui->textEdit->insertPlainText("\n");
}else if(QString::number(bgGroup->checkedId())=="1")
{
qstr="文件传输开始连接...";
ui->textEdit->insertPlainText(qstr);
ui->textEdit->insertPlainText("\n");
//tcpServer->close();
connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(displayError(QAbstractSocket::SocketError)));
}
2.4 断开监听代码
//断开监听代码
tcpSocket->disconnectFromHost();
if (tcpSocket->state() == tcpSocket->waitForDisconnected(1000)){ //已断开连接则进入if{}
ui->textEdit->insertPlainText("断开监听状态");
ui->textEdit->insertPlainText("\n");
tcpServer->close(); //不再监听端口
Break();//断开后控件状态
}else{
ui->textEdit->insertPlainText("断开监听状态失败");
ui->textEdit->insertPlainText("\n");
}
2.5 断开连接代码
//这块有问题,需要大家自行优化
for(int i=0; i<tcpClient.length(); i++)//断开所有连接
{
tcpClient[i]->disconnectFromHost();
bool ok = tcpClient[i]->waitForDisconnected(1000);
if(!ok)
{
// 处理异常
}
tcpClient.removeAt(i); //从保存的客户端列表中取去除
}
tcpServer->close(); //不再监听端口
2.6 异常disconnected槽函数代码
//由于disconnected信号并未提供SocketDescriptor,所以需要遍历寻找
for(int i=0; i<tcpClient.length(); i++)
{
if(tcpClient[i]->state() == QAbstractSocket::UnconnectedState)
{
// 删除存储在combox中的客户端信息
ui->cbxConnection->removeItem(ui->cbxConnection->findText(tr("%1:%2")\
.arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
.arg(tcpClient[i]->peerPort())));
// 删除存储在tcpClient列表中的客户端信息
tcpClient[i]->destroyed();
tcpClient.removeAt(i);
}
}
2.7 读取客户端发送的消息代码
// 客户端数据可读信号,对应的读数据槽函数 需要修改
void MyTcpServer::ReadData()
{
// 由于readyRead信号并未提供SocketDecriptor,所以需要遍历所有客户端
for(int i=0; i<tcpClient.length(); i++)
{
QByteArray buffer = tcpClient[i]->readAll();
if(buffer.isEmpty()) continue;
static QString IP_Port, IP_Port_Pre;
IP_Port = tr("[%1:%2]:").arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
.arg(tcpClient[i]->peerPort());
// 若此次消息的地址与上次不同,则需显示此次消息的客户端地址
if(IP_Port != IP_Port_Pre)
ui->edtRecv->append(IP_Port);
ui->edtRecv->append(buffer);
//更新ip_port
IP_Port_Pre = IP_Port;
}
}
2.8 向全部客户端发送代码
//全部连接
if(ui->cbxConnection->currentIndex() == 0)
{
for(int i=0; i<tcpClient.length(); i++)
tcpClient[i]->write(data.toLatin1()); //qt5除去了.toAscii()
}
2.9 向特定客户端发送代码
//指定连接
QString clientIP = ui->cbxConnection->currentText().split(":")[0];
int clientPort = ui->cbxConnection->currentText().split(":")[1].toInt();
for(int i=0; i<tcpClient.length(); i++)
{
if(tcpClient[i]->peerAddress().toString().split("::ffff:")[1]==clientIP\
&& tcpClient[i]->peerPort()==clientPort)
{
tcpClient[i]->write(data.toLatin1());
return; //ip:port唯一,无需继续检索
}
}
TCP 发送消息就告一段落了,稍等一阵子会尽快吧发送文件功能完成完毕,再给大家分享。
觉得对大家有所帮助的话点点赞,点点关注,接下来更精彩,一直在努力中。
本博客Demo下载地址:https://download.csdn.net/download/qq_37529913/13121048
获取本地计算机名、IPV4/6地址、获取了公网地址:https://blog.csdn.net/qq_37529913/article/details/107970461
全面的QSS样式:https://blog.csdn.net/qq_37529913/article/details/108735409
QT正则验证手机号、邮箱:https://blog.csdn.net/qq_37529913/article/details/109440819
博客主页地址:https://blog.csdn.net/qq_37529913?spm=1001.2101.3001.5113