您当前的位置: 首页 >  网络

龚建波

暂无认证

  • 3浏览

    0关注

    313博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Qt网络编程(3):TCP文件传输

龚建波 发布时间:2020-08-09 13:55:44 ,浏览量:3

1.实现思路

要实现文件传输功能,需要在基础的 TCP 通信的代码上进行修改。

主要有两个要处理的点:文件的读写,TCP 收发。

对于文件的读写,每次只读取部分数据进行发送,然后 seek 到紧邻的位置便于下次读取;接收端写文件更简单,收到文件数据写文件就行了(准备传输的时候会把文件名和文件长度发到接收端)。

要实现 TCP 发送和接收文件,需要拟定通信协议,用于从字节流中取出数据帧(多次发送的数据连在一起,或者一次发送的分多次收到,就需要我们先把接收的数据缓存起来,根据协议逐帧解析和校验)。我写了个很简陋的协议:

传输协议
帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
帧头:4字节定值 0x0F 0xF0 0x00 0xFF
帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
帧类型:1字节
- 0x01 [S/C]准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
- 0x02 [C]文件数据
- 0x03 [S/C]发送结束,客户端无数据段,服务端接收返回1字节数据段:成功=1,失败=0,可扩展失败原因枚举
- 0x04 [S/C]取消发送
(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
帧尾:2字节定值 0x0D 0x0A

发送数据时在命令或数据的头尾加上帧头帧尾即可:

//帧头+长度+类型
char frameHead[7] = { 0x0F, (char)0xF0, 0x00, (char)0xFF, 0x00, 0x00, 0x00 };
//帧尾
char frameTail[2] = { 0x0D, 0x0A };

void ServerOperate::sendData(char type, const QByteArray &data)
{
    if(!socket->isValid())
        return;
    frameHead[6]=type;
    const quint64 data_size=data.count();
    frameHead[5]=data_size%0x100;
    frameHead[4]=data_size/0x100;

    //发送头+数据+尾
    socket->write(frameHead,7);
    socket->write(data);
    socket->write(frameTail,2);
}

对于数据的解析,校验帧头帧长帧尾(这里偷懒没有设计校验字段)之后,通过帧类型字段分别处理。以接收端为例:

//dataTemp为QByteArray成员变量
void ServerOperate::operateReceiveData(const QByteArray &data)
{
    static QByteArray frame_head=QByteArray(frameHead,4);
    //这里只是简单的处理,所以用了QByteArray容器做缓存
    dataTemp+=data;

    //处理数据
    while(true){
        //保证以帧头为起始
        while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
            dataTemp.remove(0,1); //左边移除一字节
        }
        //小于最小帧长
        if(dataTemp.size()connectToHost(QHostAddress(address),port);
    }else{
        emit logMessage("socket->state() != QAbstractSocket::UnconnectedState");
    }
}

void ClientOperate::disconnectTcp()
{
    doDisconnect();
}

void ClientOperate::startFileTransfer()
{
    //之前如果打开了先释放
    doCloseFile();
    if(!socket->isValid())
        return;
    const QString file_path=getFilePath();
    //无效路径
    if(file_path.isEmpty() || !QFile::exists(file_path)){
        emit logMessage("无效的文件路径"+file_path);
        return;
    }
    file=new QFile(this);
    file->setFileName(file_path);
    //打开失败
    if(!file->open(QIODevice::ReadOnly)){
        doCloseFile();
        emit logMessage("打开文件失败"+file_path);
        return;
    }

    sendSize=0;
    fileSize=file->size();
    if(fileSize>0%0x100;
    file_size[2]=data_size>>8%0x100;
    file_size[1]=data_size>>16%0x100;
    file_size[0]=data_size>>24;
    //把文件大小和文件名发送给服务端,然后等待确认命令的返回
    QFileInfo info(file_path);
    sendData(0x01,QByteArray(file_size,4)+info.fileName().toUtf8());
}

void ClientOperate::cancelFileTransfer()
{
    //关闭文件
    doCancel();
    //发送停止传输指令
    sendData(0x04,QByteArray());
}

void ClientOperate::initOperate()
{
    socket=new QTcpSocket(this);

    //收到数据,触发readyRead
    connect(socket,&QTcpSocket::readyRead,[this]{
        //没有可读的数据就返回
        if(socket->bytesAvailable()readAll());
    });

    //连接状态改变
    connect(socket,&QTcpSocket::connected,[this]{
        setConnected(true);
        emit connectStateChanged(true);
        emit logMessage(QString("已连接服务器 [%1:%2]")
                        .arg(socket->peerAddress().toString())
                        .arg(socket->peerPort()));
    });
    connect(socket,&QTcpSocket::disconnected,[this]{
        setConnected(false);
        emit connectStateChanged(false);
        emit logMessage(QString("与服务器连接已断开 [%1:%2]")
                        .arg(socket->peerAddress().toString())
                        .arg(socket->peerPort()));
    });

    timer=new QTimer(this);

    //通过定时器来控制数据发送
    connect(timer,&QTimer::timeout,[this]{
        if(!socket->isValid()){
            doCancel();
            emit logMessage("Socket不可操作,发送终止");
            return;
        }
        if(!file||!file->isOpen()){
            doCancel();
            emit logMessage("文件操作失败,发送终止");
            return;
        }

        const qint64 read_size=file->read(fileBuffer,4096);
        //socket->write(fileBuffer,read_size);
        sendFile(fileBuffer,read_size);
        sendSize+=read_size;
        file->seek(sendSize);
        if(!socket->waitForBytesWritten()){
            doCancel();
            emit logMessage("文件发送超时,发送终止");
            return;
        }
        //避免除零
        if(fileSize>0){
            emit progressChanged(sendSize*100/fileSize);
        }
        if(sendSize>=fileSize){
            doCancel();
            emit logMessage("文件发送完成");
            emit progressChanged(100);
            sendData(0x03,QByteArray());
            return;
        }
    });
}

void ClientOperate::doDisconnect()
{
    //断开socket连接,释放资源
    socket->abort();
    doCloseFile();
}

void ClientOperate::doCloseFile()
{
    if(file){
        file->close();
        delete file;
        file=nullptr;
    }
}

void ClientOperate::doCancel()
{
    timer->stop();
    if(file){
        //关闭文件
        doCloseFile();
    }
}

void ClientOperate::sendData(char type,const QByteArray &data)
{
    //传输协议
    //帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
    //帧头:4字节定值 0x0F 0xF0 0x00 0xFF
    //帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
    //帧类型:1字节
    //- 0x01 准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
    //- 0x02 文件数据
    //- 0x03 发送结束
    //- 0x04 取消发送
    //(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
    //帧尾:2字节定值 0x0D 0x0A
    if(!socket->isValid())
        return;
    frameHead[6]=type;
    const quint64 data_size=data.count();
    frameHead[5]=data_size%0x100;
    frameHead[4]=data_size/0x100;

    //发送头+数据+尾
    socket->write(frameHead,7);
    socket->write(data);
    socket->write(frameTail,2);
}

void ClientOperate::sendFile(const char *data, int size)
{
    if(!socket->isValid())
        return;
    frameHead[6]=(char)0x02;
    const quint64 data_size=size;
    frameHead[5]=data_size%0x100;
    frameHead[4]=data_size/0x100;

    //发送头+数据+尾
    socket->write(frameHead,7);
    socket->write(data,size);
    socket->write(frameTail,2);
}

void ClientOperate::operateReceiveData(const QByteArray &data)
{
    static QByteArray frame_head=QByteArray(frameHead,4);
    //这里只是简单的处理,所以用了QByteArray容器做缓存
    dataTemp+=data;

    //处理数据
    while(true){
        //保证以帧头为起始
        while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
            dataTemp.remove(0,1); //左边移除一字节
        }
        //小于最小帧长
        if(dataTemp.size()isListening();
}

void ServerOperate::listen(const QString &address, quint16 port)
{
    if(server->isListening())
        doDislisten();
    //启动监听
    const bool result=server->listen(QHostAddress(address),port);
    emit listenStateChanged(result);
    emit logMessage(result?"服务启动成功":"服务启动失败");
}

void ServerOperate::dislisten()
{
    doDislisten();
    emit listenStateChanged(false);
    emit logMessage("服务关闭");
}

void ServerOperate::cancelFileTransfer()
{
    //关闭文件
    doCancel();
    //发送停止传输指令
    sendData(0x04,QByteArray());
}

void ServerOperate::initOperate()
{
    server=new QTcpServer(this);
    //监听到新的客户端连接请求
    connect(server,&QTcpServer::newConnection,this,[this]{
        //如果有新的连接就取出
        while(server->hasPendingConnections())
        {
            //nextPendingConnection返回下一个挂起的连接作为已连接的QTcpSocket对象
            QTcpSocket *new_socket=server->nextPendingConnection();
            emit logMessage(QString("新的客户端连接 [%1:%2]")
                            .arg(new_socket->peerAddress().toString())
                            .arg(new_socket->peerPort()));
            //demo只支持一个连接,多余的释放掉
            if(socket){
                new_socket->abort();
                new_socket->deleteLater();
                emit logMessage("目前已有客户端连接,新连接已释放");
                continue;
            }else{
                socket=new_socket;
            }

            //收到数据,触发readyRead
            connect(socket,&QTcpSocket::readyRead,[this]{
                //没有可读的数据就返回
                if(socket->bytesAvailable()readAll());
            });

            //连接断开,销毁socket对象
            connect(socket,&QTcpSocket::disconnected,[this]{
                emit logMessage(QString("客户端连接已断开 [%1:%2]")
                                .arg(socket->peerAddress().toString())
                                .arg(socket->peerPort()));
                socket->deleteLater();
                socket=nullptr;
            });
        }
    });
}

void ServerOperate::doDislisten()
{
    //关闭服务,断开socket连接,释放资源
    server->close();
    if(socket){
        socket->abort();
    }
    if(file){
        file->close();
    }
}

void ServerOperate::doCloseFile()
{
    if(file){
        file->close();
        delete file;
        file=nullptr;
    }
}

void ServerOperate::doCancel()
{
    if(file){
        //关闭文件
        doCloseFile();
    }
}

bool ServerOperate::readyReceiveFile(qint64 size, const QString &filename)
{
    //重置状态
    fileSize=size;
    receiveSize=0;
    if(file){
        doCloseFile();
    }
    //创建qfile用于写文件
    file=new QFile(this);
    QString file_path=getFilePath();
    if(file_path.isEmpty())
        file_path=QApplication::applicationDirPath();
    file->setFileName(file_path+"/"+filename);
    //Truncate清掉原本内容
    if(!file->open(QIODevice::WriteOnly)){
        emit logMessage("创建文件失败,无法进行接收"+file->fileName());
        return false;
    }
    emit logMessage("创建文件成功,准备接收"+file->fileName());
    return true;
}

void ServerOperate::onReceiveFile(const char *data, qint64 size)
{
    if(!file||!file->isOpen()){
        doCancel();
        //发送停止传输指令
        sendData(0x04,QByteArray());
        emit logMessage("文件操作失败,取消接收");
        return;
    }
    if(size>0){
        const qint64 write_size = file->write(data,size);
        //感觉这个waitForBytesWritten没啥用啊
        if(write_size!=size && !file->waitForBytesWritten(3000)){
            doCancel();
            //发送停止传输指令
            sendData(0x04,QByteArray());
            emit logMessage("文件写入超时,取消接收");
            return;
        }
    }
    receiveSize+=size;
    //避免除零
    if(fileSize>0){
        emit progressChanged(receiveSize*100/fileSize);
    }
    if(receiveSize>=fileSize){
        doCancel();
        emit logMessage("文件接收完成");
        emit progressChanged(100);
        return;
    }
}

void ServerOperate::sendData(char type, const QByteArray &data)
{
    //传输协议
    //帧结构:帧头4+帧长2+帧类型1+帧数据N+帧尾2(没有校验段,懒得写)
    //帧头:4字节定值 0x0F 0xF0 0x00 0xFF
    //帧长:2字节数据段长度值 arr[4]*0x100+arr[5] 前面为高位后面为低位
    //帧类型:1字节
    //- 0x01 准备发送文件,后跟四字节文件长度和N字节utf8文件名,长度计算同帧长一样前面为高位后面为低位
    //- 0x02 文件数据
    //- 0x03 发送结束
    //- 0x04 取消发送
    //(服务端收到0x01 0x03开始和结束发送两个命令要进行应答,回同样的命令码无数据段)
    //帧尾:2字节定值 0x0D 0x0A
    if(!socket->isValid())
        return;
    frameHead[6]=type;
    const quint64 data_size=data.count();
    frameHead[5]=data_size%0x100;
    frameHead[4]=data_size/0x100;

    //发送头+数据+尾
    socket->write(frameHead,7);
    socket->write(data);
    socket->write(frameTail,2);
}

void ServerOperate::operateReceiveData(const QByteArray &data)
{
    static QByteArray frame_head=QByteArray(frameHead,4);
    //这里只是简单的处理,所以用了QByteArray容器做缓存
    dataTemp+=data;

    //处理数据
    while(true){
        //保证以帧头为起始
        while(!dataTemp.startsWith(frame_head)&&dataTemp.size()>4){
            dataTemp.remove(0,1); //左边移除一字节
        }
        //小于最小帧长
        if(dataTemp.size()            
关注
打赏
1655829268
查看更多评论
0.0375s