QT学习笔记(16)-利用Socket传输xml文件并在客户端显示成树形结构

前言

这篇文章会是13,14,15的一个小集锦,将三篇文章的功能进行汇总,即在服务端将xml文件传输到客户端,客户端对其进行解析,显示为树形结构。注意,这里因为需求的缘故,需要与第(13)不同的是,这里传输的是指定的文件,并没有让我们选择文件。下面将对该需求进行实现,首先先展示一下效果图。

效果图

在这里插入图片描述

基本上是用最简易的方式去实现要实现的功能,让用户所需要做的操作越少越好。

程序解析

ServerWidget.cpp

下面这个是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";
    }
}

ClientWidget.cpp

这是该函数的构造函数,首先,先进行初始化,然后等待用户按下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();
}

ClientTreeView.cpp

该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文件
}

ClientTreeModel.cpp

解析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:如有错误,欢迎指正~

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值