====== Поддержка сети Qt ======
**qt += network**\\
Для реализации серверного функционала используется класс **QTcpServer**.\\
Для непосредственного приема передачи используются **сокеты** (базовый класс **QAbstractSocket**), есть доп реализации **Tcp, Udp, Ssl** и т.д. Собсна клиент содержит в себе только объект сокета.\\
Для сетевых запросов существует класс **QNetworkAccessManager**.\\
===== QTcpServer =====
Позволяет принимать Tcp-соединения, порт можно выбрать либо авто, IP так же можно выбрать конкретный либо все IP хоста.\\
Вызовите **listen()**, для начала прослушивания. **close()** для завершения.\\
При подключении клиента, вызывается сигнал **newConnection()**. Вызовите **nextPendingConnection()** что бы **принять соединение** как **QTcpSocket**, который можно использовать для связи с клиентом.\\
В случае ошибки, **serverError()** возвращает тип ошибки, а **errorString()** вернут описание.\\
Сервер в основном предназначен для использования сигнало/слотами, имеются методы для блокирующей работы, типа "waitFor..".\\
===== QAbstractSocket =====
{{ :develop:qt:qnet_0.png?direct&300|}}
Является базовым классом для **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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class sserv : public QDialog
{
Q_OBJECT
QLabel *labStatusText;
QTcpServer *tcpServer;
//QVector 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 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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()**, если писать через **QDataStream** в **QByteArray**, то перед каждой записью в датастрим будет автоматом добавлено интовое значение, с указанием размера записываемых данных.\\
**Физические** формируются уже на транспортном уровне, чаще всего для них отводится 65 536 байт (его размер хранится в двухбайтовом поле заголовка т.е. само это поле может содержать максимум такое значение), размер может меняться по согласованию сторон, содержится в TCP заголовках.\\
Передача ведется потоковая, сначала идет служебная инфа, с указанием размера в т.ч., затем все остальное принимаем как содержимое файла, пока не достигнем указанный размер файла.\\
Служебную инфу пишем с помощью датаСтрим, что бы был указан размер этого "программного" пакета, и чтобы нам вырезать точное кол-во байт, занимаемое этой служебной инфой, содержимое файла просто читаем пока не достигнем указанного размера.\\
Cчитывание 4ех байт остается корректным до тех пор пока программный пакет < 4gb, т.к. в эти 4 байта умещено интовое значение, которое принимает максимум 4 млрд.. байтов ~ 4gb.\\
Если программный пакет будет больше, мы просто некорректно считаем значение его размера и следовательно сам пакет не сможем принять (будет '-1')\\
:!: Пример: Сервер
sserv.h
sserv.cpp
:!: Пример: Клиент
**Примечания:**\\
:?: Установка соединения (**connectToHost()**) проходит асинхронно, после ее вызова уже можно писать данные в сокет (**write()**), данные будут собираться в буфер (**неизвестно какое ограничение**), и будут ожидать записи.\\
Правда в случае ошибки соединения, буфер очистится, при проверке, данные были доступны вплоть до последнего сигнала об ошибке, при повторном запуске отправки, буфер уже был очищен.\\
:?: Запись можно делать из файла сразу в метод **write()**, без создания дополнительных **QByteArray** и **QDataStream**, как было в прошлых примерах.\\
**write()** принимает **QByteArray**, метод **read()** возвращает **QByteArray**, для записи служебных данных можно сделать отдельный метод для конвертации, вот тут та и понадобится **QDataStream**, который корректно запишет нужные данные в **QByteArray**.\\
:?: Что касается **проверки и установления** коннекта:\\
в нашем случае сокет может быть в след состояниях:\\
**0** - не подключен - нужно **вызвать коннект**\\
**1,2** - выполняет поиск и начал устанавливать соединение - соединения еще нет, но и делать вроде ничего не надо, правда неизвестно может ли оно быть зависшим в этот состоянии ?, думаю тут- **ничего не делать**\\
**3** - установленно - **ничего не делать**\\
**6** - вот вот закроется - т.е. запрошен дисконнект, ожидается отправка очереди данных и коннект захлопнется, в таком случае явно нужно снова **вызвать коннект**\\
Что касается отправки файла, пишем данные кусками в метод **write()**, он возвращает кол-во записанных байт, это кол-во отнимаем от размера файла, так управляем циклом\\
В случае ошибки, **write()** возвращает **-1** и все идет по п#зде, поэтому необходимо проверять значение пред тем как с ним работать (отнимать от общем суммы)\\
Так же, после вызова **write()** и до фактической отправки данных нужно какое то время, до этого данные копятся в буфере (видно в **bytesToWrite()**), если писать в цикле то данные **не успевают** уходить в **заполняют буфер** до талого, поэтому нужен метод **wait()**, либо сигнало/слот можно прикрутить какой нибудь\\
Данные **пишутся** так же **пакетами**, как были записаны в **write()**, по одному после каждого **wait()**\\
Если нет коннекта, данные не уходят, **bytesToWrite()** не очищается, последующие вызовы **write()** возвращают **-1**, **wait()** возвращает **false**\\
sc.h
sc.cpp
:!: Пример: