Реализация сервера с помощью класса QTcpServer


Для реализации сервера Qt предоставляет удобный класс QTcpServer, который предназначен для управления входящими TCP-соединениями. Программа, показанная на рисунке, является реализацией простого сервера, который принимает и подтверждает получение запросов клиентов.

{рисунок}


#include <QtGui>
#include "MyServer.h"
int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    MyServer     server(2323);

    server.show();

    return app.exec();
}

Создается объект сервера. Чтобы запустить сервер, нужно создать объект класса MyServer, передав в конструктор номер порта, по которому должен осуществляться нужный сервис. В нашем случае передается номер порта, равный 2323.


#ifndef _MyServer_h_
#define _MyServer_h_

#include <QWidget>

class QTcpServer;
class QTextEdit;
class QTcpSocket;

class MyServer : public QWidget {
Q_OBJECT
private:
    QTcpServer* m_ptcpServer;
    QTextEdit*  m_ptxt;
    quint16     m_nNextBlockSize;

private:
    void sendToClient(QTcpSocket* pSocket, const QString& str);

public:
    MyServer(int nPort, QWidget* pwgt = 0);

public slots:
    virtual void slotNewConnection();
            void slotReadClient   ();
};
#endif  //_MyServer_h_

В классе MyServer мы объявляем атрибут m_ptcpServer, который и является основной частью для управления нашим сервером. Атрибут m_nNextBlockSize служит для хранения длины следующего, полученного от сокета блока.


MyServer::MyServer(int nPort, QWidget* pwgt /*=0*/) : QWidget(pwgt)
                                                    , m_nNextBlockSize(0)
{
    m_ptcpServer = new QTcpServer(this); 
    if (!m_ptcpServer->listen(QHostAddress::Any, nPort)) {
        QMessageBox::critical(0, 
                              "Server Error", 
                              "Unable to start the server:" 
                              + m_ptcpServer->errorString()
                             );
        m_ptcpServer->close();
        return;
    }
    connect(m_ptcpServer, SIGNAL(newConnection()), 
            this,         SLOT(slotNewConnection())
           );

    m_ptxt = new QTextEdit;
    m_ptxt->setReadOnly(true);

    //Layout setup
    QVBoxLayout* pvbxLayout = new QVBoxLayout;    
    pvbxLayout->addWidget(new QLabel("<H1>Server</H1>"));
    pvbxLayout->addWidget(m_ptxt);
    setLayout(pvbxLayout);
}

Для установки сервера нам необходимо вызвать в конструкторе метод listen(). В этот метод необходимо передать номер порта, который мы получили в конструкторе. При возникновении ошибочных ситуаций, например невозможности захвата порта, этот метод возвратит значение false, на которое мы отреагируем показом окна сообщения об ошибке.

Далее мы производим соединение с сигналом newConnection(), который высылается при каждом присоединении нового клиента.

Для отображения информации мы создаем виджет многострочного текстового поля (указатель m_ptxt) и устанавливаем в нем, вызовом метода setReadOnly(), режим, делающий возможным только просмотр информации.


/*virtual*/ void MyServer::slotNewConnection()
{
    QTcpSocket* pClientSocket = m_ptcpServer->nextPendingConnection();
    connect(pClientSocket, SIGNAL(disconnected()),
            pClientSocket, SLOT(deleteLater())
           );
    connect(pClientSocket, SIGNAL(readyRead()), 
            this,          SLOT(slotReadClient())
           );

    sendToClient(pClientSocket, "Server Response: Connected!");
}

Метод slotNewConnection() вызывается каждый раз при соединении с новым клиентом. Из этого метода мы выполняем соединения с сигналами disconnected() и readyRead(), которые сигнализируют об отсоединении клиента и его готовности к передаче данных соответственно. В завершение мы вызываем метод sendToServer() для отсылки приветствия присоединенному клиенту. В этом методе, вторым параметром, мы передаем строку. Внутри самого метода будет сгенерирован временной штамп, который будет отослан клиенту вместе с переданной строкой.

