Глава 13. Работа с сетью.

Для работы с протоколами FTP и HTTP в библиотеке Qt имеются классы QFtp и QHttp. Они достаточно удобны для организации обмена файлами по сети.

Классы QFtp и QHttp основаны на низкоуровневом классе QSocket, который реализует представление сокетов TCP. Протокол TCP работает в терминах потоков данных, передаваемых между узлами сети. Класс QSocket, в свою очередь, реализован поверх QSocketDevice -- тонкой "обертки" вокруг платформо-зависимого сетевого API операционной системы. Класс QSocketDevice поддерживает протоколы TCP и UDP.

В этой главе мы будем говорить об этих 4-х, и некоторых других классах, и покажем -- как с ними работать. Расскажем, как организовать обмен файлами по сети. Протокол TCP будет использоваться нами при написании приложений-серверов и соответствующих им приложений-клиентов. Аналогично, протокол UDP будет использоваться для написания передающей и принимающей части приложений. Понимание принципов работы классов QFtp и QHttp обычно ни у кого не вызывает затруднений, даже у новичков, однако, для понимания принципов работы с классами QSocket и QSocketDevice, желательно иметь некоторый опыт работы с сетями.

13.1. Класс QFtp.

Класс QFtp предназначен для создания клиентских приложений, работающих с протоколом FTP. Он реализует набор функций, для выполнения наиболее распространенных операций этого протокола, включая get(), put(), remove() и mkdir().

Все операции выполняются асинхронно. Когда вызывается функция, такая как get() или put(), управление сразу же возвращается программе, а собственно передача данных начинает производиться, когда управление опять переходит в цикл обработки событий Qt. Благодаря этому, во время исполнения FTP-команд, не возникает эффекта "замораживания" интерфейса с пользователем.

Демонстрацию возсможностей QFtp начнем с показа того, как скачать файл с сервера, используя функцию get(). Предположим, что основной класс приложения MainWindow должен скачать прейскурант с FTP-сайта.

class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0, const char *name = 0); void getPriceList(); ... private slots: void ftpDone(bool error); private: QFtp ftp; QFile file; ... }; Класс определяет публичную функцию getPriceList(), которая отвечает за получение файла с прейскурантом, и приватный слот ftpDone(bool), который вызывается по окончании приема файла. Так же в классе определены две переменные: ftp, ответственную за взаимодействие с FTP-сервером, и file, используемую для записи файла на диск. MainWindow::MainWindow(QWidget *parent, const char *name) : QMainWindow(parent, name) { ... connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); } В конструкторе выполняется соединение между сигналом done(bool), экземпляра класса QFtp, и слотом ftpDone(bool). Объекты класса QFtp выдают этот сигнал по завершении обработки всех запросов. Параметр bool указывает на наличие возможных ошибок. void MainWindow::getPriceList() { file.setName("price-list.csv"); if (!file.open(IO_WriteOnly)) { QMessageBox::warning(this, tr("Sales Pro"), tr("Cannot write file %1\n%2.") .arg(file.name()) .arg(file.errorString())); return; } ftp.connectToHost("ftp.trolltech.com"); ftp.login(); ftp.cd("/topsecret/csv"); ftp.get("price-list.csv", &file); ftp.close(); } Функция getPriceList() загружает файл ftp://ftp.trolltech.com/topsecret/csv/price-list.csv и сохраняет его под именем price-list.csv в текущем каталоге.

Начинается функция с попытки открыть на запись файл в текущем каталоге. Затем выполняется последовательность из пяти FTP-команд. Второй аргумент функции get() задает устройство, в которое будет осуществляться запись принимаемых данных.

Команды FTP ставятся в очередь и исполняются в цикле обработки событий Qt. По завершении обработки всех команд, объект QFtp выдает сигнал done(bool), который подключен к слоту ftpDone(bool).

