О ресурре... Главная
The Belarus Internet the Resource about Linux
 
   
Ж. Бланшетт, М. Саммерфильд, "Глава 13. Работа с сетью" - 01/12/2004

Предыдущая глава | Содержание | Следующая глава

Глава 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.


13.2. Класс QHttp.

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

Как и QFtp, объекты класса QHttp работают асинхронно. Функции get() и post() сразу же возвращают управление вызывающей программе, а собственно передача данных осуществляется в цикле обработки событий.

Рассмотрим принцип действия QHttp на примере приложения, которое пытается получить HTML-файл с сайта Trolltech. Мы не будем здесь приводить содержимое заголовочного файла, т.к. он очень похож на тот, который использовался в предыдущем разделе. Различие состоит лишь в том, что теперь приватный слот объявлен под именем httpDone(bool) и изменено объявление одной приватной переменной (http типа QHttp).

MainWindow::MainWindow(QWidget *parent, const char *name) 
    : QMainWindow(parent, name) 
{ 
  ...
  connect(&http, SIGNAL(done(bool)), this, SLOT(httpDone(bool))); 
}
      
В конструкторе выполняется соединение сигнала done(bool), объекта QHttp, со слотом главного окна -- httpDone(bool).
void MainWindow::getFile() 
{ 
  file.setName("aboutqt.html"); 
  if (!file.open(IO_WriteOnly)) { 
    QMessageBox::warning(this, tr("HTTP Get"), 
                         tr("Cannot write file %1\n%2.") 
                         .arg(file.name()) 
                         .arg(file.errorString())); 
    return; 
  } 
  
  http.setHost("doc.trolltech.com"); 
  http.get("/3.2/aboutqt.html", &file); 
  http.closeConnection(); 
}
      
Функция getFile() загружает файл http://doc.trolltech.com/3.2/aboutqt.html и сохраняет его в текущем каталоге, под именем aboutqt.html.

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

HTTP-запросы исполняются в цикле обработки событий. По завершении заданной последовательности команд, объект Qhttp выдает сигнал done(bool), который поступает в слот httpDone(bool).

void MainWindow::httpDone(bool error) 
{ 
  if (error) 
    QMessageBox::warning(this, tr("HTTP Get"), 
                         tr("Error while fetching file with " 
                           "HTTP: %1.") 
                         .arg(http.errorString())); 
  file.close(); 
}
      
После того, как запрос будет выполнен, файл закрывается. При возникновении ошибки перед пользователем выводится соответствующее сообщение.

Среди всего прочего, QHttp предоставляет в распоряжение программиста следующие функции: setHost(), get(), post() и head(). Ниже приводится пример передачи списка пар "имя = значение" в CGI-скрипт:

    http.setHost("www.example.com"); 
    http.post("/cgi/somescript.py", QCString("x=200&y=320"), &file);
      
Для выполнения произвольных HTTP-запросов можно использовать более универсальную функцию request(), например:
  QHttpRequestHeader header("POST", "/search.html"); 
  header.setValue("Host", "www.trolltech.com"); 
  header.setContentType("application/x-www-form-urlencoded"); 
  http.setHost("www.trolltech.com"); 
  http.request(header, QCString("qt-interest=on&search=opengl"));
      
Перед началом выполнения очередной операции, QHttp выдает сигнал requestStarted(int), а после окончания -- requestFinished(int, bool). Аргумент int определяет идентификационный номер запроса. Если вас интересует ход выполнения отдельных команд, то вам придется сохранять их идентификационные номера, при вызове соответствующей функции. Благодаря этому появится возможность предоставить пользователю более детальную информацию о ходе процесса.

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

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

Подобно QFtp, объекты класса QHttp имеют в своем распоряжении сигнал readyRead(), и функции readBlock() и readAll(), которые могут использоваться в том случае, когда функции get() не передается устройство для записи. Кроме того, этот класс так же имет сигнал dataTransferProgress(int, int), который может быть напрвлен в слот setProgress(int, int) класса QProgressBar или QProgressDialog.


13.3. Класс QSocket.

Класс QSocket может использоваться при разработке приложений серверов и клиентов, работающих по протоколу TCP. TCP -- это протокол транспортного уровня, который является базой для множества других протоколов Интернет, включая FTP и HTTP, а так же может служить основой для разработки нестандартных протоколов обмена данными.

Протокол TCP ориентирован на потоки. Протоколы более высокого уровня, работающие поверх TCP, обычно подразделяются на строко-ориентированные и блочно-ориентированные:

Класс QSocket порожден от класса QIODevice, поэтому он в состоянии читать и писать данные из/в экземпляры классов QDataStream или QTextStream. Одно важное отличие чтения данных из сети от чтения данных из файла состои в том, что перед вызовом оператора ">>" необходимо убедиться в том, что от удаленного хоста получены все данные. В случае ошибки мы можем получить непредсказуемый результат.

В этом разделе мы рассмотрим исходный код клиентского и серверного приложений, которые используют собственный, блочно-ориентированный протокол обмена. Приложение-клиент называется Trip Planner. Оно позволяет пользователю планировать поездку по железной дороге. Приложение-сервер называется Trip Server. Оно предоставляет клиенту информацию о расписании движения поездов. Начнем с приложения Trip Planner.

Рисунок 13.1. Внешний вид приложения Trip Planner.


На форме приложения находятся поля ввода From (Из), To (В), Date (Дата), Approximate Time (Примерное время) и две кнопки с зависимой фиксацией (radio buttons), которые уточняют смысл поля pproximate Time -- время отправления или время прибытия. Когда пользователь нажимает кнопку Search, приложение передает запрос серверу и получает список поездов, которые отвечают заданным критериям. Этот список отображается в виджете QListView. В самом низу формы находятся QLabel, для отображения результатов выполнения последнего запроса, и QProgressBar.

Пользовательский интерфейс приложения был разработан в среде визуального построителя Qt Designer. Поэтому, все свое внимание мы сконцентрируем на содержимом файла .ui.h. Обратите внимание: следующие четыре переменные-члены были объявлены на вкладке Members, в построителе Qt Designer, как:

    QSocket socket; 
    QTimer connectionTimer; 
    QTimer progressBarTimer; 
    Q_UINT16 blockSize;
      
Переменная socket отвечает за работу с TCP-соединением. Переменная connectionTimer используется для отслеживания тайм аута соединения. Переменная progressBarTimer предназначена для периодического обновления индикатора хода выполнения запроса. И наконец переменная blockSize используется при анализе блока данных, полученных от сервера.
void TripPlanner::init() 
{ 
  connect(&socket, SIGNAL(connected()), 
          this, SLOT(sendRequest())); 
  connect(&socket, SIGNAL(connectionClosed()), 
          this, SLOT(connectionClosedByServer())); 
  connect(&socket, SIGNAL(readyRead()), 
          this, SLOT(updateListView()));
  connect(&socket, SIGNAL(error(int)), 
          this, SLOT(error(int))); 
  connect(&connectionTimer, SIGNAL(timeout()), 
          this, SLOT(connectionTimeout())); 
  connect(&progressBarTimer, SIGNAL(timeout()), 
          this, SLOT(advanceProgressBar())); 
  
  QDateTime dateTime = QDateTime::currentDateTime(); 
  dateEdit->setDate(dateTime.date()); 
  timeEdit->setTime(QTime(dateTime.time().hour(), 0)); 
}
      
Функция init() связывает сигналы объекта QSocket -- connected(), connectionClosed(), readyRead() и error(int), и сигналы timeout() от таймеров, с соответствующими слотами. Поля ввода Date и Approximate Time заполняются значениями по-умолчанию -- текущими датой и временем.
void TripPlanner::advanceProgressBar() 
{ 
  progressBar->setProgress(progressBar->progress() + 2); 
}
      
Слот advanceProgressBar() связан с сигналом timeout(), объекта progressBarTimer.
void TripPlanner::connectToServer() 
{ 
  listView->clear(); 
  
  socket.connectToHost("tripserver.zugbahn.de", 6178); 
  
  searchButton->setEnabled(false); 
  stopButton->setEnabled(true); 
  statusLabel->setText(tr("Connecting to server...")); 
  connectionTimer.start(30 * 1000, true); 
  progressBarTimer.start(200, false); 
  blockSize = 0; 
}
      
