Содержание

Поддержка сети 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(), если писать через 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

 
:!: Пример: