这篇文章会是13,14,15的一个小集锦,将三篇文章的功能进行汇总,即在服务端将xml文件传输到客户端,客户端对其进行解析,显示为树形结构。注意,这里因为需求的缘故,需要与第(13)不同的是,这里传输的是指定的文件,并没有让我们选择文件。下面将对该需求进行实现,首先先展示一下效果图。
基本上是用最简易的方式去实现要实现的功能,让用户所需要做的操作越少越好。
下面这个是CServerWidget的构造函数,是connect函数的所在地。首先先进行一些小控件的初始化。然后,等待客户端发起连接,connect函数执行与客户端的连接操作,接下来,等待用户按下Send File按钮,发送文件信息以及文件数据。然后等待发送成功时,客户端写入TcpSocket中的file done
数据,然后服务端断开连接。并且,还有一个定时器,当文件头部信息发送结束后,会开启一个定时器,定时器结束后,才真正开始发送文件数据。这个操作是为了防止文件信息与文件数据的黏包行为。
CServerWidget::CServerWidget(QWidget *parent)
: QWidget(parent)
{
initServerWidget();
//这里是倘若建立了一个新的连接,执行槽函数serverNewConnection中的内容。
connect(tcpServer, SIGNAL(newConnection()),this,SLOT(serverNewConnection()));
//获取完服务端的文件,等待send按钮被按下,先只是发送头部信息而已,并开启一个小型定时器。等定时器执行完,执行下面有一个connect函数
connect(buttonSend,SIGNAL(pressed()), this, SLOT(on_buttonSend_clicked()));
//加入server可以从tcp中得到readyread的信号的话,证明估计client应该已经结束了。
connect(tcpSocket, SIGNAL(readyRead()),this,SLOT(slotClientReceiveOver()));
connect(&timer, &QTimer::timeout,
[=]()
{
qDebug()<<"ServerWidget timer";
//关闭定时器
timer.stop();
//发送文件
sendData();
}
);
}
这是当检测到数据已传输完毕,所执行的断开操作。
void CServerWidget::slotClientReceiveOver()
{
QByteArray buf = tcpSocket->readAll();
if(QString(buf) == "file done")
{//文件接收完毕
file.close();
//断开客户端端口
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
这里的槽函数用处不是特别大,但你若想获取连接你的对方的ip和port,则可以使用这个函数 tcpServer->nextPendingConnection();
//这里的函数可获取客户端的ip和port
void CServerWidget::serverNewConnection()
{
qDebug()<<"CServerWidget::serverNewConnection";
//取出建立好连接的套接字
tcpSocket = tcpServer->nextPendingConnection();
//获取对方的ip和端口
QString ip = tcpSocket->peerAddress().toString();
quint16 port = tcpSocket->peerPort();
QString str = QString("[%1:%2] success connect").arg(ip).arg(port);
qDebug()<<"CServerWidget::serverNewConnection peer Connect ip and port is"<<ip<<port;
getServerFile();//获取服务端文件
}
这是点击发送按钮,发送文件信息头部。并开启定时器,防止黏包。
//本小类用于当点击send按钮时,则发送头部信息,并开启定时器,防止黏包。
void CServerWidget::on_buttonSend_clicked()
{
//buttonSend->setEnabled(false);
//qDebug()<<"buttonSend";
//先发送文件头信息 文件名##文件大小
qDebug()<<"CServerWidget::on_buttonSend_clicked"<<fileName<<fileSize;
QString head = QString("%1##%2").arg(fileName).arg(fileSize);
//发送头部信息
qint64 len = tcpSocket->write( head.toUtf8() );
qDebug()<<"CServerWidget::on_buttonSend_clicked"<<len;
//qint64 len = 10;
if(len > 0)//头部信息发送成功
{
qDebug()<<"CServerWidget::on_buttonSend_clicked sendhead success";
//发送真正的文件信息
//防止TCP黏包
//需要通过定时器延时 20 ms
timer.start(20);
}
else
{
qDebug() << "The head message send fail 142";
file.close();
//buttonSend->setEnabled(false);
}
}
这个函数就是用于发送真正的文件数据了,并记录发送数据的大小。
//本小类用于发送真正的数据,这里固定每次发送数据的大小,进行统一发送
void CServerWidget::sendData()
{
qDebug()<<"exec server sendData";
qint64 len = 0;
//QByteArray array = file.readAll();
//tcpSocket->write(array);
//tcpSocket->readAll();
sendSize =0;
do
{
//每次发送数据的大小
char buf[4*1024] = {0};
len = 0;
//往文件中读数据
len = file.read(buf, sizeof(buf));
//发送数据,读多少,发多少
//len = tcpSocket->
len = tcpSocket->write(buf, len);
//发送的数据需要累积
sendSize += len;
}while(len > 0);
qDebug()<<"CServerWidget::sendData"<<sendSize;
}
这里获取指定路径下的文件,注意,这里使用的是相对路径,对于这个更改方法可以看一下https://blog.csdn.net/weixin_38809485/article/details/107162637中的2020年7月21日
这天的笔记。
//这里用于获取要传输的文件
void CServerWidget::getServerFile()
{
qDebug()<<"CServerWidget::getServerFile() exec";
QString filePath = qApp->applicationDirPath()+"/xml/test.xml";
qDebug()<<"filePath"<<filePath;
if(false == filePath.isEmpty()) //如果选择文件路径有效
{
fileName.clear();
fileSize = 0;
//获取文件信息
QFileInfo info(filePath);
fileName = info.fileName(); //获取文件名字
fileSize = info.size(); //获取文件大小
sendSize = 0; //发送文件的大小
//只读方式打开文件
//指定文件的名字
file.setFileName(filePath);
//打开文件
bool isOk = file.open(QIODevice::ReadOnly);
if(false == isOk)
{
qDebug() << "open file fail with readonly 106";
}
else {
qDebug()<<"the path is right";
}
}
else
{
qDebug() << "Choose file path is wrong in ServerWidget getFile";
}
}
这是该函数的构造函数,首先,先进行初始化,然后等待用户按下connect按钮,与服务端进行连接,然后读取服务端传输过来的内容,并写成文件的形式,为后面展示成树形结构打下基础。并且,等待客户端的用户按下displayTree按钮,将文件显示成树形结构。
#include "ClientWidget.h"
CClientWidget::CClientWidget(QWidget *parent) : QWidget(parent)
{
initClientWidget();
connect(buttonConnect,SIGNAL(clicked()), this, SLOT(on_buttonConnect_clicked()));//一旦打印出connect success就是连接成功了
isStart = true;
connect(tcpSocket, SIGNAL(readyRead()),this,SLOT(clientReadFile()));
connect(buttonDisplayTree,&QPushButton::clicked,this,&CClientWidget::slotTreeDisplay);
}
本小类用于对ClientWidget进行初始化,包括一些按钮,widget等。
void CClientWidget::initClientWidget()
{
tcpSocket = new QTcpSocket(this);
ClientWidget = new QWidget(this);
ClientWidget->resize(400, 300);
buttonConnect = new QPushButton(ClientWidget);
buttonConnect->setText("connect");
buttonDisplayTree = new QPushButton(ClientWidget);
buttonDisplayTree->move(0,60);
buttonDisplayTree->setText("DisplayTree");
}
客户端连接固定地址的固定端口。
void CClientWidget::on_buttonConnect_clicked()
{
//tcpSocket = new QTcpSocket(this);
QString ip = "127.0.0.1";
quint16 port = 8888;
qDebug()<<"CClientWidget::initClientWidget port"<<port;
tcpSocket->connectToHost(QHostAddress(ip), port);
connect(tcpSocket, &QTcpSocket::connected,
[=]()
{
//提示连接成功
qDebug()<<"connect success";
}
);
}
读取socket传输过来的信息,并写入文件之中,为后续显示成树形结构作好准备。
//本小类用于当Socket中readyRead()时,执行读取socket中文件
void CClientWidget::clientReadFile()
{
//取出接收的内容
QByteArray buf = tcpSocket->readAll();//把这个buf传递给ServerTreeView
qDebug()<<"CClientWidget::clientReadFile buf.size()"<<buf.size();
if(true == isStart)//在第一次文件头发过来的时候,不要去处理这些数据,等第二次或后面的n多次再去处理
{//接收头
isStart = false;
//解析头部信息 QString buf = "hello##1024"
// QString str = "hello##1024#mike";
// str.section("##", 0, 0)
//初始化
//文件名
fileName = QString(buf).section("##", 0, 0);
//文件大小
fileSize = QString(buf).section("##", 1, 1).toInt();
qDebug()<<"CClientWidget::clientReadFile fileSize"<<fileSize;
recvSize = 0; //已经接收文件大小
//打开文件
//关联文件名字
QString xmlPath2 = qApp->applicationDirPath()+"/client/test2.xml";
file.setFileName(xmlPath2);
//file.setFileName(fileName);//生成的file的目录在当前文件夹下qApp->applicationDirPath()
//只写方式方式,打开文件
bool isOk = file.open(QIODevice::WriteOnly);
if(false == isOk)
{
qDebug() << "CClientWidget::clientReadFile WriteOnly error 49";
tcpSocket->disconnectFromHost(); //断开连接
tcpSocket->close(); //关闭套接字
return; //如果打开文件失败,中断函数
}
}
else //文件信息
{
bufAll+=buf;
//可以在这里不断的将buf汇总到bufAll之中
qint64 len = file.write(buf);//这里做的是一个写入文件的操作。
qDebug()<<"CClientWidget::clientReadFile len"<<len;
if(len >0) //接收数据大于0
{
recvSize += len; //累计接收大小
}
if(recvSize == fileSize) //文件接收完毕
{
qDebug()<<"file done";
//emit signalBufEmit(bufAll);
//p_treeView = new CClientTreeView();
emit signalBufEmit(bufAll);
//p_treeView->initTreeView(bufAll);
// p_treeView->receiveByte(bufAll);
//先给服务发送(接收文件完成的信息)
tcpSocket->write("file done");
// textEdit->append("file receive over");
QMessageBox::information(this, "over", "file receive over");
file.close(); //关闭文件
//断开连接
tcpSocket->disconnectFromHost();
tcpSocket->close();
}
}
}
本槽函数用于显示树形结构。读取文件中的内容结束后,便把该文件删除,相当于只使用其中的字节流而已,这是需求所至。
void CClientWidget::slotTreeDisplay()
{
//提示连接成功
CClientTreeView *w2 = new CClientTreeView();
w2->setWindowTitle("Tree");
w2->show();
QString xmlPath3 = qApp->applicationDirPath()+"/client/test2.xml";
QFile fileTemp(xmlPath3);
fileTemp.remove();
}
该View较为简单,只有下述代码,设置model,并setModel,并解析之前的xml文件。
CClientTreeView::CClientTreeView(QDialog *parent) : QTreeView(parent)
{
qDebug()<<"CClientTreeView";
p_model = new CClientTreeModel();
this->setModel(p_model);
QString xmlPath = qApp->applicationDirPath()+"/client/test2.xml";
p_model->setHorizontalHeaderLabels(QStringList()<<QStringLiteral("文件目录"));
p_model->readFile(xmlPath);//解析xml文件
}
解析xml,并利用xml文件构造好整棵树。并且,先把root加进model之中。
bool CClientTreeModel::readFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
std::cerr << "Error:"
"Cannot read file " << qPrintable(fileName)
<< ": " << qPrintable(file.errorString())
<< std::endl;
return false;
}
QString errorStr;
int errorLine;
int errorColumn;
QDomDocument doc;
if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
{
std::cerr << "Error: Parse error at line " << errorLine << ", "
<< "column " << errorColumn << ": "
<< qPrintable(errorStr) << std::endl;
return false;
}
QDomElement root = doc.documentElement();
if (root.tagName() != "root")
{
std::cerr << "Error: Not a school file" << std::endl;
return false;
}
else{
QFileInfo appInfo(root.attribute("Path"));
QString value = appInfo.baseName();//文件名
item1 = new QStandardItem((value));
this->appendRow(item1);
qDebug()<<"root";
}
parseAllMembers(root,item1);
return true;
}
本函数,用于将树中的内容加入到model之中,运行完该函数,model中的数据就已被填写完整了。
//本小类用于将doc树中的元素添加到Item之中,从而实现显示树形功能
void CClientTreeModel::parseAllMembers(const QDomElement &element,QStandardItem *ParentItem)
{
QDomNode child = element.firstChild();
while(!child.isNull())
{
QFileInfo appInfo(child.toElement().attribute("Path"));
QString value = appInfo.baseName();//文件名
QString suffix = appInfo.suffix();
QString readvalue = value + "."+suffix;
ChildItem = new QStandardItem(readvalue);
childItems.clear();
childItems.push_back(ChildItem);
ParentItem->appendRows(childItems);
childItems.clear();
if (child.toElement().tagName() == "Folder")
{
parseAllMembers(child.toElement(),ChildItem);
}
child = child.nextSibling();
}
}
https://download.csdn.net/download/weixin_38809485/12624914
P.S:如有错误,欢迎指正~