- Работа с сетью в Qt. Сокеты. Паттерн Adapter
- 1. Шаблон проектирования Adapter
- 2. Работа с сетью в Qt. Классы QTcpServer и QTcpSocket
- 3. Пример использования паттерна Adapter
- 4. Исходный код сетевого чата
- Qt Documentation
- Contents
- Public Functions
- Detailed Description
- Member Function Documentation
- QUdpSocket:: QUdpSocket ( QObject *parent = nullptr)
- [virtual] QUdpSocket::
- bool QUdpSocket:: hasPendingDatagrams () const
- bool QUdpSocket:: joinMulticastGroup (const QHostAddress &groupAddress)
- bool QUdpSocket:: joinMulticastGroup (const QHostAddress &groupAddress, const QNetworkInterface &iface)
- bool QUdpSocket:: leaveMulticastGroup (const QHostAddress &groupAddress)
- bool QUdpSocket:: leaveMulticastGroup (const QHostAddress &groupAddress, const QNetworkInterface &iface)
- QNetworkInterface QUdpSocket:: multicastInterface () const
- qint64 QUdpSocket:: pendingDatagramSize () const
- qint64 QUdpSocket:: readDatagram ( char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
- QNetworkDatagram QUdpSocket:: receiveDatagram ( qint64 maxSize = -1)
- void QUdpSocket:: setMulticastInterface (const QNetworkInterface &iface)
- qint64 QUdpSocket:: writeDatagram (const char *data, qint64 size, const QHostAddress &address, quint16 port)
- qint64 QUdpSocket:: writeDatagram (const QNetworkDatagram &datagram)
- qint64 QUdpSocket:: writeDatagram (const QByteArray &datagram, const QHostAddress &host, quint16 port)
Работа с сетью в Qt. Сокеты. Паттерн Adapter
В статье показана работа с сетью на примере очень простого сетевого чата, а также описан никак не связанный с сетью шаблон проектирования адаптер (adapter, wrapper, обертка).
Несмотря на то, что наш чат максимально прост (он не позволяет передавать файлы и оффлайн-сообщения, не хранит историю, передает сообщения не шифрованными и т.д.), мы все же отделим часть, ответственную за работу с сетью. Эта часть будет использовать класс QTcpSocket, интерфейс которого нас не устраивает, в связи с чем, мы применим шаблон проектирования Wrapper.
Сервер чата работает в одном потоке, т.к. целью статьи я ставил описание примера, наиболее просто и наглядно показывающего использование сокетов библиотеки Qt. В одной из следующих статей я возможно опишу многопоточный чат.
1. Шаблон проектирования Adapter
Адаптер относится к структурным шаблонам проектирования и применяется в случаях, когда появляется класс, интерфейс которого нам не подходит по каким-либо причинам. Очевидное решение проблемы — создать новый класс с «правильным» интерфейсом, который будет каким-либо образом делегировать запросы к классу с «плохим» интерфейсом — при этом не угодный нам интерфейс как бы «скрывается». Поэтому шаблон проектирования адаптер также называют оберткой (wrapper).
рис. 1 шаблон проектирования адаптер. Проблема
На рисунке 1 показана типичная ситуация, в которой может быть уместным применение адаптера. Есть некоторый код (Client), который использует экземпляры классов A и B, реализующих общий интерфейс (Interface). В один прекрасный момент, нам потребовалось наравне с A и B использовать класс Adaptee, но он не реализует нужный нам интерфейс.
Вероятно, класс Adaptee достаточно сложен, чтобы нам не пришло в голову переписывать его с новым интерфейсом или лезть в него чтобы «подпилить» интерфейс.
Первый вариант решения такой проблемы заключается в наследовании классов Interface и Adaptee классом Adapter, как показано на рисунке 2. При этом в отношении класса Adaptee используется закрытое наследование, т.к. адаптер не должен предоставлять лишний интерфейс. Такой вариант решения проблемы называется адаптером классов, использует множественное наследование и все вытекающие из него проблемы.
рис. 2 адаптер классов
Другим вариантом, является адаптер объектов, заключающийся в наследовании интерфейса и агрегации адаптируемого класса (рисунок 3). Такое решение является более гибким и не имеет недостатков от множественного наследования, однако, за счет того, что доступ к элементам адаптируемого класса осуществляется через указатель, снижается быстродействие.
рис. 3 адаптер объектов
Пример из диаграмм очень прост и абстрактен, в реальности все бывает совсем иначе — стоит задача не просто заменить функцию bar на foo, а проделать кучу работы по адаптации интерфейса.
В классической книге [1], приводится пример с графическими объектами — в качестве наших классов A и B выступают круги и квадраты, но нам приспичило кроме кругов выводить текст. Выводить текст не так просто, ведь нужна поддержка разных шрифтов и стилей, поэтому принимается решение взять готовое, проверенное решение (чей-нибудь код). Но вот беда — разработчик писал код не под наш редактор и не учел кучу его особенностей, проблему решает адаптер.
2. Работа с сетью в Qt. Классы QTcpServer и QTcpSocket
В библиотеке Qt есть модуль работы с сетью — Qt Network, для подключения которого в файле проекта достаточно добавить одну строку:
Модуль предоставляет ряд полезных классов, среди которых рассмотренный ранее QHttp [2], QTcpSocket и QTcpServer.
Основная задача QTcpServer — отслеживать подключение клиентов. Сервер слушает определенный порт, задаваемый при вызове метода listen. При подключении клиента, вырабатывается сигнал newConnection и создается сокет (QTcpSocket) для обмена данными с клиентом. Получить указатель на сокет можно вызовом nextPendingConnection.
Обмен данными с клиентом осуществляется через сокет. При отключении клиента сокет вырабатывает сигнал disconnected, а при поступлении в сокет данных — сигнал readyRead. Сигнал readyRead вызывается всякий раз, когда новая порция данных поступает в сокет, узнать сколько именно данных доступно для чтения в сокете можно вызовом метода bytesAvailable.
Чтобы данные поступили в сокет, их нужно туда записать, для этого используется метод QTcpSocket::write. Мы можем олдскульно записывать в сокет последовательности байт:
В приведенном примере в сокет записывается столько байт, начиная с адреса переменной i, сколько достаточно для хранения целого числа. На другом конце провода можно будет считать эти байты методом read, но это очень не удобно, поэтому есть другое, более красивое решение с использованием QByteArray и QDataStream.
На листинг 1 приведен код записи строки в сокет. Если помимо строки потребуется записать еще что-нибудь — изменится только 5 строка. Мы пишем в сокет данные блоками, в начале блока идет количество байт. В 5 строке, мы резервируем в начале блока память для будующего размера, пишем в блок остальные данные, в 7 строке устанавливаем указатель записи на начало и в 8 строке помещаем в это начало получившийся размер блока.
При считывании данных с сокета нужно учитывать, что данные могут поступать частями. Никто не гарантирует, что в результате двух вызовов функции write, будет ровно два раза выслан сигнал readyRead — он может быть выслан любое количество раз (даже лишь один раз).
В листинг 2 с сокета считывается ровно одна строка, но если бы считывалось больше данных — изменения коснулись бы только 14 строчки кода. Вечный цикл в этом коде нужен именно потому, что на несколько операций записи в сокет может прийти один сигнал readyRead — обработать же нужно все имеющиеся в сокете данные. При считывании строки, в приведенном примере выработается сигнал message.
3. Пример использования паттерна Adapter
Наш пример адаптера будет не совсем типичным, потому что у нас не будет классов A и B (рис. 3) — мы адаптируем класс не к существующей системе, а к проектируемой.
На рисунке 4 очень грубо показано что именно мы будем делать. В качестве Adaptee будет выступать сокет библиотеки Qt — класс QTcpSocket. Работать с этим сокетом не очень приятно — это все же весьма низкоуровневая штука. В качестве клиента будет выступать чат или сервер, при разработке которых нам бы хотелось иметь более удобный интерфейс — по этой причине мы добавим прослойку в виде адаптера. Внешне это не очень похоже на стандартный адаптер, но он решает те же задачи — интерфейс класса сокета приводится (адаптируется) к желаемому виду.
Хотелось бы, чтобы сокет сообщал о разрыве соединения и новых данных, когда они полностью получены, а также, позволял бы отправлять данные. Для этого интерфейс адаптера должен содержать 2 сигнала и метод (или слот) отправки. Такой интерфейс показан на листинг 3.
Класс адаптера агрегирует сокет и наследует интерфейс. В результате все операции сокета оказываются скрыты за приятным интерфейсом.
Пример был бы более удачным, если чат мог бы получать данные из других источников (помимо TCP-сокета), которые реализовывали бы наш интерфейс. В качестве таких источников могли бы быть например обертки над QUdpSocket или вариации на тему передачи данных в защищенном виде. Наш же пример адаптера по результатам применения получился похожим на фасад.
4. Исходный код сетевого чата
Диаграмма классов (не совсем UML) наших клиента и сервера приведена на рисунке 5. Часть классов мы уже подробно рассмотрели.
рис. 5 диаграмма классов сетевого чата
MainWidget представляет собой главное окно чата, он агрегирует форму, созданную в Qt Designer [4]. Форма содержит 2 поля ввода и кнопку. При щелчке на кнопку вызывается функция sendString класса ClientSocketAdapter, а поле ввода очищается. При получении сигнала message от ClientSocketAdapter, второе поле ввода главной формы дополняется принятой от сервера строкой.
Класс Server агрегирует QTcpServer, а также, список указателей на адаптеры сокетов. При подключении клиента наш сервер получает от QTcpServer указатель на созданный сокет, на основе которого создается адаптер серверного сокета (ServerSocketAdapter) и добавляется в список. Сервер хранит список подключенных клиентов чтобы пересылать между ними сообщения. При получении сообщения от любого клиента, сервер обходит список адаптеров и для каждого вызывает метод sendString. При отключении клиента, адаптер удаляется из списка. Деструктор адаптера обеспечивает корректное освобождение памяти из под сокета (QTcpSocket).
В связи с тем, что клиент и сервер обменивается сообщения в одинаковом формате, адаптер сокета клиента почти не отличается от адаптера сокета сервера, поэтому основную часть кода можно перенести в класс SocketAdapter.
Первое различие между ними заключается в том, что адаптер серверного сокета создается на основе уже имеющегося объекта QTcpSocket, а адаптер сокета клиента должен создать такой объект. Эта разница учтена в конструкторе класса SocketAdapter.
Кроме того, клиентский сокет должен выполнить метод connectToHost чтобы начать диалог с сервером. Сервер таких действий предпринимать не должен.
Полный исходный код клиента и сервера можно скачать бесплатно: клиент-серверный чат Qt.
В следующей статье мы подумаем над распараллеливанием сервера с использованием QThread.
Источник
Qt Documentation
Contents
The QUdpSocket class provides a UDP socket. More.
Header: | #include |
qmake: | QT += network |
Inherits: | QAbstractSocket |
Note: All functions in this class are reentrant.
Public Functions
QUdpSocket(QObject *parent = nullptr) | |
virtual | QUdpSocket() |
bool | hasPendingDatagrams() const |
bool | joinMulticastGroup(const QHostAddress &groupAddress) |
bool | joinMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface) |
bool | leaveMulticastGroup(const QHostAddress &groupAddress) |
bool | leaveMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface) |
QNetworkInterface | multicastInterface() const |
qint64 | pendingDatagramSize() const |
qint64 | readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr) |
QNetworkDatagram | receiveDatagram(qint64 maxSize = -1) |
void | setMulticastInterface(const QNetworkInterface &iface) |
qint64 | writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port) |
qint64 | writeDatagram(const QNetworkDatagram &datagram) |
qint64 | writeDatagram(const QByteArray &datagram, const QHostAddress &host, quint16 port) |
Detailed Description
UDP (User Datagram Protocol) is a lightweight, unreliable, datagram-oriented, connectionless protocol. It can be used when reliability isn’t important. QUdpSocket is a subclass of QAbstractSocket that allows you to send and receive UDP datagrams.
The most common way to use this class is to bind to an address and port using bind(), then call writeDatagram() and readDatagram() / receiveDatagram() to transfer data. If you want to use the standard QIODevice functions read(), readLine(), write(), etc., you must first connect the socket directly to a peer by calling connectToHost().
The socket emits the bytesWritten() signal every time a datagram is written to the network. If you just want to send datagrams, you don’t need to call bind().
The readyRead() signal is emitted whenever datagrams arrive. In that case, hasPendingDatagrams() returns true . Call pendingDatagramSize() to obtain the size of the first pending datagram, and readDatagram() or receiveDatagram() to read it.
Note: An incoming datagram should be read when you receive the readyRead() signal, otherwise this signal will not be emitted for the next datagram.
QUdpSocket also supports UDP multicast. Use joinMulticastGroup() and leaveMulticastGroup() to control group membership, and QAbstractSocket::MulticastTtlOption and QAbstractSocket::MulticastLoopbackOption to set the TTL and loopback socket options. Use setMulticastInterface() to control the outgoing interface for multicast datagrams, and multicastInterface() to query it.
With QUdpSocket, you can also establish a virtual connection to a UDP server using connectToHost() and then use read() and write() to exchange datagrams without specifying the receiver for each datagram.
The Broadcast Sender, Broadcast Receiver, Multicast Sender, and Multicast Receiver examples illustrate how to use QUdpSocket in applications.
Member Function Documentation
QUdpSocket:: QUdpSocket ( QObject *parent = nullptr)
Creates a QUdpSocket object.
parent is passed to the QObject constructor.
[virtual] QUdpSocket::
Destroys the socket, closing the connection if necessary.
bool QUdpSocket:: hasPendingDatagrams () const
Returns true if at least one datagram is waiting to be read; otherwise returns false .
bool QUdpSocket:: joinMulticastGroup (const QHostAddress &groupAddress)
Joins the multicast group specified by groupAddress on the default interface chosen by the operating system. The socket must be in BoundState, otherwise an error occurs.
Note that if you are attempting to join an IPv4 group, your socket must not be bound using IPv6 (or in dual mode, using QHostAddress::Any). You must use QHostAddress::AnyIPv4 instead.
This function returns true if successful; otherwise it returns false and sets the socket error accordingly.
Note: Joining IPv6 multicast groups without an interface selection is not supported in all operating systems. Consider using the overload where the interface is specified.
This function was introduced in Qt 4.8.
bool QUdpSocket:: joinMulticastGroup (const QHostAddress &groupAddress, const QNetworkInterface &iface)
This is an overloaded function.
Joins the multicast group address groupAddress on the interface iface.
This function was introduced in Qt 4.8.
bool QUdpSocket:: leaveMulticastGroup (const QHostAddress &groupAddress)
Leaves the multicast group specified by groupAddress on the default interface chosen by the operating system. The socket must be in BoundState, otherwise an error occurs.
This function returns true if successful; otherwise it returns false and sets the socket error accordingly.
Note: This function should be called with the same arguments as were passed to joinMulticastGroup().
This function was introduced in Qt 4.8.
bool QUdpSocket:: leaveMulticastGroup (const QHostAddress &groupAddress, const QNetworkInterface &iface)
This is an overloaded function.
Leaves the multicast group specified by groupAddress on the interface iface.
Note: This function should be called with the same arguments as were passed to joinMulticastGroup().
This function was introduced in Qt 4.8.
QNetworkInterface QUdpSocket:: multicastInterface () const
Returns the interface for the outgoing interface for multicast datagrams. This corresponds to the IP_MULTICAST_IF socket option for IPv4 sockets and the IPV6_MULTICAST_IF socket option for IPv6 sockets. If no interface has been previously set, this function returns an invalid QNetworkInterface. The socket must be in BoundState, otherwise an invalid QNetworkInterface is returned.
This function was introduced in Qt 4.8.
qint64 QUdpSocket:: pendingDatagramSize () const
Returns the size of the first pending UDP datagram. If there is no datagram available, this function returns -1.
qint64 QUdpSocket:: readDatagram ( char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr)
Receives a datagram no larger than maxSize bytes and stores it in data. The sender’s host address and port is stored in *address and *port (unless the pointers are nullptr ).
Returns the size of the datagram on success; otherwise returns -1.
If maxSize is too small, the rest of the datagram will be lost. To avoid loss of data, call pendingDatagramSize() to determine the size of the pending datagram before attempting to read it. If maxSize is 0, the datagram will be discarded.
QNetworkDatagram QUdpSocket:: receiveDatagram ( qint64 maxSize = -1)
Receives a datagram no larger than maxSize bytes and returns it in the QNetworkDatagram object, along with the sender’s host address and port. If possible, this function will also try to determine the datagram’s destination address, port, and the number of hop counts at reception time.
On failure, returns a QNetworkDatagram that reports not valid.
If maxSize is too small, the rest of the datagram will be lost. If maxSize is 0, the datagram will be discarded. If maxSize is -1 (the default), this function will attempt to read the entire datagram.
This function was introduced in Qt 5.8.
void QUdpSocket:: setMulticastInterface (const QNetworkInterface &iface)
Sets the outgoing interface for multicast datagrams to the interface iface. This corresponds to the IP_MULTICAST_IF socket option for IPv4 sockets and the IPV6_MULTICAST_IF socket option for IPv6 sockets. The socket must be in BoundState, otherwise this function does nothing.
This function was introduced in Qt 4.8.
qint64 QUdpSocket:: writeDatagram (const char *data, qint64 size, const QHostAddress &address, quint16 port)
Sends the datagram at data of size size to the host address address at port port. Returns the number of bytes sent on success; otherwise returns -1.
Datagrams are always written as one block. The maximum size of a datagram is highly platform-dependent, but can be as low as 8192 bytes. If the datagram is too large, this function will return -1 and error() will return DatagramTooLargeError.
Sending datagrams larger than 512 bytes is in general disadvised, as even if they are sent successfully, they are likely to be fragmented by the IP layer before arriving at their final destination.
Warning: Calling this function on a connected UDP socket may result in an error and no packet being sent. If you are using a connected socket, use write() to send datagrams.
qint64 QUdpSocket:: writeDatagram (const QNetworkDatagram &datagram)
This is an overloaded function.
Sends the datagram datagram to the host address and port numbers contained in datagram, using the network interface and hop count limits also set there. If the destination address and port numbers are unset, this function will send to the address that was passed to connectToHost().
If the destination address is IPv6 with a non-empty scope id but differs from the interface index in datagram, it is undefined which interface the operating system will choose to send on.
The function returns the number of bytes sent if it succeeded or -1 if it encountered an error.
Warning: Calling this function on a connected UDP socket may result in an error and no packet being sent. If you are using a connected socket, use write() to send datagrams.
This function was introduced in Qt 5.8.
qint64 QUdpSocket:: writeDatagram (const QByteArray &datagram, const QHostAddress &host, quint16 port)
This is an overloaded function.
Sends the datagram datagram to the host address host and at port port.
The function returns the number of bytes sent if it succeeded or -1 if it encountered an error.
В© 2021 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.
Источник