====== Поддержка сети 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
:!: Пример: