本页内容

SCXML FTP 客户端

使用状态机实现了一个简单的 FTP 客户端。

该FTP客户端利用Qt SCXML 实现,能够通过发送由状态机事件转换而来的FTP控制消息,并将服务器响应转换为状态机事件,从而与FTP服务进行通信。从FTP服务器接收到的数据将输出到控制台。

RFC 959规定了 FTP 客户端命令处理的状态图。这些状态图可以轻松转换为 SCXML,从而利用 SCXML 的嵌套状态功能。客户端与服务器之间的连接以及数据传输均通过 C++ 实现。此外,还使用了 Qt 的信号与槽机制。

该状态机具有以下状态:

包含 I、S、F、P 状态以及用于登录和传输工作流的 B、W 子系统的 FTP 客户端状态图

  • I作为初始状态。
  • B表示发送命令。
  • S表示成功。
  • F表示失败。
  • W表示等待回复。
  • P表示应服务器请求提供密码。

状态机在simpleftp.scxml文件中定义,并编译为实现 FTP 协议逻辑的FtpClient 类。该类通过切换状态和发送外部事件,来响应用户输入以及控制信道发来的应答。此外,我们还实现了FtpControlChannel 类和FtpDataChannel 类,用于处理 TCP 套接字和服务器,并转换行结束符。

运行示例

您可以从以下位置运行示例:

编译状态机

通过在项目构建文件中添加以下行,将项目与Qt SCXML 模块进行链接。

使用 qmake 时,我们在ftpclient.pro中添加以下内容

QT = core scxml network

然后,我们指定要编译的状态机:

STATECHARTS += simpleftp.scxml

使用 CMake 时,我们在CMakeLists.txt中添加以下内容

find_package(Qt6 REQUIRED COMPONENTS Core Network Scxml)

target_link_libraries(ftpclient PRIVATE
    Qt6::Core
    Qt6::Network
    Qt6::Scxml
)

然后,我们指定要编译的状态机:

qt6_add_statecharts(ftpclient
    simpleftp.scxml
)

Qt SCXML 编译器(qscxmlc )会自动运行,生成simpleftp.h和 simpleftp.cpp文件,并将其作为头文件和源文件添加到项目中。

实例化状态机

我们在main.cpp文件中实例化生成的FtpClient 类,以及FtpDataChannel 和FtpControlChannel 类:

#include "ftpcontrolchannel.h"
#include "ftpdatachannel.h"
#include "simpleftp.h"
    ...
int main(int argc, char *argv[])
{
    ...
    QCoreApplication app(argc, argv);
    FtpClient ftpClient;
    FtpDataChannel dataChannel;
    FtpControlChannel controlChannel;
    ...

与FTP服务器通信

我们将从服务器检索到的所有数据输出到控制台:

    QObject::connect(&dataChannel, &FtpDataChannel::dataReceived,
                     [](const QByteArray &data) {
        std::cout << data.constData() << std::flush;
    });

我们将服务器的响应转换为状态机事件:

    QObject::connect(&controlChannel, &FtpControlChannel::reply, &ftpClient,
                     [&ftpClient](int code, const QString &parameters) {
        ftpClient.submitEvent(QString("reply.%1xx")
                              .arg(code / 100), parameters);
    });

我们将状态机发出的命令转换为FTP控制消息:

    ftpClient.connectToEvent("submit.cmd", &controlChannel,
                             [&controlChannel](const QScxmlEvent &event) {
        controlChannel.command(event.name().mid(11).toUtf8(),
                               event.data().toMap()["params"].toByteArray());
    });

我们发送命令,以匿名用户身份登录FTP服务器,声明数据连接的端口,并检索文件:

    QList<Command> commands({
        {"cmd.USER", "anonymous"},// login
        {"cmd.PORT", ""},         // announce port for data connection,
                                  // args added below.
        {"cmd.RETR", file}        // retrieve a file
    });

我们指定FTP客户端在进入B状态时应发送下一个命令:

    ftpClient.connectToState("B",QScxmlStateMachine::onEntry([&]() {
        if(commands.isEmpty()) {
            app.quit();
            return;
        }
        Command command=commands.takeFirst();
        qDebug() << "Posting command" << command.cmd << command.args;
        ftpClient.submitEvent(command.cmd,command.args);
    }));

我们指定,如果服务器要求输入密码,FTP 客户端应发送一个空字符串作为密码:

    ftpClient.connectToState("P",QScxmlStateMachine::onEntry([&ftpClient]() {
        qDebug() << "Sending password";
        ftpClient.submitEvent("cmd.PASS",QString());
    }));

最后,我们连接到该方法第一个参数指定的FTP服务器,并获取第二个参数指定的文件:

    controlChannel.connectToServer(server);
    QObject::connect(&controlChannel, &FtpControlChannel::opened, &dataChannel,
                     [&](const QHostAddress &address, int) {
        dataChannel.listen(address);
        commands[1].args = dataChannel.portspec();
        ftpClient.start();
    });

例如,以下调用将从指定服务器打印指定文件:ftpclient <server> <file> 。

示例项目 @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.