使用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完成回复,也未正确关闭套接字,导致连接异常终止。
正确实现步骤
- 正确解析PASV端口:将227回复的后两个数字组合成有效端口。
- 同步命令执行:通过
commandFinished信号确保前一个raw命令完成后,再发送下一个命令。 - 优化数据传输:分块读取文件并发送,设置连接和写入超时,确保数据完整传输。
- 断点续传实现:上传前发送
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
相关产品推荐
相关产品推荐