Слот connectToServer() вызывается по нажатию на кнопку Search. Функция вызывает connectToHost() для установления соединения с мифическим сервером tripserver.zugbahn.de, который ожидает поступления запросов на порту с номером 6178. (Если вы планируете опробовать пример на своей машине, замените имя удаленного сервера на localhost.) Функция connectToHost() работает асинхронно -- она всегда сразу же возвращает управление вызывающей программе. Само соединение устанавливается немного позже, в этот момент QSocket выдает сигнал connected(). В случае возникновении ошибки, выдается сигнал error(int) (с кодом ошибки).

После этого обновляется интерфейсная часть приложения и запускаются два таймера. Первый из них, connectionTimer -- это таймер с однократным срабатыванием. Он выдает сигнал timeout() через 30 секунд после запуска. Второй таймер, progressBarTimer, отрабатывает через каждые 200 миллисекунд. С его помощью выполняется обновление индикатора хода процесса.

И в заключении в переменную blockSize записывается значение 0. Она хранит размер очередного блока данных, принятого от сервера.

void TripPlanner::sendRequest() 
{ 
  QByteArray block; 
  QDataStream out(block, IO_WriteOnly); 
  out.setVersion(5); 
  out << (Q_UINT16)0 << (Q_UINT8)'S'
      << fromComboBox->currentText() 
      << toComboBox->currentText() 
      << dateEdit->date() 
      << timeEdit->time(); 
  if (departureRadioButton->isOn()) 
    out << (Q_UINT8)'D'; 
  else 
    out << (Q_UINT8)'A'; 
  out.device()->at(0); 
  out << (Q_UINT16)(block.size() - sizeof(Q_UINT16)); 
  socket.writeBlock(block.data(), block.size()); 
  
  statusLabel->setText(tr("Sending request...")); 
}
      
Слот sendRequest() связан с сигнвлом connected(), объекта QSocket. При появлении сигнала, слот генерирует запрос серверу, передавая информацию, введенную пользователем.

Блок запроса имеет следующую структуру:

Q_UINT16 Размер блока в байтах (исключая это поле)
Q_UINT8 Тип запроса (всегда 'S')
QString Пункт отправления
QString Пункт прибытия
QDate Дата
QTime Примерное время
Q_UINT8 Тип поля "Примерное время": 'D' -- отправление, 'A' -- прибытие.
Сначала данные записываются в объект QByteArray, который называется block. Записать данные напрямую в QSocket не представляется возможным, потому что размер блока заранее не известен.

Изначально, в поле size записывается число 0. Затем, после записи в блок всех данных, производится переход к началу блока, вызовом функции at(0) и записывается корректное значение размера передаваемого блока данных. После этого блок передается серверу, вызовом writeBlock().

void TripPlanner::updateListView() 
{ 
  connectionTimer.start(30 * 1000, true); 
  
  QDataStream in(&socket); 
  in.setVersion(5); 
  for (;;) { 
    if (blockSize == 0) { 
      if (socket.bytesAvailable() < sizeof(Q_UINT16)) 
        break; 
      in >> blockSize; 
    } 
    
    if (blockSize == 0xFFFF) { 
      closeConnection(); 
      statusLabel->setText(tr("Found %1 trip(s)") 
                              .arg(listView->childCount())); 
      break; 
    } 
    
    if (socket.bytesAvailable() < blockSize) 
      break; 
      
    QDate date; 
    QTime departureTime;
    QTime arrivalTime; 
    Q_UINT16 duration; 
    Q_UINT8 changes; 
    QString trainType; 
    
    in >> date >> departureTime >> duration >> changes 
       >> trainType; 
    arrivalTime = departureTime.addSecs(duration * 60); 
    
    new QListViewItem(listView, 
                      date.toString(LocalDate), 
                      departureTime.toString(tr("hh:mm")), 
                      arrivalTime.toString(tr("hh:mm")), 
                      tr("%1 hr %2 min").arg(duration / 60) 
                                        .arg(duration % 60), 
                         QString::number(changes), 
                         trainType); 
    blockSize = 0; 
  } 
}
      
