У статті показана робота з мережею на прикладі дуже простого мережевого чату, а також описаний ніяк не пов'язана з мережею шаблон проектування адаптер (adapter, wrapper, обгортка).
Незважаючи на те, що наш чат максимально простий (він не дозволяє передавати файли і оффлайн-повідомлення, не зберігає історію, передає повідомлення не шифрованими і т.д.), ми все ж відділимо частина, відповідальну за роботу з мережею. Ця частина буде використовувати клас QTcpSocket, інтерфейс якого нас не влаштовує, в зв'язку з чим, ми застосуємо шаблон проектування Wrapper.
Сервер чату працює в одному потоці, тому що метою статті я ставив опис прикладу, найбільш просто і наочно показує використання сокетів бібліотеки Qt. В одній з наступних статей я можливо опишу багато-чат.
- шаблон проектування Adapter;
- робота з мережею в Qt. Класи QTcpServer і QTcpSocket;
- приклад використання патерну Adapter;
- вихідний код мережевого чату.
1. Шаблон проектування Adapter
Адаптер відноситься до структурних шаблонів проектування і застосовується у випадках, коли з'являється клас, інтерфейс якого нам не підходить з якихось причин. Очевидне рішення проблеми - створити новий клас з "правильним" інтерфейсом, який буде якимось чином делегувати запити до класу з "поганим" інтерфейсом - при цьому не угодний нам інтерфейс як би "ховається". Тому шаблон проектування адаптер також називають обгорткою (wrapper).
Мал. 1 шаблон проектування адаптер. проблема
На малюнку 1 показана типова ситуація, в якій може бути доречним застосування адаптера. Є деякий код (Client), який використовує екземпляри класів A і B. реалізують спільний інтерфейс (Interface). В один прекрасний момент, нам треба було нарівні з A і B використовувати клас Adaptee. але він не реалізує потрібний нам інтерфейс.
Ймовірно, клас Adaptee досить складний, щоб нам не спало на думку переписувати його з новим інтерфейсом або лізти в нього щоб "підпиляти" інтерфейс.
Перший варіант вирішення такої проблеми полягає в спадкуванні класів Interface і Adaptee класом Adapter. як показано на малюнку 2. При цьому відносно класу Adaptee використовується закрите спадкоємство. тому адаптер не повинен надавати зайвий інтерфейс. Такий варіант вирішення проблеми називається адаптером класів, використовує множинне успадкування і все що випливають з нього проблеми.
Мал. 2 адаптер класів
Іншим варіантом, є адаптер об'єктів, що полягає в спадкуванні інтерфейсу і агрегації адаптируемого класу (малюнок 3). Таке рішення є більш гнучким і не має недоліків від множинного спадкоємства. однак, за рахунок того, що доступ до елементів адаптируемого класу здійснюється через покажчик, знижується швидкодія.
Мал. 3 адаптер об'єктів
Приклад з діаграм дуже простий і абстрактний, в реальності все буває зовсім інакше - стоїть завдання не просто замінити функцію bar на foo. а виконати купу роботи з адаптації інтерфейсу.
2. Робота з мережею в Qt. Класи QTcpServer і QTcpSocket
У бібліотеці Qt є модуль роботи з мережею - Qt Network. для підключення якого в файлі проекту досить додати один рядок:
Модуль надає ряд корисних класів, серед яких розглянутий раніше QHttp [2], QTcpSocket і QTcpServer.
Основне завдання QTcpServer - відстежувати підключення клієнтів. Сервер слухає певний порт. задається при виклику методу listen. При підключенні клієнта, виробляється сигнал newConnection і створюється сокет (QTcpSocket) для обміну даними з клієнтом. Отримати покажчик на сокет можна викликом nextPendingConnection.
Обмін даними з клієнтом здійснюється через сокет. При відключенні клієнта сокет виробляє сигнал disconnected. а при надходженні в сокет даних - сигнал readyRead. Сигнал readyRead викликається всякий раз, коли нова порція даних надходить в сокет, дізнатися скільки саме даних є для читання в сокеті можна викликом методу bytesAvailable.
Щоб дані надійшли в сокет, їх потрібно туди записати, для цього використовується метод QTcpSocket :: write. Ми можемо олдскульний записувати в сокет послідовності байт:
На лістинг 1 наведено код записи рядки в сокет. Якщо крім рядка потрібно записати ще що-небудь - зміниться лише 5 рядок. Ми пишемо в сокет дані блоками, на початку блоку йде кількість байт. У 5 рядку. ми резервуємо на початку блоку пам'ять для прийдешнього розміру, пишемо в блок інші дані, в 7 рядку встановлюємо покажчик записи на початок і в 8 рядку поміщаємо в це початок вийшов розмір блоку.
При зчитуванні даних з сокета потрібно враховувати, що дані можуть надходити частинами. Ніхто не гарантує, що в результаті двох викликів функції write. буде рівно два рази висланий сигнал readyRead - він може бути висланий будь-яку кількість разів (навіть лише один раз).
У лістинг 2 з сокета зчитується рівно один рядок, але якби зчитувалося більше даних - зміни торкнулися б тільки 14 рядки коду. Вічний цикл в цьому коді потрібен саме тому, що на кілька операцій запису в сокет може прийти один сигнал readyRead - обробити ж потрібно всі наявні в сокеті дані. При зчитуванні рядки, в наведеному прикладі виробиться сигнал message.
3. Приклад використання патерну Adapter
Наш приклад адаптера буде не зовсім типовим, тому що у нас не буде класів A і B (рис. 3) - ми адаптуємо клас ні до існуючої системи, а до проектованої.
Мал. 4 незвичайний адаптер
На малюнку 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.
Список корисних джерел
Post navigation
7 thoughts on "Робота з мережею в Qt. Сокети. Патерн Adapter "
Фича в додатку.
При закритті всіх клієнтів сервер продовжує роботу.
Тобто припинити роботу сервера виходить тільки через диспетчер задач.
Це не фіча. Сервер як би від клієнтів не залежить, він може розташовуватися на іншій машині (десь в інтернеті).
Вихід клієнтів з чату не повинен призводити до закриття сервера. Зараз все вийшли, але через хвилину хтось зайде (спробує підключитися) і що? - сервер на вашу повинен виявитися недоступним, тому що до цього всі вийшли?
Володимир, добрий день.
1. Чому ви не використовуєте перевизначення метод incomingConnection (int)?
2. Ну і чому замість #include вживається class QTcpSocket. - Просто як стиль написання?
Чому ви не використовуєте перевизначення метод incomingConnection (int)
На стороні сервера (де з'являються нові сполуки) я використовую сигнал QTcpServer :: newConnection ():
connect (m_ptcpServer, SIGNAL (newConnection ()), SLOT (on_newConnection ()));
Цей сигнал виробляється щоразу, коли з'являється нове з'єднання. З документації:
This signal is emitted every time a new connection is available.
QTcpServer :: incomingConnection є віртуальною функцію, яка також викликається при появі нового з'єднання. У документації написано, що ця функція реалізується в QTcpServer так, що вона додає сокет (з'єднання) в список і виконує emit newConnection (). Змінювати її варто лише у випадках, якщо нам потрібно іншу поведінку, якщо ж нам потрібен лише сигнал про те, що з'явилося нове з'єднання - потрібно завжди використовувати сигнал QTcpServer :: newConnection ().
Змінювати її варто лише у випадках, якщо нам потрібно іншу поведінку, якщо ж нам потрібен лише сигнал про те, що з'явилося нове з'єднання - потрібно завжди використовувати сигнал QTcpServer :: newConnection ()
А що мається на увазі під іншим поведінкою. Ну просто адже сигнал прийшов і далі вже почали описувати те що нам потрібно, наприклад порахували щось на сервері і передали клієнту. Як би все непереборної залежать від того є сигнал чи ні. Ну тобто є сигнал ми в будь-якому випадку щось робимо.
В яких випадках в нашому укладення потрібно застосовувати incomingConnection (int)? Ну просто приклад.
2) Застосування
Буває щось роблю на Arduino. Зараз замовив wi-fi модулі ESP-8266 для Arduino. Також збираюся замовити камеру щоб в цю ESP-8266 вставити і отримувати інформацію на ПК. В принципі, сама Arduino і не потрібна, тому що все робить ESP-8266 (тим більше туди можна Arduino IDE залити для простоти роботи). Для зв'язку з wi-fi і стане в нагоді Ваша розробка. Дякуємо.
Ще хочу opencv на ПК використовувати, щоб якось обробляти отримується зображення.