Класс QSocket может использоваться при разработке приложений серверов и клиентов, работающих по протоколу TCP. TCP -- это протокол транспортного уровня, который является базой для множества других протоколов Интернет, включая FTP и HTTP, а так же может служить основой для разработки нестандартных протоколов обмена данными.
Протокол TCP ориентирован на потоки. Протоколы более высокого уровня, работающие поверх TCP, обычно подразделяются на строко-ориентированные и блочно-ориентированные:
Строко-ориентированные протоколы передают данные в виде текстовых строк, каждая из которых завершается символом перевода строки.
Блочно-ориентированные протоколы передают данные в виде блоков. Размер каждого блока содержится в отдельном поле, внутри блока.
В этом разделе мы рассмотрим исходный код клиентского и серверного приложений, которые используют собственный, блочно-ориентированный протокол обмена. Приложение-клиент называется Trip Planner. Оно позволяет пользователю планировать поездку по железной дороге. Приложение-сервер называется Trip Server. Оно предоставляет клиенту информацию о расписании движения поездов. Начнем с приложения Trip Planner.
Рисунок 13.1. Внешний вид приложения Trip Planner.
Пользовательский интерфейс приложения был разработан в среде визуального построителя 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' -- прибытие. |
Изначально, в поле 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, разбитый на блоки.
Если размер блока меньше 0xFFFF, то выполняется попытка прочитать блок, но прежде всего проверяется -- получен ли блок полностью. Если это не так, то цикл прерывается. При поступлении новой порции данных, снова будет выдан сигнал readyRead() и тогда можно будет повторить попытку.
После того как блок будет получен целиком, можно безопасно прочитать его оператором ">>", выделить нужную информацию и записать ее в объект класса QListViewItem. Блок, поступающий от сервера имеет следующую структуру:
Q_UINT16 | Размер блока в байтах (исключая это поле) |
QDate | Дата отправления |
QTime | Время отправления |
Q_UINT16 | Время в пути (в минутах) |
Q_UINT8 | Количество остановок |
QString | Тип поезда |
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, который работает независимо от цикла обработки событий.
Пред. | В начало | След. |
Класс QHttp. | На уровень выше | Протокол UDP и класс QSocketDevice. |