QFtpPI类是对FTP协议的封装,所有的客户端命令、服务器的响应处理都在此类中执行。
这篇文章将挑选几个比较重要的函数进行说明。
1. void QFtpPI::connectToHost(const QString &host, quint16 port)
连接主机的操作。
void QFtpPI::connectToHost(const QString &host, quint16 port)
{
emit connectState(QFtp::HostLookup);
#ifndef QT_NO_BEARERMANAGEMENT
//copy network session down to the socket & DTP
commandSocket.setProperty("_q_networksession", property("_q_networksession"));
dtp.setProperty("_q_networksession", property("_q_networksession"));
#endif
commandSocket.connectToHost(host, port);
}
2. bool QFtpPI::sendCommands(const QStringList &cmds)
通过QFtpPrivate类调用sendCommands函数压入到pendingCommands容器中,之后会立即调用一次startNextCmd函数,进行第一条命令的发送。
/*
将命令序列\a cmd发送到FTP服务器。当命令完成后,finished()信号就会发出。
发生错误时发出Error()信号。
如果队列中有待处理的命令,则此函数返回false和CMDS未添加到队列中;否则返回true。
*/
bool QFtpPI::sendCommands(const QStringList &cmds)
{
if (!pendingCommands.isEmpty())
return false;
if (commandSocket.state() != QTcpSocket::ConnectedState || state != Idle) {
emit error(QFtp::NotConnected, QFtp::tr("Not connected"));
return true; // there are no pending commands
}
pendingCommands = cmds;
startNextCmd();
return true;
}
3. bool QFtpPI::startNextCmd()
- 从pendingCommands中取出下一条FTP命令,若堆栈中已经不存在待处理的命令,将发射finished信号。
- 区分PORT和PASV,这取决于是否应该尝试扩展传输连接命令EPRT和EPSV。
- 将State设置为Idle。
- 调用write函数发送命令数据。
bool QFtpPI::startNextCmd()
{
if (waitForDtpToConnect)
// don't process any new commands until we are connected
return true;
#if defined(QFTPPI_DEBUG)
if (state != Idle)
qDebug("QFtpPI startNextCmd: Internal error! QFtpPI called in non-Idle state %d", state);
#endif
if (pendingCommands.isEmpty()) {
currentCmd.clear();
emit finished(replyText);
return false;
}
currentCmd = pendingCommands.first();
// PORT and PASV are edited in-place, depending on whether we
// should try the extended transfer connection commands EPRT and
// EPSV. The PORT command also triggers setting up a listener, and
// the address/port arguments are edited in.
QHostAddress address = commandSocket.localAddress();
if (currentCmd.startsWith(QLatin1String("PORT"))) {
if ((address.protocol() == QTcpSocket::IPv6Protocol) && transferConnectionExtended) {
int port = dtp.setupListener(address);
currentCmd = QLatin1String("EPRT |");
currentCmd += (address.protocol() == QTcpSocket::IPv4Protocol) ? QLatin1Char('1') : QLatin1Char('2');
currentCmd += QLatin1Char('|') + address.toString() + QLatin1Char('|') + QString::number(port);
currentCmd += QLatin1Char('|');
} else if (address.protocol() == QTcpSocket::IPv4Protocol) {
int port = dtp.setupListener(address);
QString portArg;
quint32 ip = address.toIPv4Address();
portArg += QString::number((ip & 0xff000000) >> 24);
portArg += QLatin1Char(',') + QString::number((ip & 0xff0000) >> 16);
portArg += QLatin1Char(',') + QString::number((ip & 0xff00) >> 8);
portArg += QLatin1Char(',') + QString::number(ip & 0xff);
portArg += QLatin1Char(',') + QString::number((port & 0xff00) >> 8);
portArg += QLatin1Char(',') + QString::number(port & 0xff);
currentCmd = QLatin1String("PORT ");
currentCmd += portArg;
} else {
// No IPv6 connection can be set up with the PORT
// command.
return false;
}
currentCmd += QLatin1String("\r\n");
} else if (currentCmd.startsWith(QLatin1String("PASV"))) {
if ((address.protocol() == QTcpSocket::IPv6Protocol) && transferConnectionExtended)
currentCmd = QLatin1String("EPSV\r\n");
}
pendingCommands.pop_front();
#if defined(QFTPPI_DEBUG)
qDebug("QFtpPI send: %s", currentCmd.left(currentCmd.length() - 2).toLatin1().constData());
#endif
state = Waiting;
commandSocket.write(currentCmd.toLatin1());
qDebug() << "qftp currentCmd : " << currentCmd.toLatin1();
return true;
}
4. bool QFtpPI::processReply()
这个函数用于处理来自FTP服务器的应答,主要是针对响应码的处理。如果应答已被处理,则返回true;如果应答必须被处理,则返回false稍后再处理。
以下整理了一些常见的FTP响应码信息:
1xx - 信息响应:这些响应码表示一些信息性的消息,通常用于指示某个操作正在进行中或将要发生。
响应码 | 信息 |
---|---|
100 | 服务器准备就绪,等待客户端继续请求 |
110 | 重新启动标记应答 |
120 | 服务不久即将就绪 |
125 | 数据连接已打开,正在开始传输数据 |
150 | 打开数据连接,准备开始传输 |
2xx - 成功响应:这些响应码表示操作成功完成。
响应码 | 信息 |
---|---|
200 | 命令成功 |
202 | 命令失败 |
211 | 系统状态 |
212 | 目录状态目录状态 |
213 | 文件状态 |
214 | 帮助信息 |
215 | 名字系统类型 |
220 | 服务就绪,可以执行新用户的请求 |
221 | 服务关闭控制连接,可以退出登录 |
225 | 数据连接打开,无传输正在进行 |
226 | 关闭数据连接,请求的文件操作成功 |
227 | 进入被动模式 |
230 | 用户登录 |
250 | 请求的文件操作完成 |
257 | 创建”PATHNAME” |
3xx - 重定向响应:这些响应码表示需要进一步的操作以完成请求。
响应码 | 信息 |
---|---|
331 | 用户名正确,需要口令 |
332 | 登录时需要帐户信息 |
350 | 下一步命令 |
4xx - 临时错误响应:这些响应码表示客户端的请求包含错误,但可能在稍后的请求中得到解决。
响应码 | 信息 |
---|---|
421 | 不能提供服务,关闭控制连接 |
425 | 不能打开数据连接 |
426 | 关闭连接,中止传输 |
450 | 请求的文件操作未执行 |
451 | 中止请求的操作:有本地错误 |
452 | 未执行请求的操作:系统存储空间不足 |
5xx - 永久错误响应:这些响应码表示客户端的请求包含错误,且错误是永久性的,无法在当前情况下解决。
响应码 | 信息 |
---|---|
500 | 格式错误,命令不可识别 |
501 | 参数语法错误 |
502 | 命令未实现 |
503 | 命令顺序错误 |
504 | 此参数下的命令功能未实现 |
530 | 未登录 |
551 | 请求操作中止:页类型未知 |
552 | 请求的文件操作中止,存储分配溢出 |
bool QFtpPI::processReply()
{
#if defined(QFTPPI_DEBUG)
// qDebug("QFtpPI state: %d [processReply() begin]", state);
if (replyText.length() < 400)
qDebug("QFtpPI recv: %d %s", 100 * replyCode[0] + 10 * replyCode[1] + replyCode[2], replyText.toLatin1().constData());
else
qDebug("QFtpPI recv: %d (text skipped)", 100 * replyCode[0] + 10 * replyCode[1] + replyCode[2]);
#endif
// 响应码组合
int replyCodeInt = 100 * replyCode[0] + 10 * replyCode[1] + replyCode[2];
// 进程226回复(“关闭数据连接”)只有当数据连接是真正关闭的,以避免短读DTP
if (replyCodeInt == 226 || (replyCodeInt == 250 && currentCmd.startsWith(QLatin1String("RETR")))) {
if (dtp.state() != QTcpSocket::UnconnectedState) {
waitForDtpToClose = true;
return false;
}
}
switch (abortState) {
case AbortStarted:
abortState = WaitForAbortToFinish;
break;
case WaitForAbortToFinish:
abortState = None;
return true;
default:
break;
}
// get new state
static const State table[5] = {
/* 1yz 2yz 3yz 4yz 5yz */
Waiting, Success, Idle, Failure, Failure
};
switch (state) {
case Begin:
if (replyCode[0] == 1) {
return true;
} else if (replyCode[0] == 2) {
state = Idle;
emit finished(QFtp::tr("Connected to host %1").arg(commandSocket.peerName()));
break;
}
// reply codes not starting with 1 or 2 are not handled.
return true;
case Waiting:
if (static_cast<signed char>(replyCode[0]) < 0 || replyCode[0] > 5)
state = Failure;
else
#if defined(Q_OS_IRIX) && defined(Q_CC_GNU)
{
// work around a crash on 64 bit gcc IRIX
State *t = (State *) table;
state = t[replyCode[0] - 1];
}
#else
if (replyCodeInt == 202)
state = Failure;
else
state = table[replyCode[0] - 1];
#endif
break;
default:
// ignore unrequested message
return true;
}
#if defined(QFTPPI_DEBUG)
// qDebug("QFtpPI state: %d [processReply() intermediate]", state);
#endif
// special actions on certain replies
emit rawFtpReply(replyCodeInt, replyText);
if (rawCommand) {
rawCommand = false;
} else if (replyCodeInt == 227) {
// 227进入被动模式(h1,h2,h3,h4,p1,p2)
// rfc959没有精确地定义这个响应,并给出
// 两个使用括号的例子
// 它们丢失了。我们需要扫描地址和主机
// 信息。
QRegExp addrPortPattern(QLatin1String("(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+)"));
if (addrPortPattern.indexIn(replyText) == -1) {
#if defined(QFTPPI_DEBUG)
qDebug("QFtp: bad 227 response -- address and port information missing");
#endif
// this error should be reported
} else {
QStringList lst = addrPortPattern.capturedTexts();
QString host = lst[1] + QLatin1Char('.') + lst[2] + QLatin1Char('.') + lst[3] + QLatin1Char('.') + lst[4];
quint16 port = (lst[5].toUInt() << 8) + lst[6].toUInt();
waitForDtpToConnect = true;
dtp.connectToHost(host, port);
}
} else if (replyCodeInt == 229) {
// 229 Extended Passive mode OK (|||10982|)
int portPos = replyText.indexOf(QLatin1Char('('));
if (portPos == -1) {
#if defined(QFTPPI_DEBUG)
qDebug("QFtp: bad 229 response -- port information missing");
#endif
// this error should be reported
} else {
++portPos;
QChar delimiter = replyText.at(portPos);
QStringList epsvParameters = replyText.mid(portPos).split(delimiter);
waitForDtpToConnect = true;
dtp.connectToHost(commandSocket.peerAddress().toString(),
epsvParameters.at(3).toInt());
}
} else if (replyCodeInt == 230) {
if (currentCmd.startsWith(QLatin1String("USER ")) && pendingCommands.count() > 0 &&
pendingCommands.first().startsWith(QLatin1String("PASS "))) {
// no need to send the PASS -- we are already logged in
pendingCommands.pop_front();
}
// 230 User logged in, proceed.
emit connectState(QFtp::LoggedIn);
} else if (replyCodeInt == 213) {
// 213 File status.
if (currentCmd.startsWith(QLatin1String("SIZE ")))
dtp.setBytesTotal(replyText.simplified().toLongLong());
} else if (replyCode[0] == 1 && currentCmd.startsWith(QLatin1String("STOR "))) {
dtp.waitForConnection();
dtp.writeData();
}
// react on new state
switch (state) {
case Begin:
// should never happen
break;
case Success:
// success handling
state = Idle;
// no break!
case Idle:
if (dtp.hasError()) {
emit error(QFtp::UnknownError, dtp.errorMessage());
dtp.clearError();
}
startNextCmd();
break;
case Waiting:
// do nothing
break;
case Failure:
// If the EPSV or EPRT commands fail, replace them with
// the old PASV and PORT instead and try again.
if (currentCmd.startsWith(QLatin1String("EPSV"))) {
transferConnectionExtended = false;
pendingCommands.prepend(QLatin1String("PASV\r\n"));
} else if (currentCmd.startsWith(QLatin1String("EPRT"))) {
transferConnectionExtended = false;
pendingCommands.prepend(QLatin1String("PORT\r\n"));
} else {
emit error(QFtp::UnknownError, replyText);
}
if (state != Waiting) {
state = Idle;
startNextCmd();
}
break;
}
#if defined(QFTPPI_DEBUG)
// qDebug("QFtpPI state: %d [processReply() end]", state);
#endif
return true;
}