Для подтверждения соединения с клиентом необходимо вызвать метод nextPendingConnection(), который возвратит сокет, посредством которого можно осуществлять дальнейшую связь с клиентом. Мы соединяем сигнал disconnected(), высылаемый сокетом при отсоединении клиента, со слотом deleteLater(), предназначенным для его последующего уничтожения. При поступлении запросов от клиентов высылается сигнал readyToRead(), который мы соединяем со слотом slotReadClient().


void MyServer::slotReadClient()
{
    QTcpSocket* pClientSocket = (QTcpSocket*)sender();
    QDataStream in(pClientSocket);
    in.setVersion(QDataStream::Qt_4_2);
    for (;;) {
        if (!m_nNextBlockSize) {
            if (pClientSocket->bytesAvailable() < sizeof(quint16)) {
                break;
            }
            in >> m_nNextBlockSize;
        }

        if (pClientSocket->bytesAvailable() < m_nNextBlockSize) {
            break;
        }
        QTime   time;
        QString str;
        in >> time >> str;

        QString strMessage = 
            time.toString() + " " + "Client has sended - " + str;
        m_ptxt->append(strMessage);

        m_nNextBlockSize = 0;

        sendToClient(pClientSocket, 
                     "Server Response: Received \"" + str + "\""
                    );
    }
}

Сначала производится преобразование указателя, возвращаемого методом sender(), к типу QTcpSocket. Цикл for нам нужен, так как не все высланные клиентом данные могут прийти одновременно. Поэтому сервер должен быть в состоянии получать как весь блок целиком, так и только часть блока, а так же и все блоки сразу. Каждый переданный сокетом блок начинается полем, описывающим размер блока. Размер блока, который считывается при условии того, что размер полученных данный не меньше двух байт и атрибут m_nNextBlockSize равен нулю (то есть размер блока неизвестен). Если доступных данных для чтения больше или равно размеру блока, тогда производится считывание из потока данных в переменные time и str. После этого значение переменной time преобразуется вызовом метода toString() в строку и используется вместе со строкой str для строки сообщения strMessage. Строка сообщения добавляется в виджет текстового поля вызовом метода append(). Анализ блока данных завершается присваиванием атрибуту m_nNextBlockSize значения 0, которое говорит о том, что размер очередного блока данных неизвестен. Вызовом метода sendToClient() мы сообщаем клиенту о том, что нам успешно удалось прочитать высланные им данные.


void MyServer::sendToClient(QTcpSocket* pSocket, const QString& str)
{
    QByteArray  arrBlock;
    QDataStream out(&arrBlock, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_4_2);
    out << quint16(0) << QTime::currentTime() << str;

    out.device()->seek(0);
    out << quint16(arrBlock.size() - sizeof(quint16));

    pSocket->write(arrBlock);
}

В методе sendToClient() мы формируем данные, которые будут высланы клиенту. Но есть один нюанс, который заключается в том, что нам неизвестен размер блока, и, следовательно, мы не можем записывать данные сразу в сокет, так как размер блока должен быть выслан в первую очередь. Поэтому мы прибегаем к следующим действиям. Мы сначала создаем объект QByteArray. В него мы записываем все данные блока, причем записываем размер, равный 0. После этого мы перемещаем указатель на начало блока вызовом метода seek(), вычисляем и записываем размер блока в поток (out) (вычисляется как размер arrBlock с вычитанием из него sizeof(quint16)). После этого созданный блок записывается в сокет вызовом метода write().

Примечание. Для пересылки обычных строк мы могли бы использовать класс потока ввода QTextStream. Причиной, по которой используется класс QDataStream, является то, что пересылка бинарных данных представляет собой общий случай, это нам необходимо для того, чтобы переслать объект класса QTime. To есть, вы можете отправлять не только строки, но и еще растровые изображения, объекты палитры и т. д. Далее мы будем использовать класс QDataStream.

Читать далее: Реализация клиента с помощью класса QTcpSocket