QTCpSocket文件传输_qtcpsocket发送文件-程序员宅基地

技术标签: Qt/MFC GUI  tcp  文件传输  应用  

UDP由于不用建立连接,所以常用于聊天程序(点对点、群聊天等);而TCP由于其建立连接,具有可靠性强、能够保证不丢包,所以经常用于大文件的传输。但是由于TCP粘包,所以在使用TCP进行文件传输时,需要进行粘包问题的考虑。关于TCP/UDP用于聊天程序的应用可以参考:QTcpServer、QTcpSocket、QUdpSocket在聊天程序上的应用 。QTcpSocket、QTcpServer、QFile、QFileInfo这些类是Qt中用于TCP文件传输的一些主要的类。其继承派生关系如下:
这里写图片描述

文件传输的图解分析如下:
这里写图片描述

首先我们还是要创建在服务器端用于监听的QTcpServer对象,在客户端创建一个用于通信的QTcpSocket对象,然后请求连接,当连接成功以后就可以选择文件给客户端进行发送了:
发送前先进行文件信息的读取,获取文件信息与以只读方式打开文件,是在选择按钮的槽函数中完成,获取文件大小与文件名,用以发送给和客户端进行客户端文件的创建。

当文件打开、文件信息获取完毕后,Send()的槽函数就完成。接着就可以点击发送按钮,进入发送按钮的槽函数中进行发送,发送分为两部分:头部信息和文件内容数据。头部信息是在发送文件之前,告诉客户段先创建一个文件同名并打开,由于头部信息与文件内容可能会粘包,所以在头部信息发送后,应该暂停等待头部信息接收完成后进行文件内容的发送(可以指定定时器,定时器到时则发送;也可以在客户端接收头部信息完成后回射,服务器来判断回射信息)。我们以回射信息的方式进行粘包处理。

另外,由于头部信息需要发送文件名字、大小等信息。客户端要识别发送过来的QByteArray类型数据,必须先转换成QString,而转换成字符串的数据需要分解为文件名与文件大小,就需要在发送时以一定的格式发送(类似于自定义协议),解析时也按该方式进行解析(比如:在每单个信息之间加上##进行分割,拆包时也同样检测##进行字符串的分割)。

具体测试实现:

/*serverwidget.h*/
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H

#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QFile>
#include <QTimer>

#define BUF_SIZE 1024*4

namespace Ui {
class ServerWidget;
}

class ServerWidget : public QWidget
{
    Q_OBJECT

public:
    explicit ServerWidget(QWidget *parent = 0);
    ~ServerWidget();
    void sendData();    //发送文件数据
private slots:
    void on_buttonChooseFile_clicked();

    void on_butttonSendFile_clicked();

private:
    Ui::ServerWidget *ui;
    QTcpServer * tcpServer; //监听
    QTcpSocket * tcpSocket; //通信
    QFile file;             //文件对象
    QString fileName;       //文件名字
    qint64 fileSize;        //文件大小
    qint64 sendSize;        //已经发送大小
    QTimer timer;           //定时器
};

#endif // SERVERWIDGET_H
/*serverwidget.cpp*/
#include "serverwidget.h"
#include "ui_serverwidget.h"
#include <QFileDialog>
#include <QFileInfo>
#include <QByteArray>
#include <QMessageBox>

ServerWidget::ServerWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::ServerWidget)
{
    ui->setupUi(this);
    //创建对象指定父对象
    tcpServer = new QTcpServer(this);
    //绑定监听
    tcpServer->listen(QHostAddress::Any,8888);
    setWindowTitle("服务器端口:8888");

    //两个按钮最开始都不能按
    ui->buttonChooseFile->setEnabled(false);
    ui->butttonSendFile->setEnabled(false);

    //如果客户端和服务器成功连接
    //tcpServer会自动触发newConnection()信号
    connect(tcpServer,&QTcpServer::newConnection,
            [=](){
                //获取通信套接字
                tcpSocket = tcpServer->nextPendingConnection();
                //获取对方IP、port
                QString ip = tcpSocket->peerAddress().toString();
                quint16 port = tcpSocket->peerPort();

                QString str = QString("[%1:%2]:成功连接").arg(ip).arg(port);
                ui->textEdit->append(str);

                //成功连接后可以选择
                ui->buttonChooseFile->setEnabled(true);

                connect(tcpSocket, QTcpSocket::readyRead,
                        [=](){
                            QByteArray buf = tcpSocket->readAll();
                            //采用回射信息进行粘包处理
                            if("FileHead recv" == QString(buf)){
                                //ui->textEdit->append("文件头部接收成功,开始发送文件...");
                                sendData();
                            }
                            else if("file write done" == QString(buf)){
                            //服务器发送的快,而客户端接收的慢,所以要等客户端接收完毕后才能断开连接,以免丢包
                                QMessageBox::information(this,"完成","对端接收完成");
                                //ui->textEdit->append("文件发送且接收完成");
                                file.close();
                                tcpSocket->disconnectFromHost();
                                tcpSocket->close();
                            }
                        }
                        );
            }
            );
}

ServerWidget::~ServerWidget()
{
    delete ui;
}

//选择文件
void ServerWidget::on_buttonChooseFile_clicked()
{
    QString filePath = QFileDialog::getOpenFileName(this,"open","../");
    if(false == filePath.isEmpty()){    //路径有效
        fileName.clear();
        fileSize = 0;
        //获取文件信息:名字、大小
        QFileInfo info(filePath);
        fileName = info.fileName();
        fileSize = info.size();
        sendSize = 0;   //已经发送文件大小

        //以只读方式打开文件
        file.setFileName(filePath);
        if(false == file.open(QIODevice::ReadOnly)){
            ui->textEdit->append("只读方式打开文件失败");
        }
        //提示已经打开的文件路径
        ui->textEdit->append(filePath);

        //可以发送
        ui->buttonChooseFile->setEnabled(false);  //只能选择一次
        ui->butttonSendFile->setEnabled(true);
    }
    else{
        ui->textEdit->append("选择文件路径无效:SERVER80");
    }
}

void ServerWidget::on_butttonSendFile_clicked()
{
    //发送按钮已经点击,发送过程中不能再点击,恢复按钮初始化
    ui->buttonChooseFile->setEnabled(false);
    ui->butttonSendFile->setEnabled(false);
    //先发送文件头信息:文件名##大小
    //构造头部信息
    QString head = QString("%1##%2").arg(fileName).arg(fileSize);
    //发送头部信息
    qint64 len = tcpSocket->write(head.toUtf8());

    if(len < 0){
        ui->textEdit->append("文件头部信息发送失败!");
        //关闭文件
        file.close();
    }
}
void ServerWidget::sendData()
{
    qint64 len = 0;
    do{
        //一次发送的大小
        char buf[BUF_SIZE] = {
   0};
        len = 0;
        len = file.read(buf,BUF_SIZE);  //len为读取的字节数
        len = tcpSocket->write(buf,len);    //len为发送的字节数
        //已发数据累加
        sendSize += len;
    }while(len > 0);
}

客户端头文件将sendSize改为recvSize、添加一个布尔类型的isStart标志位,用来判断头部是否已经发送并给接收、少一个选择文件路径的槽函数(也可以不少,而可指定下载路径)。

#include "clientwidget.h"
#include "ui_clientwidget.h"

#include <QByteArray>
#include <QMessageBox>
ClientWidget::ClientWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::ClientWidget)
{
    ui->setupUi(this);
    ui->progressBar->setValue(0);
    tcpSocket = new QTcpSocket(this);

    connect(tcpSocket,&QTcpSocket::readyRead,
            [=](){
                QByteArray buf = tcpSocket->readAll();
                if(true == isStart){
                    isStart = false;
                    //接收包头
                    fileName = QString(buf).section("##",0,0);
                    fileSize = QString(buf).section("##",1,1).toInt();
                    recvSize = 0;

                    QString str = QString("接收的文件:[%1:%2kB]").arg(fileName).arg(fileSize/1024);
                    setWindowTitle(str);

                    ui->progressBar->setMaximum(fileSize);
                    ui->progressBar->setMinimum(0);
                    ui->progressBar->setValue(0);

                    file.setFileName(fileName);
                    if(false == file.open(QIODevice::WriteOnly)){
                        QMessageBox::information(this,"Error","文件创建并打开失败!");
                    }
                    tcpSocket->write("FileHead recv");
                }else{
                    //接收处理文件
                    qint64 len = file.write(buf);
                    recvSize += len;

                    ui->progressBar->setValue(recvSize);
                    if(recvSize == fileSize){//接收完毕
                        file.close();
                        //提示信息
                        QMessageBox::information(this,"完成","文件接收完成");
                        //回射信息
                        tcpSocket->write("file write done");
                        tcpSocket->disconnectFromHost();
                        tcpSocket->close();
                    }
                }
            }
            );

}
ClientWidget::~ClientWidget()
{
    delete ui;
}

void ClientWidget::on_buttonConnect_clicked()
{
    QString ip = ui->lineEditIp->text();
    qint16 port = ui->lineEditPort->text().toInt();
    QMessageBox::information(this,"连接状态","服务器连接成功");
    tcpSocket->connectToHost(QHostAddress(ip),port);
    isStart = true;

}

结果测试(最终对比复制的文件与原文件MD5是否相同):
这里写图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Apollon_krj/article/details/72876746

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法