You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用QFtp raw命令STOR上传文件遇数据通道超时问题求助

问题描述

需要向服务器上传文件并实现断点续传,计划使用QFtp的raw命令结合REST实现。当前使用STOR命令时,收到227回复码后解析IP和端口,收到150回复码时创建TCP套接字发送数据,但几秒后出现错误:

Data channel timed out. Win32 error: The network connection was aborted by the local system.

附原始代码:

#include "client.h"
#include <QDebug>
#include <QNetworkAccessManager>
#include <QFileInfo>
QString ConvertSize(qint64 size){
    QString output("0 Bytes");
    if(size<1024){
        output=QString::number(size)+" Bytes";
        return output;
    }else if(size>1024 && size <1048576){
        double newNumber=size/1024.0;
        output=QString::number(newNumber)+" KBs";
        return output;
    }else if(size >1048576){
        double newNumber=size/1048576.0;
        output=QString::number(newNumber)+" MBs";
        return output;
    }else if(size >1073741824){
        double newNumber=size/1073741824.0;
        output=QString::number(newNumber)+" GBs";
        return output;
    }else{
        return output;
    }
}
Client::Client(QObject *parent)
    : QObject{parent}
{
    qInfo()<<this<<"constructed on: "<<QThread::currentThread();
    connect(&ftp,&QFtp::commandStarted,this,&Client::commandStarted);
    connect(&ftp,&QFtp::commandFinished,this,&Client::commandFinished);
    connect(&ftp,&QFtp::dataTransferProgress,this,&Client::dataProgress);
    connect(&ftp,&QFtp::done,this,&Client::done);
    connect(&ftp,&QFtp::listInfo,this,&Client::listInfo);
    connect(&ftp,&QFtp::stateChanged,this,&Client::stateChanged);
    connect(&ftp,&QFtp::rawCommandReply,this,&Client::rawCommandReply);


}

Client::~Client()
{
    qInfo()<<this<<"deConstructed on: "<<QThread::currentThread();
}

void Client::connectToHost(QString ip, quint16 port)
{
    qInfo()<<"connecting to    "<<ip<<":"<<port;
    ftp.connectToHost(ip,port);
    qInfo()<<"loging in";
    ftp.login("testuser","123456");

}

void Client::commandStarted(int id)
{
    //qInfo()<<"command start:"<<id;
    quint8  command=ftp.currentCommand();
    switch (command){
    case 0:qInfo()<<"command start: "<<None";break;
    case 1:qInfo()<<"command start: "<<SetTransferMode";break;
    case 2:qInfo()<<"command start: "<<SetProxy";break;
    case 3:qInfo()<<"command start: "<<ConnectToHost";break;
    case 4:qInfo()<<"command start: "<<Login";break;
    case 5:qInfo()<<"command start: "<<Close";break;
    case 6:qInfo()<<"command start: "<<List";break;
    case 7:qInfo()<<"command start: "<<Cd";break;
    case 8:qInfo()<<"command start: "<<Get";break;
    case 9:qInfo()<<"command start: "<<Put";break;
    case 10:qInfo()<<"command start: "<<Remove";break;
    case 11:qInfo()<<"command start: "<<Mkdir";break;
    case 12:qInfo()<<"command start: "<<Rmdir";break;
    case 13:qInfo()<<"command start: "<<Rename";break;
    case 14:qInfo()<<"command start: "<<RawCommand";break;

    }
}

void Client::commandFinished(int id)
{
    qInfo()<<"command finished: "<<id;
    if(ftp.error()){
        qInfo()<<ftp.errorString();
    }else{
        qInfo()<<"no error";
    }
    if(ftp.currentCommand()==9){
        myfile->flush();
        myfile->close();
        myfile->deleteLater();
        myfile=NULL;
        qInfo()<<"put command ended.closing open file.";

    }
}

void Client::rawCommandReply(int code, QString detail)
{


    qInfo()<<"reply code: "<<code<<"detail: "<<detail;
    if (code==226){
        qInfo()<<"transmission ended";
    }
    if (code==150){
        qInfo()<<"serverReady";
        dataChanel();
    }
    if(code==227){
        QString ip1=detail.split("(")[1];
        QString ip2=ip1.split(")")[0];
        qInfo()<<ip2;
        QStringList data=ip2.split(",");
        QStringList ip;
        QStringList ports;

        for (int i=0;i<data.size();i++){
            if(i<4){
                ip.append(data[i]);

            }else {
                ports.append(data[i]);
            }
        }
        qInfo()<<"ip:"<<ip.join(".");
        IpPorts.append(ip.join("."));
        qInfo()<<"port:"<<ports.join("-");
        for(int i=0;i<ports.size();i++){


            IpPorts.append(ports[i]);
        }

    }
}

