Инструменты пользователя

Инструменты сайта


develop:qt:network

Это старая версия документа!


Поддержка сети Qt

qt += network
Для реализации серверного функционала используется класс QTcpServer.
Для непосредственного приема передачи используются сокеты (базовый класс QAbstractSocket), есть доп реализации Tcp, Udp, Ssl и т.д. Собсна клиент содержит в себе только объект сокета.
Для сетевых запросов существует класс QNetworkAccessManager.

QTcpServer

Позволяет принимать Tcp-соединения, порт можно выбрать либо авто, IP так же можно выбрать конкретный либо все IP хоста.

Вызовите listen(), для начала прослушивания. close() для завершения.
При подключении клиента, вызывается сигнал newConnection(). Вызовите nextPendingConnection() что бы принять соединение как QTcpSocket, который можно использовать для связи с клиентом.

В случае ошибки, serverError() возвращает тип ошибки, а errorString() вернут описание.

Сервер в основном предназначен для использования сигнало/слотами, имеются методы для блокирующей работы, типа «waitFor..».

QAbstractSocket

Является базовым классом для QTcp и QUdp сокетов, и содержит все общие функции этих двух классов. Его можно наследовать, для реализации собственного сокета.

Так же, API QAbstractSocket объединяет большинство различий между двумя протоколами.
Например метод connectToHost(), для Udp, устанавливает виртуальное соединение. QAbstractSocket запоминает адрес и порт, переданные в этот метод, и такие функции как read() или write() читают эти данные оттуда.

В любой момент, объект имеет определенное состояние (метод state()).
Начальное состояние- UnconnectedState.
После вызова connectToHost() сокет сначала входит в HostLookupState, если хост найден тогда переходит в ConnectingState и излучает сигнал hostFound().
После установки соединения, статус переходит в ConnectedState и излучает сигнал connected().
В случае ошибки, на любом этапе, выдается сигнал errorOcurred(). При смене состояния, генерируется stateChanged().
Метод isValid() возвращает bool, готов ли сокет к чтению/записи, но состояние должно быть ConnectedState.

Читать/писать данные можно методами: read(), write(), readLine(), readAll(), getChar(), putChar(), ungetChar().
Сигнал bytesWritten() возникает каждый раз, когда данные были записаны в сокет. Qt не ограничивает размер буфера записи, за ним можно следить используя этот сигнал.

Сигнал readyRead() выдается каждый раз, когда поступает новый блок данных, bytesAvailable() возвращает кол-во доступных байт.
Для чтения подключаемся к слоту readyRead() и читаем все доступные байты, если считать не все, то оставшиеся будут доступны позже, а любые входящие данные будут добавлены во внутренний буфер. Чтобы ограничить размер этого буфера, метод- setReadBufferSize().

Для закрытия соединения, метод disconnectFromHost(), сокет входит в состояние ClosingState и ждет очереди ожидающих данных, после нее, фактически закрывается соединение, переходит в UnconecctedState и посылает сигнал disconnected().
Если нужно немедленно прервать соединение, есть метод abort().
Если удаленный хост закрывает соединение, то сокет выдаст errorOcurrent(QAbstractSocket::RemoteHostClosedError), в течении которого состояние будет все еще ConnectedState, а затем будет сигнал disconnected().

Параметры удаленного узла (переданного в connectedToHost())- peerPort(), peerAddress(), peerName().
Локальные- localPort(), localAddress().

Так же есть ряд методов синхронизации потоков, типа «waitFor..».

QUdpSocket


Распространенный способ использования- привязка к адресу и порту с помощью bind(), затем вызов write/read/receiveDatagram(), можно не вызывать ее, если просто отправлять дейтаграммы.
Что бы использовать read(), readLine(), write() и т.д. нужно выполнить (условное) подключение к узлу, метод connectToHost().

Сигналы bytesWrite()/readyRead() вызываются при отправке/получении дейтаграмм.

QUdpSocket поддерживает многоадресную рассылку, {join,leave}Multicast{Group,Interface}.

Для работы с дейтаграммами используется класс QNetworkDatagram.

QNetworkAccessManager


API для доступа к сети построено вокруг этого объекта, содержит в общую конфигурацию и настройку отправляемых запросов, прокси, кэша, а так же связанные с сетевой работой слото-сигналы.
Одного экземпляра достаточно для всего приложения, вызывается только из своего потока.
Этот объект управляет авторизацией на ресурсе, если она необходима.

Для запросов и ответов есть классы QNetworkRequest и QNetworkReply соответственно.

QNetworkRequest


QNetworkReply


QHostAddress


Класс содержит адреса v4/v6.
Адрес устанавливается и извлекается, можно проверить его тип, поддерживает предопределенные адреса, типа LocalHost, Broadcast, Any.

Пример клиент/сервера

Сервер


Сервер на базе QTcpServer, слушает указанный IP и порт.
При приеме входящего подключения отправляет клиенту указанную инфу и закрывает соединение.
Данные передаются в открытом виде

При приеме соединения вызывается сигнал NewConnection(), его обрабатываем.
Внутри создаем объект пользовательского подключения, методом nextPendingConnection(), и работаем с этим сокетом подключения, отправляемые данные просто пишем в него, как в устройство (QIODevice).
Запись делаем методом write(), на сколько я понял разделение на пакеты происходит уже на уровне протокола, в зависимости от размера данных, и неважно сколько раз мы вызываем write().
Пакеты по 60-65 кБ.

В данном примере открываем файл и заносим его содержимое в массив QByteArray, можно сразу целиком, можно частями, с указанным размером (чтение автоматом продолжается с нужного места) Далее передаем получившийся QByteArray в метод write(), тем самым отправляя его клиенту.

:!: Пример: Простой tcp-сервер, передача одного файла

Основан на примере «Fortune Server Example», переделан под файл sserv.h

#ifndef SSERV_H
#define SSERV_H
 
#include <QDialog>
#include <QLabel>
#include <QVector>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMessageBox>
#include <QNetworkInterface>
#include <QRandomGenerator>
#include <QByteArray>
#include <QDataStream>
#include <QBoxLayout>
#include <QFile>
 
class sserv : public QDialog
{
    Q_OBJECT
 
    QLabel *labStatusText;
    QTcpServer *tcpServer;
    //QVector<QString> vecFurtunes;
 
private slots:
    void slotSendFortune();
 
public:
    sserv(QWidget *parent = nullptr);
 
};
#endif // SSERV_H

sserv.cpp

#include "sserv.h"
 
sserv::sserv(QWidget *parent): QDialog(parent)
{
    tcpServer= new QTcpServer(this);
    if(!tcpServer->listen(QHostAddress::LocalHost, 2233))
    {
        QMessageBox::critical(this, "Caption", tr("Unable to start: %1").arg(tcpServer->errorString()));
        this->close();
        return;
    }
 
    QString vIpAddr;
    QList<QHostAddress> vListIp= QNetworkInterface::allAddresses();
    for(int i= 0; i < vListIp.size(); ++i)
    {
        if(vListIp.at(i) != QHostAddress::LocalHost && vListIp.at(i).toIPv4Address())
        {
            vIpAddr= vListIp.at(i).toString();
            break;
        }
    }
    if(vIpAddr.isEmpty())
        vIpAddr= QHostAddress(QHostAddress::LocalHost).toString();
 
    this->setFont(QFont("Arial", 14));
    labStatusText= new QLabel(this);
    labStatusText->setText(tr("It's running on\n\nIP: %1\nport: %2").arg(vIpAddr).arg(tcpServer->serverPort()));
 
    QBoxLayout *layCenter= new QBoxLayout(QBoxLayout::Direction::LeftToRight, this);
    layCenter->addWidget(labStatusText, 0, Qt::AlignCenter);
 
 
    this->vecFurtunes << "First String" << "Second String" << "Three String" << "Four String" << "Fiftin String";
 
    connect(this->tcpServer, &QTcpServer::newConnection, this, &sserv::slotSendFortune);
}
 
 
void sserv::slotSendFortune()
{
    // Тут была передача строки из массива "this->vecFurtunes"
    /*QByteArray vBlockData;
    QDataStream vOutStreem(&vBlockData, QIODevice::WriteOnly);
    vOutStreem.setVersion(QDataStream::Qt_5_10);
    vOutStreem << this->vecFurtunes[QRandomGenerator::global()->bounded(this->vecFurtunes.size())];
 
    QTcpSocket *vClientConnection= this->tcpServer->nextPendingConnection();
    connect(vClientConnection, &QAbstractSocket::disconnected, vClientConnection, &QObject::deleteLater);
 
    vClientConnection->write(vBlockData);
    vClientConnection->disconnectFromHost(); */
 
    // Передаем указанный файл
    QFile vFile("D:\\pic1.png");
    vFile.open(QIODevice::ReadOnly);
 
    QByteArray vBlockData= vFile.readAll(); // read(64)- размер указан в байтах
 
    QTcpSocket *vClientConnection= this->tcpServer->nextPendingConnection();
    connect(vClientConnection, &QAbstractSocket::disconnected, vClientConnection, &QObject::deleteLater);
    vClientConnection->write(vBlockData);
    vClientConnection->disconnectFromHost();
}