Слот updateListView() реагирует на сигнал readyRead(), объекта QSocket, который выдается при получении новых данных от сервера. Первое, что необходимо сделать -- это перезапустить таймер с однократным срабатыванием, отслеживающий тайм аут соединения. Всякий раз, когда от сервера приходит очередная порция данных, необходимо продлить срок "жизни" соединения еще на 30 секунд.

Сервер передает расписание движения поездов, которые удовлетворяют заданным критериям. Каждая строка расписания передается в виде отдельного блока и каждый блок начинается полем, содержащим размер блока. Сложность обработки данных в цикле for заключается в том, что от сервера не все данные приходят одновременно. Мы можем получить блок целиком, или только часть блока, или полтора блока, или даже все блоки сразу.

Рисунок 13.2. Поток данных от Trip Server, разбитый на блоки.


Так как же работает цикл for? Если значение переменной blockSize равно 0, это означает, что размер очередного блока еще не прочитан. Значение 0xFFFF используется для индикации окончания передачи, поэтому, прочитав это значение, можно быть уверенным, что новых данных больше не поступит.

Если размер блока меньше 0xFFFF, то выполняется попытка прочитать блок, но прежде всего проверяется -- получен ли блок полностью. Если это не так, то цикл прерывается. При поступлении новой порции данных, снова будет выдан сигнал readyRead() и тогда можно будет повторить попытку.

После того как блок будет получен целиком, можно безопасно прочитать его оператором ">>", выделить нужную информацию и записать ее в объект класса QListViewItem. Блок, поступающий от сервера имеет следующую структуру:

Q_UINT16 Размер блока в байтах (исключая это поле)
QDate Дата отправления
QTime Время отправления
Q_UINT16 Время в пути (в минутах)
Q_UINT8 Количество остановок
QString Тип поезда
Завершив разбор блока данных, функция записывает значение 0 в переменную blockSize, говоря о том, что размер очередного блока данных неизвестен.
void TripPlanner::closeConnection() 
{ 
  socket.close(); 
  searchButton->setEnabled(true); 
  stopButton->setEnabled(false); 
  connectionTimer.stop(); 
  progressBarTimer.stop(); 
  progressBar->setProgress(0); 
}
      
Функция closeConnection() закрывает соединение с сервером, обновляет интерфейс с пользователем и останавливает таймеры. Она вызывается из updateListView(), когда будет получен блок с размером 0xFFFF, и из некоторых других слотов, которые будут описаны чуть ниже.
void TripPlanner::stopSearch() 
{ 
  statusLabel->setText(tr("Search stopped")); 
  closeConnection(); 
}
      
Слот stopSearch() реагирует на нажите кнопки Stop. Суть его состоит в закрытии соединения вызовом функции closeConnection().
void TripPlanner::connectionTimeout() 
{ 
  statusLabel->setText(tr("Error: Connection timed out")); 
  closeConnection(); 
}
      
Слот connectionTimeout() отрабатывает по истечении тайм аута соединения.
void TripPlanner::connectionClosedByServer() 
{ 
  if (blockSize != 0xFFFF) 
    statusLabel->setText(tr("Error: Connection closed by " 
                            "server")); 
    closeConnection(); 
  }
      
Слот connectionClosedByServer() реагирует на сигнал connectionClosed(), объекта socket. Если сервер закрыл соединение до того, как был получен маркер конца передачи (0xFFFF), пользователю выводится сообщение об ошибке. Затем вызывается closeConnection(), чтобы обновить интерфейс и остановить таймеры.
void TripPlanner::error(int code) 
{ 
  QString message; 
  
  switch (code) { 
    case QSocket::ErrConnectionRefused: 
        message = tr("Error: Connection refused"); 
        break; 
    case QSocket::ErrHostNotFound: 
        message = tr("Error: Server not found"); 
        break; 
    case QSocket::ErrSocketRead: 
    default: 
        message = tr("Error: Data transfer failed"); 
  } 
  statusLabel->setText(message); 
  closeConnection(); 
}
      
Слот error(int) связан с сигналом error(int) сокета. Он генерирует текст сообщения, соответствующий полученному коду ошибки.

Функция main() не содержит ничего нового:

int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  
  TripPlanner tripPlanner; 
  app.setMainWidget(&tripPlanner); 
  tripPlanner.show(); 
  return app.exec(); 
}
      
Перейдем к реализации приложения-сервера. Сервер состоит из двух классов: TripServer и ClientSocket. Первый порожден от QServerSocket и предназначен для приема входящих соединений. Второй -- наследник QSocket предназначен для обслуживания одиночного соединения с клиентом. В каждый конкретный момент времени, в памяти приложения будет находиться столько экземпляров ClientSocket, сколько клиентов подключено к серверу.
class TripServer : public QServerSocket 
{ 
public: 
  TripServer(QObject *parent = 0, const char *name = 0); 
  
  void newConnection(int socket); 
};
      
В классе TripServer перекрыт родительский метод newConnection(). Эта функция вызывается всякий раз, когда сервер обнаруживает попытку соединения с ним.
TripServer::TripServer(QObject *parent, const char *name) 
    : QServerSocket(6178, 1, parent, name) 
{ 
}
      
Здесь, родительскому конструктору передается номер порта (6178). Второй аргумент, 1, это количество подключений, ожидающих обработки.
void TripServer::newConnection(int socketId) 
{ 
  ClientSocket *socket = new ClientSocket(this); 
  socket->setSocket(socketId); 
}
      
В функции newConnection() создается новый объект класса ClientSocket, которому присваивается заданный идентификационный номер.
class ClientSocket : public QSocket 
{ 
  Q_OBJECT 
public: 
  ClientSocket(QObject *parent = 0, const char *name = 0);  

private slots: 
  void readClient(); 
  
private: 
  void generateRandomTrip(const QString &from, const QString &to, 
                          const QDate &date, const QTime &time); 
  
  Q_UINT16 blockSize; 
};
      
Класс ClientSocket порожден от класса QSocket и отвечает за обслуживание одиночного соединения с клиентом.
ClientSocket::ClientSocket(QObject *parent, const char *name) 
    : QSocket(parent, name) 
{ 
  connect(this, SIGNAL(readyRead()), 
          this, SLOT(readClient())); 
  connect(this, SIGNAL(connectionClosed()), 
          this, SLOT(deleteLater())); 
  connect(this, SIGNAL(delayedCloseFinished()), 
          this, SLOT(deleteLater())); 
  blockSize = 0; 
}
      
В конструкторе устанавливаются все необходимые соединения между сигналами и слотами, и записывается значение 0 в переменную blockSize.

Сигналы connectionClosed() и delayedCloseFinished() соединены со слотом deleteLater(). Эта функция унаследована от QObject. Она удаляет объект, когда управление переходит в цикл обработки событий. Она обеспечивает удаление экземпляров ClientSocket при закрытии соединения.

void ClientSocket::readClient() 
{ 
  QDataStream in(this); 
  in.setVersion(5); 
  
  if (blockSize == 0) { 
    if (bytesAvailable() < sizeof(Q_UINT16)) 
      return; 
    in >> blockSize; 
  } 
  if (bytesAvailable() < blockSize) 
    return; 
  
  Q_UINT8 requestType; 
  QString from; 
  QString to; 
  QDate date; 
  QTime time; 
  Q_UINT8 flag; 
  in >> requestType;

  if (requestType == 'S') { 
    in >> from >> to >> date >> time >> flag; 
    
    srand(time.hour() * 60 + time.minute()); 
    int numTrips = rand() % 8; 
    for (int i = 0; i < numTrips; ++i) 
      generateRandomTrip(from, to, date, time); 
      
    QDataStream out(this); 
    out << (Q_UINT16)0xFFFF; 
  } 
  close(); 
  if (state() == Idle) 
    deleteLater(); 
}
      
Слот readClient() связан с сигналом readyRead() сокета. Если переменная blockSize содержит 0, то выполняется попытка прочитать размер очередного блока данных, в противном случае предполагается, что размер уже прочитан и необходимо проверить -- поступил ли блок данных полностью. Если блок данных поступил целиком, то выполняется чтение блока. Чтение производится с помощью QDataStream напрямую из сокета (аргумент this).