void MainWindow::ftpDone(bool error) { if (error) QMessageBox::warning(this, tr("Sales Pro"), tr("Error while retrieving file with " "FTP: %1.") .arg(ftp.errorString())); file.close(); } После выполнения FTP-команд, файл закрывается. Если возникла какая либо ошибка, перед пользователем выводится соответствующее сообщение.

Класс QFtp предоставляет следующие операции: connectToHost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir() и rename(). Все эти функции ставят соответствующие команды в очередь и возвращают идентификационный номер команды. Любые команды FTP могут быть испольнены с помощью функции rawCommand(). Например, так выглядит исполнение команды SITE CHMOD:

ftp.rawCommand("SITE CHMOD 755 fortune"); Объекты QFtp, перед исполнением команды, выдают сигнал commandStarted(int), а по завершении -- commandFinished(int, bool). Аргумент int -- это идентификационный номер команды. Если вас интересует ход выполнения отдельных команд, то вам придется сохранять их идентификационные номера, при вызове соответствующей функции. Благодаря этому появится возможность предоставить пользователю более детальную информацию о ходе процесса. Например: void MainWindow::getPriceList() { ... connectId = ftp.connectToHost("ftp.trolltech.com"); loginId = ftp.login(); cdId = ftp.cd("/topsecret/csv"); getId = ftp.get("price-list.csv", &file); closeId = ftp.close(); } void MainWindow::commandStarted(int id) { if (id == connectId) { statusBar()->message(tr("Connecting...")); } else if (id == loginId) { statusBar()->message(tr("Logging in...")); ... } Другой способ обеспечения пользователя обратной связью с процессом -- использовать сигнал stateChanged().

Однако, в большинстве приложений нас интересует только результат выполнения всей последовательности команд. В этом случае мы просто соединяемся с сигналом done(bool), который выдается послк выполнения последней команды в последовательности.

При возникновении ошибки, QFtp автоматически очищает очередь команд. Это означает, что если ошибка произошла во время установления соединения или во время авторизации на сервере, следующие за ними команды никогда не будут выполнены. Но если после возникновения ошибки в очередь будут поставлены другие команды, то они будут исполнены как ни в чем не бывало.

Теперь рассмотрим более сложный пример:

class Downloader : public QObject { Q_OBJECT public: Downloader(const QUrl &url); signals: void finished(); private slots: void ftpDone(bool error); void listInfo(const QUrlInfo &urlInfo); private: QFtp ftp; std::vector<QFile *> openedFiles; }; Экземпляр класса Downloader попытается скачать все файлы из каталога FTP. Имя каталога задается как QUrl, при вызове конструктора. Класс QUrl -- это стандартный класс из библиотеки Qt, который реализует интерфейс для работы со строками URL и выделения их них отдельных частей, таких как имя файла, путь к файлу, протокол и порт. Downloader::Downloader(const QUrl &url) { if (url.protocol() != "ftp") { QMessageBox::warning(0, tr("Downloader"), tr("Protocol must be ftp .")); emit finished(); return; } int port = 21; if (url.hasPort()) port = url.port(); connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(listInfo(const QUrlInfo &))); ftp.connectToHost(url.host(), port); ftp.login(url.user(), url.password()); ftp.cd(url.path()); ftp.list(); } В конструкторе прежде всего выполняется проверка строки URL -- она должна начинаться с комбинации символов: "ftp:". Затем из URL извлекается номер порта, если порт не указан, то предполагается использование стандартного FTP-порта -- 21.

Затем выполняются соединения сигнал-слот и в очередь помещаются 4 FTP-команды. Последняя из них запрашивает у сервера список файлов и выдает сигнал listInfo(const QUrlInfo &), когда от сервера приходит очередное имя файла. Этот сигнал связан со слотом listInfo(), отвечающим за скачивание файла.