void Client::dataProgress(quint64 now, quint64 all)
{
    qInfo()<<"now:\t"<<ConvertSize(now)<<"\t from:\t"<<ConvertSize(all);
}

void Client::done(bool error)
{
    if (error){
        qInfo()<<"error occured when done:"<<ftp.errorString();
    }
    //ftp.close();
    qInfo()<<"ftp connection closed";


}

void Client::listInfo(const QUrlInfo &i)
{
    qInfo()<<"dir found:"<<i.name();
}

void Client::stateChanged(int state)
{

    switch(state){
    case 0:qInfo()<<"state changed: "<<Unconnected";break;
    case 1:qInfo()<<"state changed: "<<HostLookup";break;
    case 2:qInfo()<<"state changed: "<<Connecting";break;
    case 3:qInfo()<<"state changed: "<<Connected";break;
    case 4:qInfo()<<"state changed: "<<LoggedIn";break;
    case 5:qInfo()<<"state changed: "<<Closing";break;
    }
}



void Client::abort()
{
    ftp.abort();
    qInfo()<<"ftp connection aborted";
}

void Client::ended()
{

    qInfo()<<"ftp connection ended";

}

void Client::upload(QString &location)
{
    myfile=new QFile(location);
    if(myfile->open(QIODevice::ReadOnly)){
        ftp.rawCommand("PASV");
        ftp.rawCommand("TYPE I");
        QFileInfo fileinfo(myfile->fileName());
        qInfo()<<"size: "<<fileinfo.size();
        qInfo()<<"name: "<<fileinfo.fileName();
        qInfo()<<"uploading : "<<location;
        //qInfo()<<"file name is:"<<myfile->fileName();
        //ftp.put(myfile,fileinfo.fileName());
        QString text="STOR "+fileinfo.fileName();
        qInfo()<<text;
        ftp.rawCommand(text);
        //client.rawCommand("PASV");

        //client.rawCommand("TYPE I");
        //client.rawCommand("STAT");
        //client.rawCommand("STOR test.txt");
    }


}

void Client::list(QString &dir)
{
    qInfo()<<"listing dir:"<<dir;
    ftp.list(dir);
}

void Client::dataChanel()
{
    for(int i=0;i<IpPorts.size();i++){
        qInfo()<<IpPorts[i];
    }

    QTcpSocket* socket=new QTcpSocket(this);
    socket->connectToHost(IpPorts[0],IpPorts[1].toInt());
    QByteArray data;
    data=myfile->readAll();
    socket->waitForConnected();

    socket->write(data);
}

错误原因分析

  • 端口解析错误:227回复中的端口由两个十进制数组成,需通过(p1 << 8) + p2计算出实际16位端口,你直接使用第二个数作为端口,导致连接到无效端口。
  • 命令顺序混乱:QFtp的rawCommand是异步执行的,你未等待前一个命令完成就发送下一个,服务器可能无法按预期响应。
  • 数据发送逻辑缺陷:无超时的waitForConnected()可能无限阻塞;一次性读取大文件到内存易引发内存问题;未确保数据完全发送就结束,服务器未接收完数据会触发超时。
  • 套接字管理不当:发送数据后未等待服务器的226完成回复,也未正确关闭套接字,导致连接异常终止。

正确实现步骤

  1. 正确解析PASV端口:将227回复的后两个数字组合成有效端口。
  2. 同步命令执行:通过commandFinished信号确保前一个raw命令完成后,再发送下一个命令。
  3. 优化数据传输:分块读取文件并发送,设置连接和写入超时,确保数据完整传输。
  4. 断点续传实现:上传前发送REST <offset>命令,告知服务器从指定位置开始接收,同时移动本地文件指针到断点处。

修改后的关键代码示例

1. 修正PASV端口解析