После того как блок запроса прочитан, можно приступать к формированию ответа. Если бы это было реальное приложение, все необходимые сведения можно было бы брать из базы данных. Но здесь мы будем довольствоваться функцией generateRandomTrip(), которая генерирует расписание случайным образом. Функция будет вызываться случайное число раз и в конце передачи будет отправляться маркер конца передачи (0xFFFF).

В заключение -- соединение закрывается. Если выходной буфер сокета пуст, то соединение закрывается немедленно и можно вызвать deleteLater(), чтобы удалить сокет, когда управление попадет в цикл обработки событий. (Вполне безопасно было бы вызвать delete this.) В противном случае, сокет продолжит передачу данных и затем закроет соединение по сигналу delayedCloseFinished().

void ClientSocket::generateRandomTrip(const QString &, 
        const QString &, const QDate &date, const QTime &time) 
{ 
  QByteArray block; 
  QDataStream out(block, IO_WriteOnly); 
  out.setVersion(5); 
  Q_UINT16 duration = rand() % 200; 
  out << (Q_UINT16)0 << date << time << duration 
      << (Q_UINT8)1 << QString("InterCity"); 
  out.device()->at(0); 
  out << (Q_UINT16)(block.size() - sizeof(Q_UINT16)); 
  
  writeBlock(block.data(), block.size()); 
}
      
Функция generateRandomTrip() показывает, как можно отправить блок данных через TCP-соединение. Это очень похоже на то, что мы уже видели в клиентском приложении (функция sendRequest()). Опять же, чтобы определить размер блока, данные сначала записываются в QByteArray, а затем передаются сокету вызовом writeBlock().
int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  TripServer server; 
  if (!server.ok()) { 
    qWarning("Failed to bind to port"); 
    return 1; 
  } 
  QPushButton quitButton(QObject::tr("&Quit"), 0); 
  quitButton.setCaption(QObject::tr("Trip Server")); 
  app.setMainWidget(&quitButton); 
  QObject::connect(&quitButton, SIGNAL(clicked()), 
                   &app, SLOT(quit())); 
  quitButton.show(); 
  return app.exec(); 
}
      
В функции main() создается экземпляр класса TripServer и кнопка QPushButton, с помощью которой пользователь может остановить сервер.

На этом мы завершаем рассмотрение примера построения клиентского и серверного приложений. В данном случае мы реализовали обмен по своему, блочно-ориентированному протоколу, что позволило нам использовать QDataStream для чтения и записи данных. Если бы мы занялись реализацией строково-ориентированного протокола, то в самом простейшем случае мы могли бы воспользоваться функциями класса QSocket -- canReadLine() и readLine(), при получении сигнала readyRead():

    QStringList lines; 
    while (socket.canReadLine()) 
      lines.append(socket.readLine());
      
После этого можно былобы обработать каждую прочитанную строку. Передача текстовых строк могла бы быть выполнена с помощью QTextStream, связанного с QSocket.

Серверное приложение, в данной реализации, довольно плохо масштабируется, при наличии большого числа подключений. Проблема состоит в том, что когда обслуживается одно подключение, приложение не в состоянии обслужить другие соединения. Более масштабируемый подход заключается в создании отдельного потока для каждого соединения. Но экземпляры класса QSocket могут использоваться только в том потоке, который содержит цикл обработки событий (запускаемый вызовом QApplication::exec()), по причинам, которые более подробно будут описаны в Главе 17. Решение проблемы заключается в использовании низкоуровневого класса QSocketDevice, который работает независимо от цикла обработки событий.


13.4. Протокол UDP и класс QSocketDevice.

Класс QSocketDevice реализует низкоуровневый интерфейс для работы с протоколами UDP и TCP. В большинстве TCP-приложений, достаточно будет функциональности, заложенной в QSocket, но если в приложении необходимо работать с протоколом UDP, то тогда вам придется обратить свой взор на класс QSocketDevice.

UDP -- это протокол с негарантированной доставкой сообщений, ориентированный на работу с датаграммами. Некоторые нестандартные протоколы работают поверх UDP, поскольку он не такой "тяжелый" как TCP. По протоколу UDP, данные передаются в виде отдельных пакетов - датаграмм. В этом протоколе отсутствует понятие "соединения", если UDP-пакет теряется где-то в недрах сети, то система не получит уведомления об ошибке.

