SCXML FTP 客户端

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

FTP 客户端使用Qt SCXML 来实现一个 FTP 客户端,它可以通过发送由状态机事件转换而来的 FTP 控制信息,以及将服务器回复转换为状态机事件,与 FTP 服务进行通信。从 FTP 服务器接收到的数据将打印在控制台上。

RFC 959规定了 FTP 客户端命令处理的状态图。它们可以很容易地转化为 SCXML,从而受益于 SCXML 嵌套状态。客户端和服务器之间的连接以及数据传输是通过 C++ 实现的。此外,还使用了 Qt 信号和插槽。

状态机有以下状态

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

状态机在simpleftp.scxml文件中指定,并编译到实现 FTP 协议逻辑的FtpClient 类中。它通过改变状态和发送外部事件对用户输入和来自控制通道的回复做出反应。此外,我们还实现了一个FtpControlChannel 类和一个FtpDataChannel 类,用于处理 TCP 套接字和服务器以及转换行尾。

运行示例

运行示例 Qt Creator,打开Welcome 模式,并从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建和运行

编译状态机

我们在项目构建文件中添加以下一行来链接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.hsimpleftp.cpp,并将它们作为头文件和源文件添加到项目中。

实例化状态机

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

#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

© 2025 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.