Клиент


Клиент сам подключается к серверу и сервер сразу же начинает передачу. На клиенте просто обрабатываем сигнал readyRead().

Объект сокета должен быть глобальным, этим сокетом инициируем соединение и с этого сокета затем читаем доступные данные. Читаем в QByteArray и далее работаем с ними. Если сохраняем в файл (как в примере) то необходимо открывать его в режиме добавления, т.к. вызов этой функции и собсна запись, происходят при получении каждого пакета.

:!: Пример: Простой tcp-клиент, получение файла

sc.h

#ifndef SC_H
#define SC_H
 
#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTcpSocket>
#include <QDataStream>
#include <QByteArray>
#include <QComboBox>
#include <QMessageBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QTimer>
#include <QNetworkInterface>
#include <QGridLayout>
#include <QFile>
#include <QIODevice>
 
class sc : public QDialog
{
    Q_OBJECT
 
    QComboBox *comboHostServ;
    QLineEdit *editPortServ;
    QLabel *labComboCapt, *labEditCapt, *labStatus;
    QPushButton *butGetFortune, *butQuit;
 
    QTcpSocket *tcpSocket;
    QDataStream VInputStream;
    QString VCurrFortune;
 
private slots:
    void slotRequestNewFortune();
    void slotReadFortune();
    void slotDisplayError(QAbstractSocket::SocketError vSockError);
 
public:
    sc(QWidget *parent = nullptr);
};
#endif // SC_H

sc.cpp

#include "sc.h"
 