Рисунок 13.3. Внешний вид приложения TheWeather Station.


Рассмотрим принципы организации сетевых взаимодействий по протоколу UDP, на примере приложений Weather Balloon и Weather Station. Приложение Weather Balloon не имеет графического интерфейса. Его задача -- раз в 5 секунд отсылать UDP-датаграмму, которая содержит информацию о погодных условиях. Приложение Weather Station будет принимать эти датаграммы и отображать полученные сведения на экране. Начнем с приложения Weather Balloon.
class WeatherBalloon : public QPushButton 
{ 
  Q_OBJECT 
public: 
  WeatherBalloon(QWidget *parent = 0, const char *name = 0); 
  
  double temperature() const; 
  double humidity() const; 
  double altitude() const; 

protected: 
  void timerEvent(QTimerEvent *event); 
  
private: 
  QSocketDevice socketDevice; 
  int myTimerId; 
};
      
Класс WeatherBalloon порожден от QPushButton. Для взаимодествия с Weather Station он использует QSocketDevice.
WeatherBalloon::WeatherBalloon(QWidget *parent, const char *name) 
    : QPushButton(tr("Quit"), parent, name), 
        socketDevice(QSocketDevice::Datagram) 
{ 
  socketDevice.setBlocking(false); 
  myTimerId = startTimer(5 * 1000); 
}
      
В списке инициализаторов конструктора, вызовом QSocketDevice::Datagram, создается устройство QSocketDevice. В теле конструктора, созданное устройство переводится в асинхронный режим работы, вызовом setBlocking(false). (По-умолчанию, QSocketDevice работают в синхронном режиме.)
void WeatherBalloon::timerEvent(QTimerEvent *event) 
{ 
  if (event->timerId() == myTimerId) { 
    QByteArray datagram; 
    QDataStream out(datagram, IO_WriteOnly); 
    out.setVersion(5); 
    out << QDateTime::currentDateTime() << temperature() 
        << humidity() << altitude(); 
    socketDevice.writeBlock(datagram, datagram.size(), 
                            0x7F000001, 5824); 
  } else { 
    QPushButton::timerEvent(event); 
  } 
}
      
В обработчике событий от таймера генерируется датаграмма, содержащая текущие дату, время, температуру воздуха, влажность и высоту над уровнем моря:
QDateTime Дата и время измерений
double Температура (в градусах Цельсия)
double Влажность (в %)
double Высота над уровнем моря (в метрах)
Передача датаграммы осуществляется вызовом writeBlock(). Третий и четвертый аргументы функции writeBlock() -- это IP-адрес и номер порта клиентского узла (Weather Station). В данном случае мы исходим из предположения, что оба приложения работают на одной машине, поэтому IP-адрес -- 127.0.0.1 (0x7F000001). В отличие от QSocket, экземпляры класса QSocketDevice не работает с сетевыми именами компьютеров, ему нужны IP-адреса. Если вам необходимо преобразовать имя хоста в его IP-адрес, это можно сделать с помощью класса QDns.
int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  WeatherBalloon balloon; 
  balloon.setCaption(QObject::tr("Weather Balloon")); 
  app.setMainWidget(&balloon); 
  QObject::connect(&balloon, SIGNAL(clicked()), 
                   &app, SLOT(quit())); 
  balloon.show(); 
  return app.exec(); 
}
      
Функция main() просто создает объект WeatherBalloon, который выступает в двух ипостасях: как UDP-узел и как кнопка QPushButton на экране.

Теперь перейдем к приложению Weather Station.

class WeatherStation : public QDialog 
{ 
  Q_OBJECT 
public: 
  WeatherStation(QWidget *parent = 0, const char *name = 0); 
  
private slots: 
  void dataReceived(); 
  
private: 
  QSocketDevice socketDevice; 
  QSocketNotifier *socketNotifier; 
  
  QLabel *dateLabel; 
  QLabel *timeLabel; 
  ... 
  QLineEdit *altitudeLineEdit; 
};
      