void Client::rawCommandReply(int code, QString detail)
{
    qInfo()<<"reply code: "<<code<<"detail: "<<detail;
    if (code==226){
        qInfo()<<"transmission ended";
        // 传输完成后清理文件
        if(myfile){
            myfile->flush();
            myfile->close();
            myfile->deleteLater();
            myfile=NULL;
        }
    }
    if (code==150){
        qInfo()<<"serverReady";
        dataChanel();
    }
    if(code==227){
        QString ip1=detail.split("(")[1];
        QString ip2=ip1.split(")")[0];
        QStringList data=ip2.split(",");
        // 解析IP
        QString ip = QString("%1.%2.%3.%4").arg(data[0]).arg(data[1]).arg(data[2]).arg(data[3]);
        // 计算实际端口
        int port = (data[4].toInt() << 8) + data[5].toInt();
        IpPorts.clear();
        IpPorts.append(ip);
        IpPorts.append(QString::number(port));
        qInfo() << "Parsed PASV IP:" << ip << ", Port:" << port;
    }
    // 处理SIZE命令的回复,用于断点续传
    if(code==213){
        qint64 serverFileSize = detail.toLongLong();
        qInfo() << "Server file size:" << ConvertSize(serverFileSize);
        // 调用upload方法,传入断点偏移量
        upload(currentUploadFile, serverFileSize);
    }
}

2. 优化数据通道发送逻辑

void Client::dataChanel()
{
    if(IpPorts.size() < 2) return;

    QTcpSocket* socket=new QTcpSocket(this);
    connect(socket, &QTcpSocket::errorOccurred, this, [=](QAbstractSocket::SocketError err){
        qInfo() << "Data socket error:" << socket->errorString();
        socket->deleteLater();
    });

    // 设置3秒连接超时
    socket->connectToHost(IpPorts[0], IpPorts[1].toInt());
    if(!socket->waitForConnected(3000)){
        qInfo() << "Failed to connect to data channel:" << socket->errorString();
        socket->deleteLater();
        return;
    }

    qint64 bytesSent = 0;
    const qint64 chunkSize = 1024 * 1024; // 1MB分块发送
    QByteArray chunk;
    while(!myfile->atEnd()){
        chunk = myfile->read(chunkSize);
        qint64 sent = socket->write(chunk);
        if(sent == -1){
            qInfo() << "Failed to write data:" << socket->errorString();
            break;
        }
        bytesSent += sent;
        // 等待数据写入套接字,设置1秒超时
        socket->waitForBytesWritten(1000);
        qInfo() << "Sent" << ConvertSize(bytesSent);
    }

    // 等待服务器关闭连接,设置5秒超时
    socket->waitForDisconnected(5000);
    socket->close();
    socket->deleteLater();
    qInfo() << "Data transfer completed";
}

3. 同步命令执行与断点续传实现

// 新增成员变量:QString currentUploadFile;

void Client::upload(QString &location, qint64 resumeOffset)
{
    currentUploadFile = location;
    myfile=new QFile(location);
    if(myfile->open(QIODevice::ReadOnly)){
        if(resumeOffset > 0){
            // 发送REST命令,指定断点偏移量
            ftp.rawCommand(QString("REST %1").arg(resumeOffset));
            // 移动文件指针到断点位置
            myfile->seek(resumeOffset);
        } else {
            // 首次上传,先发送PASV
            ftp.rawCommand("PASV");
        }
    }
}

void Client::commandFinished(int id)
{
    qInfo()<<"command finished: "<<id;
    if(ftp.error()){
        qInfo()<<ftp.errorString();
        return;
    }

    // 根据完成的命令,发送下一个指令
    if(ftp.currentCommand() == 14){ // RawCommand
        QString lastCmd = ftp.rawCommand(id);
        if(lastCmd.startsWith("PASV")){
            ftp.rawCommand("TYPE I");
        } else if(lastCmd.startsWith("TYPE I")){
            QFileInfo fileinfo(currentUploadFile);
            ftp.rawCommand("STOR " + fileinfo.fileName());
        } else if(lastCmd.startsWith("REST")){
            ftp.rawCommand("PASV");
        } else if(lastCmd.startsWith("SIZE")){
            // SIZE命令完成后会在rawCommandReply中处理断点续传
        }
    }
}

// 调用上传前,先查询服务器文件大小(用于断点续传)
void Client::startUpload(QString &location)
{
    QFileInfo fileinfo(location);
    ftp.rawCommand(QString("SIZE %1").arg(fileinfo.fileName()));
}

内容的提问来源于stack exchange,提问作者Aligh2022

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 04:15:40