void Downloader::listInfo(const QUrlInfo &urlInfo) { if (urlInfo.isFile() && urlInfo.isReadable()) { QFile *file = new QFile(urlInfo.name()); if (!file->open(IO_WriteOnly)) { QMessageBox::warning(0, tr("Downloader"), tr("Error: Cannot open file " "%1:\n%2.") .arg(file->name()) .arg(file->errorString())); emit finished(); return; } ftp.get(urlInfo.name(), file); openedFiles.push_back(file); } } Аргумент типа QUrlInfo предоставляет подробную информацию о файле. Если это обычный файл (не каталог) и доступен на чтение, то производится попытка скачать его, вызовом get(). Объект QFile используется для сохранения локальной копии файла, он создается оператором new, а указатель на него сохраняется в динамическом массиве (векторе) openedFiles. void Downloader::ftpDone(bool error) { if (error) QMessageBox::warning(0, tr("Downloader"), tr("Error: %1.") .arg(ftp.errorString())); for (int i = 0; i < (int)openedFiles.size(); ++i) delete openedFiles[i]; emit finished(); } Слот ftpDone() вызывается по завершении выполнения последовательности команд или в случае возникновения ошибки. Функция удаляет все объекты QFile, попутно закрывая все файлы. (Файлы закрываются автоматически деструктором класса QFile.)

Если ошибок не возникло, то порядок выполнения команд и выдачи сигналов будет следующим:

connectToHost(host) login() cd(path) list() emit listInfo(file_1) get(file_1) emit listInfo(file_2) get(file_2) ... emit listInfo(file_N) get(file_N) emit done() Если ошибка возникла, например, во время скачивания пятого файла из двадцати имевшихся, то оставшиеся пятнадуать файлов не будут скачиваться. Если вас это не устраивает, то можно попробовать выполнять скачивание файлов по одному -- запускать команду GET, ждать появления сигнала done(bool) и только после этого запускать GET для очередного файла. А в функции listInfo() -- просто создавать список имен файлов в каталоге сервера. В этом случае порядок выполнения команд и выдачи сигналов будет следующим: connectToHost(host) login() cd(path) list() emit listInfo(file_1) emit listInfo(file_2) ... emit listInfo(file_N) emit done() get(file_1) emit done() get(file_2) emit done() ... get(file_N) emit done() Другой вариант решения проблемы состоит в использовании отдельного объекта QFtp для каждого из файлов. Это позволит выполнять параллельную загрузку нескольких файлов через различные FTP-соединения. int main(int argc, char *argv[]) { QApplication app(argc, argv); QUrl url("ftp://ftp.example.com/"); if (argc >= 2) url = argv[1]; Downloader downloader(url); QObject::connect(&downloader, SIGNAL(finished()), &app, SLOT(quit())); return app.exec(); } Реализацией функции main() мы завершаем рассмотрение программы. Если пользователь указывает URL в командной строке, то файлы скачиваются из указанного каталога, в противном случае -- из каталога ftp://ftp.example.com/.

В обоих вышеприведенных примерах, файл скачивается с помощью функции get() и записывается на диск посредством объекта QFile. В случае же, если принятый файл нужно сохранить в памяти, то для этого прекрасно подойдет класс QBuffer, производный от класса QIODevice -- обертки вокруг класса QByteArray. Например:

QBuffer *buffer = new QBuffer(byteArray); buffer->open(IO_WriteOnly); ftp.get(urlInfo.name(), buffer); В функции get() мы могли бы опустить второй аргумент или передать в место него "пустой" (NULL) указатель. В этом случае QFtp будет выдавать сигнал readyRead() всякий раз, при поступлении очередной порции данных, которые могут быть прочитаны вызовом readBlock() или readAll().

Если необходимо отображать ход выполнения скачивания файла, то можно связать сигнал dataTransferProgress(int, int), класса QFtp, со слотом setProgress(int, int) класса QProgressBar или QProgressDialog. Кроме того, можно привязать сигнал canceled(), класса QProgressBar или QProgressDialog со слотом abort(), класса QFtp.