Класс WeatherStation порожден от QDialog. Его назначение -- ожидать поступления датаграмм на определенном порту, производить разбор поступившей информации (от Weather Balloon) и отображать ее в пяти компонентах QLineEdit.

Класс содержит две приватные переменные, которые представляют для нас наибольший интерес: socketDevice и socketNotifier. Первая переменная имеет тип QSocketDevice. Она используется для приема датаграмм. Вторая переменная имеет тип QSocketNotifier. Она используется для того, чтобы известить приложение о поступлении датаграммы.

WeatherStation::WeatherStation(QWidget *parent, const char *name) 
    : QDialog(parent, name), socketDevice(QSocketDevice::Datagram) 
{ 
  socketDevice.setBlocking(false); 
  socketDevice.bind(QHostAddress(), 5824); 
  socketNotifier = new QSocketNotifier(socketDevice.socket(), 
                                       QSocketNotifier::Read, 
                                       this); 
  connect(socketNotifier, SIGNAL(activated(int)), 
          this, SLOT(dataReceived())); 
  ... 
}
      
В списке инициализаторов конструктора создается устройство QSocketDevice, вызовом QSocketDevice::Datagram. В теле конструктора, вызовом setBlocking(false), оно переводится в асинхронный режим работы. Вызовом функции bind(), сокету назначаются IP-адрес и номер порта. Вызовом функции QHostAddress() мы сообщаем, что будем принимать датаграммы, отправляемые на любой IP-адрес, который принадлежит компьютеру с запущенным приложением Weather Station.

Затем создается объект QSocketNotifier, который будет "следить" за сокетом. Объект QSocketNotifier будет выдавать сигнал activated(int) в момент поступления датаграммы. Этот сигнал соединяется со слотом dataReceived().

void WeatherStation::dataReceived() 
{ 
  QDateTime dateTime; 
  double temperature; 
  double humidity; 
  double altitude; 
  
  QByteArray datagram(socketDevice.bytesAvailable()); 
  socketDevice.readBlock(datagram.data(), datagram.size()); 
  
  QDataStream in(datagram, IO_ReadOnly); 
  in.setVersion(5); 
  in >> dateTime >> temperature >> humidity >> altitude; 
  
  dateLineEdit->setText(dateTime.date().toString()); 
  timeLineEdit->setText(dateTime.time().toString()); 
  temperatureLineEdit->setText(tr("%1 C").arg(temperature)); 
  humidityLineEdit->setText(tr("%1%").arg(humidity)); 
  altitudeLineEdit->setText(tr("%1 m").arg(altitude)); 
}
      
В функции dataReceived() производится чтение датаграммы, вызовом readBlock(). Функция QByteArray::data() возвращает указатель на данные в QByteArray, по которому readBlock() запишет полученные сведения. Затем производится извлечение отдельных значений, которые заносятся в визуальные компоненты для отображения на экране. С точки зрения приложения, датаграмма всегда передается и принимается как единый блок данных. Это означает, что если доступен хотя бы один байт, то следовательно датаграмма получена целиком.
int main(int argc, char *argv[]) 
{ 
  QApplication app(argc, argv); 
  WeatherStation station; 
  app.setMainWidget(&station); 
  station.show(); 
  return app.exec(); 
}
      
В заключение, в функции main() создается объект WeatherStation и назначается главным виджетом приложения.

На этом мы заканчиваем рассмотрение принципов работы с UDP. Мы постарались создать приемное и передающее приложения настолько простыми, насколько это возможно. В большинстве реальных применений, приложения должны как передавать, так и принимать датаграммы. В составе класса QSocketDevice имеются функции peerAddress() и peerPort(), которые могут использоваться для определения IP-адреса и номера порта, на которые нужно послать ответ.


Предыдущая глава | Содержание | Следующая глава


Ж. Бланшетт, М. Саммерфильд, "Глава 13. Работа с сетью" - 01/12/2004

г.ВИТЕБСК
РЕСПУБЛИКА БЕЛАРУСЬ

 
  О ресурре... Главная
[ ГЛАВНАЯ ] [ СОДЕРЖАНИЕ ]

E-mail : БИРЛ "Ombrello".

Hosted by uCoz