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


Для реализации клиента нужно создать объект класса QTcpSocket, а затем вызвать метод connectToHost(), передав в него первым параметром имя компьютера (или его IP-адрес), а вторым — номер порта сервера. Объект класса QTcpSocket сам попытается произвести установку связи с сервером и, в случае успеха, вышлет сигнал connected(). В противном случае будет выслан сигнал error(int) с кодом ошибки, определенным в перечислении QAbstractSocket::SocketError. Это может произойти, например, в том случае, если на указанном компьютере не установлен сервер или не соответствует номер порта. После установления соединения объект класса QTcpSocket может высылать или считывать данные сервера.

Взаимодействие клиента с сервером изображено на рисунке. Высылка на сервер информации, введенной в однострочном текстовом поле окна клиента, производится после нажатия на кнопку Send (Выслать).


#include <QApplication>
#include "MyClient.h"

int main(int argc, char** argv)
{
    QApplication app(argc, argv);
    MyClient     client("localhost", 2323);

    client.show();

    return app.exec();
}

{рисунок}

В функции main() мы создаем объект клиента. Если запуск сервера и клиента производится на одном и том же компьютере, то в качестве имени компьютера можно передать строку "localhost". Номер порта, в нашем случае, равен 2323, так как это тот порт, который используется нашим сервером.


#ifndef _MyClient_h_
#define _MyClient_h_

#include <QWidget>
#include <QTcpSocket>

class QTextEdit;
class QLineEdit;

class MyClient : public QWidget {
Q_OBJECT
private:
    QTcpSocket* m_pTcpSocket;
    QTextEdit*  m_ptxtInfo;
    QLineEdit*  m_ptxtInput;
    quint16     m_nNextBlockSize;

public:
    MyClient(const QString& strHost, int nPort, QWidget* pwgt = 0) ;

private slots:
    void slotReadyRead   (                            );
    void slotError       (QAbstractSocket::SocketError);
    void slotSendToServer(                            );
    void slotConnected   (                            );
};
#endif  //_MyClient_h_

В классе MyClient мы объявляем атрибут m_pTcpSocket, который необходим для управления нашим клиентом, и атрибут m_nNextBlockSize, необходимый для хранения длины следующего полученного от сокета блока. Остальные два атрибута — m_txtInfo и m_txtInput — используются для отображения и ввода информации соответственно.


MyClient::MyClient(const QString& strHost, 
                   int            nPort, 
                   QWidget*       pwgt /*=0*/
                  ) : QWidget(pwgt)
                    , m_nNextBlockSize(0)
{
    m_pTcpSocket = new QTcpSocket(this);

    m_pTcpSocket->connectToHost(strHost, nPort);
    connect(m_pTcpSocket, SIGNAL(connected()), SLOT(slotConnected()));
    connect(m_pTcpSocket, SIGNAL(readyRead()), SLOT(slotReadyRead()));
    connect(m_pTcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
            this,         SLOT(slotError(QAbstractSocket::SocketError))
           );

    m_ptxtInfo  = new QTextEdit;
    m_ptxtInput = new QLineEdit;

    connect(m_ptxtInput, SIGNAL(returnPressed()), 
            this,        SLOT(slotSendToServer())
           );
    m_ptxtInfo->setReadOnly(true);

    QPushButton* pcmd = new QPushButton("&Send");
    connect(pcmd, SIGNAL(clicked()), SLOT(slotSendToServer()));

    //Layout setup
    QVBoxLayout* pvbxLayout = new QVBoxLayout;    
    pvbxLayout->addWidget(new QLabel("<H1>Client</H1>"));
    pvbxLayout->addWidget(m_ptxtInfo);
    pvbxLayout->addWidget(m_ptxtInput);
    pvbxLayout->addWidget(pcmd);
    setLayout(pvbxLayout);
}

В конструкторе, происходит создание объекта сокета (указатель m_pTcpSocket). Из объекта сокета вызывается метод connectToHost(), осуществляющий связь с сервером. Первым параметром в этот метод передается имя компьютера, а вторым — номер порта. Связь между сокетами асинхронна. Сокет высылает сигнал connected() как только будет произведено соединение, а также высылает сигнал readyRead() о готовности предоставить данные для чтения. Мы соединяем сигналы connected(), readyRead() со слотами slotConnected() и slotReadyRead() соответственно. В случаях возникновения ошибок сокет высылает сигнал error(), который мы соединяем со слотом slotError(), в котором производим отображение ошибок.

Затем создается пользовательский интерфейс программы, состоящий из надписи, кнопки нажатия, однострочного и многострочного текстовых полей. Сигнал clicked() виджета кнопки нажатия соединяется со слотом slotSendToServer() класса MyClient, ответственным за отправку данных на сервер. Для того чтобы к аналогичному действию приводило и нажатие клавиши <Enter>, мы соединяем сигнал returnPressed() виджета текстового поля (m_ptxtInput) c тем же слотом slotSendToServer().


void MyClient::slotReadyRead()
{
    QDataStream in(m_pTcpSocket);
    in.setVersion(QDataStream::Qt_4_2);
    for (;;) {
        if (!m_nNextBlockSize) {
            if (m_pTcpSocket->bytesAvailable() < sizeof(quint16)) {
                break;
            }
            in >> m_nNextBlockSize;
        }

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

        m_ptxtInfo->append(time.toString() + " " + str);
        m_nNextBlockSize = 0;
    }
}

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

После того как мы будем уверенны, что блок получен целиком, то можем без опасения использовать оператор >> объекта потока QDataStream (переменная in). Чтение данных из сокета осуществляется при помощи объекта потока данных. Полученная информация добавляется в виджет многострочного текстового поля (указатель m_ptxtInfo) с помощью метода append().

В завершение анализа блока данных мы присваиваем атрибуту m_nNextBlockSize значение 0, которое указывает на то, что размер очередного блока данных неизвестен.


void MyClient::slotError(QAbstractSocket::SocketError err)
{
    QString strError = 
        "Error: " + (err == QAbstractSocket::HostNotFoundError ? 
                     "The host was not found." :
                     err == QAbstractSocket::RemoteHostClosedError ? 
                     "The remote host is closed." :
                     err == QAbstractSocket::ConnectionRefusedError ? 
                     "The connection was refused." :
                     QString(m_pTcpSocket->errorString())
                    );
    m_ptxtInfo->append(strError);
}

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


void MyClient::slotSendToServer()
{
    QByteArray  arrBlock;
    QDataStream out(&arrBlock, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_4_2);
    out << quint16(0) << QTime::currentTime() << m_ptxtInput->text();

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

    m_pTcpSocket->write(arrBlock);
    m_ptxtInput->setText("");
}

Обратите внимание на то, что мы не можем записывать данные сразу в QTcpSocket, потому что мы не знаем размер блока, который должен быть выслан в первую очередь. Поэтому мы должны сначала создать объект QByteArray, для того чтобы записывать все данные блока в него, записывая сначала размер равным 0. После того как все необходимые данные блока записаны, мы перемещаем указатель на начало блока и вызовом метода seek() записываем размер блока, который вычисляется как размер arrBlock с вычитанием из него sizeof(quint16). Это делается для исключения данных размера при подсчете байт.

После нажатия кнопки Send (Послать) вызывается слот slotSendToServer(), который записывает в сокет строку, введенную пользователем в виджете однострочного текстового поля (указатель m_ptxtInput).


void MyClient::slotConnected()
{
    m_ptxtInfo->append("Received the connected() signal");
}

Как только связь с сервером установлена, вызывается метод slotConnected() и в виджет текстового поля добавляется строка сообщения.

Читать далее: Класс QFtp