sc::sc(QWidget *parent): QDialog(parent)
{
    this->setFont(QFont("Arial", 14));
    QGridLayout *layMainGrid= new QGridLayout(this);
    layMainGrid->setColumnStretch(1, 1);
    layMainGrid->setColumnStretch(2, 1);
 
    labComboCapt= new QLabel("Выберите хост", this);
    comboHostServ= new QComboBox(this);
    comboHostServ->addItem(QHostAddress(QHostAddress::LocalHost).toString());
    layMainGrid->addWidget(labComboCapt, 0, 0);
    layMainGrid->addWidget(comboHostServ, 0, 1, 1, -1);
 
    labEditCapt= new QLabel("Укажите порт", this);
    editPortServ= new QLineEdit(this);
    editPortServ->setText("2233");
    layMainGrid->addWidget(labEditCapt, 1, 0);
    layMainGrid->addWidget(editPortServ, 1, 1, 1, -1);
 
    labStatus= new QLabel("This is status oO", this);
    layMainGrid->addWidget(labStatus, 2, 0, 1, -1, Qt::AlignHCenter);
 
    butGetFortune= new QPushButton("Get Fort", this);
    butQuit= new QPushButton("Quit", this);
    layMainGrid->addWidget(butGetFortune, 3, 1);
    layMainGrid->addWidget(butQuit, 3, 2);
 
    layMainGrid->setSpacing(20); // setRowStretch(0, 10);
 
    tcpSocket= new QTcpSocket(this);
    VInputStream.setDevice(tcpSocket);
    VInputStream.setVersion(QDataStream::Qt_4_0);
 
    connect(tcpSocket, &QIODevice::readyRead, this, &sc::slotReadFortune);
    connect(tcpSocket, &QAbstractSocket::errorOccurred, this, &sc::slotDisplayError);
    connect(butGetFortune, &QPushButton::clicked, this, &sc::slotRequestNewFortune);
    connect(butQuit, &QPushButton::clicked, this, &QDialog::close);
}
 
 
void sc::slotRequestNewFortune()
{
    butGetFortune->setEnabled(false);
    tcpSocket->abort();
    tcpSocket->connectToHost(comboHostServ->currentText(), editPortServ->text().toInt());
}
 
 
void sc::slotReadFortune()
{
    // Тут было получение строки, не до конца понятна работа с транзакцией
    /*VInputStream.startTransaction();
    QString vNextFortune;
    VInputStream >> vNextFortune;
 
    if(!VInputStream.commitTransaction())
        return;
 
    if(vNextFortune== VCurrFortune)
    {
        QTimer::singleShot(0, this, &sc::slotRequestNewFortune);
        return;
    }
 
    VCurrFortune= vNextFortune;
    labStatus->setText(VCurrFortune);
    butGetFortune->setEnabled(true);*/
 
    // Получение файла
    QByteArray vGettingData;
    vGettingData= tcpSocket->readAll();
 
    QFile vFile("D:\\pic1-getting.png");
    vFile.open(QIODevice::Append);
    vFile.write(vGettingData);
    vFile.close();
 
 
    butGetFortune->setEnabled(true);
}
 
 
void sc::slotDisplayError(QAbstractSocket::SocketError vSockError)
{
    switch (vSockError)
    {
        case QAbstractSocket::RemoteHostClosedError:
            break;
        case QAbstractSocket::HostNotFoundError:
            QMessageBox::information(this, "Header", "Host not found");
            break;
        case QAbstractSocket::ConnectionRefusedError:
            QMessageBox::information(this, "Header", "Connection refured");
            break;
        default:
            QMessageBox::information(this, "Header", "Error - "+ tcpSocket->errorString());
            break;
    }
    butGetFortune->setEnabled(true);
}

Еще пример клиент/сервера

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

При передаче, данные делятся на пакеты дважды, типа программные и физические.
Программные формируются при каждом вызове метода write(), в начало такого пакета автоматом добавляется числовое значение, содержащее размер передаваемого программного пакета.
Физические формируются уже на транспортном уровне, чаще всего для них отводится 65 535 байт (его размер хранится в двухбайтовом поле заголовка т.е. само это поле может содержать максимум такое значение), размер может меняться по согласованию сторон, содержится в TCP заголовках.

Непонятно правда как быть с большими файлами. 20гб точно не передает.

:!: Пример: Сервер

sserv.h

 

sserv.cpp

 
:!: Пример: Клиент

Примечания:
:?: Установка соединения (connectToHost()) проходит асинхронно, после ее вызова уже можно писать данные в сокет (write()), данные будут собираться в буфер (неизвестно какое ограничение), и будут ожидать записи.
Правда в случае ошибки соединения, буфер очистится, при проверке, данные были доступны вплоть до последнего сигнала об ошибке, при повторном запуске отправки, буфер уже был очищен.
:?: Запись можно делать из файла сразу в метод write(), без создания дополнительных QByteArray и QDataStream, как было в прошлых примерах.
write() принимает QByteArray, метод read() возвращает QByteArray, для записи служебных данных можно сделать отдельный метод для конвертации, вот тут та и понадобится QDataStream, который корректно запишет нужные данные в QByteArray.

sc.h

 

sc.cpp

 
:!: Пример:
 
develop/qt/network.1633801698.txt.gz · Последнее изменение: 2021/10/09 17:48 — admin