Пример использования gSOAP

Пример использования gSOAP

Мы уже занимались разработкой клиент-серверного приложения в Qt. Напомню, что тогда мы использовали библиотеку LibQxt и реализовали простой текстовый чат. То решение было удобным, но оно оказалось привязано к используемой библиотеке. Поэтому, например, реализация совместимого чат-клиента на другом языке программирования или с другим набором библиотек хоть возможна, но выглядит уже не так просто и удобно, как это казалось сначала.

Одна из возможных альтернатив заключается в использовании стандартных сетевых протоколов. Примером такого протокола является SOAP. Главной его особенностью является то, что он полностью основан на XML. Это является одновременно и преимуществом, и недостатком. Преимущество кроется в том, что с XML достаточно легко работать. Однако недостаток также упирается в использование XML: он избыточен, то есть приходится передавать много лишних данных. Но с учетом всех плюсов и минусов веб-службы на основе SOAP все равно создаются и используются. О их реализации на C++ с помощью gSOAP мы и поговорим.

Прежде чем начать

Общепринятым форматом описания протокола SOAP-служб является WSDL, который также основан на XML. Однако в этой заметке мы не будем вдаваться в его подробности, поскольку gSOAP позволяет обойтись без него. Более того, он сам может сгенерировать WSDL-файл. Но как он поймет, что мы хотим? — Об этом вы скоро узнаете, а пока что займемся подготовкой.

Сам по себе gSOAP включает определенный набор утилит и библиотек. Утилиты предназначены для автоматической генерации каркасов приложений, а библиотеки берут на себя все вопросы, связанные с сетевым взаимодействием и сериализацией данных. Таким образом, нам остается только предоставить описание протокола и реализовать обработку запросов на стороне сервера. На клиентской стороне и вовсе все уже готово. Бери и пользуйся. Звучит неплохо, но где взять gSOAP? У меня установлен Archlinux и в его репозиторий входит пакет gsoap. Поэтому мне было достаточно вызвать yaourt -S gsoap. Если вы используете другой дистрибутив Linux, то вполне возможно, что и в его репозиториях завалялся этот пакет. Если же пакета не нашлось или вы пользуетесь Windows, то gSOAP можно просто скачать отсюда. Когда вы скачаете и распакуете архив, то увидите, что для Windows и MacOS уже предусмотрены бинарные исполняемые файлы утилит, которые нам понадобятся. Однако для Linux требуется ручная сборка. Самым простым вариантом будет воспользоваться командами ./configure && make. В результате будут собраны некоторые статические библиотеки и необходимые утилиты. При желании вы можете установить все это в свои системные папки с помощью make install, но я этого делать не буду, поскольку в моем дистрибутиве пакет gsoap уже установлен из репозитория. Если у вас что-то не получилось или произошли ошибки компиляции, то рекомендую заглянуть в файл INSTALL.txt в корневом каталоге. На 146% уверен, что там вы найдете решение проблемы.

Будем считать, что предыдущий этап пройден нормально. Для Windows и MacOS утилиты gSOAP расположены в соответствующих подкаталогах ./gsoap/bin/. Если же вы собирали gsoap в Linux, то одна из этих утилит окажется в ./gsoap/src/, а другая в ./gsoap/wsdl/. Речь идет об исполняемых файлах soapcpp2 и wsdl2h. Последний в этой заметке нам не понадобится. Он позволяет компоновать каркасы SOAP-сервера и SOAP-клиента на основе WSDL-описания протокола. Другая утилита soapcpp2 также позволяет формировать каркасы приложений, но не из WSDL, а из заголовочного h-файла особого формата. Кстати, из этого же h-файла утилита soapcpp2 генерирует и WSDL-файл. А теперь посмотрим на то, как этим всем пользоваться.

Создаем протокол веб-службы

В качестве примера веб-службы разработаем простой сервис, который позволяет сохранять и загружать файлы. Принцип его использования будет заключаться в том, что клиент передает серверу запрос, в котором указывает имя файла и его содержимое в бинарное форме. Сервер каким-то образом сохраняет этот файл, связывая его с указанным именем. Затем клиент может отправить запрос на сервер, чтобы получить сохраненный ранее файл по его имени. Сервер ищет этот файл и возвращает его содержимое, если он существует.

Напишем заголовочный файл с описанием предложенного протокола в формате, понятном gSOAP:

В первых четырех строках расположены комментарии, предназначенные для gSOAP. Он их не пропустит, а интерпретирует определенным образом.

В первой строке мы определили имя нашей веб-службы, которую назвали FileSaver. Обратите внимание, что это определение привязывается к префиксу пространства имен ns. Это необходимо, чтобы в дальнейшем избежать конфликтов имен на уровне SOAP-протокола в рамках WSDL. Вы можете выбрать любой префикс, однако он не имеет особого значения и часто используются формальные варианты на подобии: ns, ns1, ns2 и т.д.

Во второй строке мы просто связываем префикс ns с фактическим пространством имен нашей веб-службы urn:FileSaver. Поэтому все дальнейшие объявления в этом заголовочном файле, начинающиеся с префикса ns, за которым следует два символа подчеркивания (то есть ns__), gSOAP отнесет к пространству имен urn:FileSaver. Все это достаточно формально, но необходимо.

Следующие две строки комментариев определяют способ кодирования сообщений при их передаче по нашему протоколу в формате XML. Поскольку всю работу берет на себя gSOAP, то мы не будем вдаваться в подробности. Отметим лишь то, что для параметра style мы можем выбрать варианты rpc и document, а для encoding доступны значения encoded и literal. Причем, их можно комбинировать во всех сочетаниях. Мы остановились на достаточно распространенной комбинации rpc/encoded.

Далее следует typedef, в котором мы определяем связь между строковым WSDL-типом xsd:string и строковым типом C++ std::wstring. Мы выбрали именно wstring, поскольку собираемся обеспечить передачу имен файла, для которых требуется поддержка Юникода. Хотя использовать кодировку UTF-8 мы могли бы еще с помощью:

Но этот вариант будет не так удобно использовать в коде приложений.

Затем мы определили перечисление ns__ErrorCode, содержащее коды ошибок, которые сервер будет возвращать клиентам, чтобы сообщить о возможных проблемах.

Следующее объявление класса xsd__base64Binary является стандартным и взято без изменений из официальной документации gSOAP. Этот тип нам понадобится для передачи содержимого файлов.

Класс ns__loadFileResponse нужен нам по той причине, что в запросе ns__loadFile() мы собираемся вернуть сразу два параметра: содержимое файла fileData и код ошибки errorCode. Чтобы рассказать gSOAP о наших намерениях, требуется обернуть весь набор выходных параметров в структуру, которая будет единственных возвращаемым значением.

Наконец мы дошли до самих запросов. В них нет ничего сложного. Для ns__saveFile() на вход мы ожидаем получение имени файла и его содержимого, а на выход отправляем код ошибки. В этом случае нам ничего оборачивать самим не пришлось, поскольку gSOAP и так поймет, что первые два параметра являются входными, а последний возвращаемым. Для ns__loadFile() мы ожидаем получения имени файла, а возвращаем нашу структуру ns__loadFileResponse, в которой передаем содержимое загруженного файла и код ошибки.

С протоколом мы разобрались вполне безболезненно. Сохраните где-нибудь этот файл под именем file_saver_service.h и мы можем приступать к созданию приложений, которые будут обмениваться сообщениями на его основе.

Подготовка проекта

Пришло время создать проект. Поскольку мы будем писать реализацию на C++ с использованием библиотеки Qt, то и проект скомпонуем на базе qmake (см. Структура Qt-проекта на C++). Посмотрим на дерево проекта, который я подготовил:

В папку bin/, как обычно, будут помещаться исполняемые файлы. В import/ мы создаем подкаталог gsoap/, а в него копируем файл stdsoap2.cpp из пакета gsoap, который мы скачивали в самом начале из интернета. Этот файл предоставляет реализацию низкоуровневых функций gSOAP, поэтому мы вынуждены подключать его к нашим проектам явно. Другой выход заключается в использовании статической библиотеки libgsoap++, которую вы могли собрать командой ./configure && make. Но мы не будем ничего усложнять и просто добавим stdsoap2.cpp в список исходников для наших проектов. И наконец каталог src/. В нем мы заранее создаем подкаталог FileSaverClient/ и FileSaverServer/, в которые поместим реализацию клиентской и серверной частей. А также не забудьте про подкаталог src/include/. В него мы положим единственный заголовочный файл shareddefs.h, где определим одну вспомогательную функцию, которая будет использовать как на сервере, так и на клиенте.

Главный pro-файл помещаем в корневой каталог проекта. Я назвал его FileSaverService.pro и определил для него шаблон subdirs:

Кроме того, в корневом каталоге проекта я создал файл common.pri, который мы подключим в подпроектах серверной и клиентской реализации:

Здесь все просто. Отметим лишь то, что мы расширили INCLUDEPATH, чтобы проекты видели наш shareddefs.h; добавили к SOURCES файл stdsoap2.cpp, по причине указанной выше, а также определили единый путь DESTDIR, в который будут помещены готовые бинарники приложений.

Отлично. Пора заняться серверной стороной.

Реализуем SOAP-сервер

Предполагается, что к этому моменту у вас уже должен быть рабочий набор утилит gSOAP. Сейчас нам понадобится soapcpp2. Эта утилита не имеет графического интерфейса и использовать ее придется через консоль. Выполните команду, подобную следующей, в терминале вашей операционной системы:

Исполняемый файл soapcpp2 должен быть виден для вашей оболочки терминала, поэтому либо запускайте команду из каталога, в котором расположен этот файл, либо пропишите его местоположение в переменную окружения PATH. Не думаю, что у вас могут возникнуть с этим проблемы. Ключ -i означает, что мы хотим получить классы C++, а не код на чистом C. А указав -S мы говорим, что нас пока что интересует только серверная часть. Последним аргументом мы просто передаем путь к заголовочному файлу с описанием протокола нашей веб-службы, которым мы ранее занимались.

После выполнения команды в том каталоге, из которого вы ее запустили, должен появиться набор файлов. Обратите внимание, что среди них есть и WSDL-файл FileSaver.wsdl с описанием нашего протокола на XML. Можете в него заглянуть и почитать, но сейчас мы на нем останавливаться не будем. Однако он вам понадобится, если вы решите распространить описание протокола, чтобы другие разработчики смогли написать клиентский код для взаимодействия с вашей веб-службой.

Для реализация сервера из созданных файлом нам понадобятся:

  1. FileSaver.nsmap — файл с описанием пространств имен;
  2. soapC.cpp и soapH.h — файлы с реализацией низкоуровневых функций работы протокола, которые для вас уже подготовил gSOAP;
  3. soapStub.h — файл с определением классов, структур и типов данных, которые используются в нашей веб-службе;
  4. soapFileSaverService.cpp и soapFileSaverService.h — файлы с определением базового класса сервера, который мы будем наследовать и наполнять функционалом.

Необходимо скопировать эти файлы в подкаталог проекта src/FileSaverServer/. Нашу реализацию класса с обработчиком запросов, определенных в протоколе, мы назовем FileSaverServiceImpl и разместим ее в файлах filesaverserviceimpl.(h|cpp). Также нам понадобится вспомогательный класс для запуска нашего сервера. Его мы поместим в файле SOAPServiceController.h. Само собой, не забудьте про файл main.cpp. В итоге получается следующее описание pro-файла, которое должно лежать в src/FileSaverServer/:

Здесь мы подключили все подготовленные нами файлы, а также файлы, созданные с помощью gSOAP. Кроме того, мы определили макрос WITH_PURE_VIRTAUL. Он нужен для того, чтобы виртуальные функции-члены, описанные в базовом классе, который для нас подготовил gSOAP, имели определение чисто виртуальных и не требовали реализации. Кроме того, мы подключили файл common.pri, который создали в предыдущем разделе.

Начнем с реализации класса FileSaverServiceImpl. Вот содержимое заголовочного файла filesaverserviceimpl.h:

Он должен наследовать класс FileSaverSerivce, который сгенерировал gSOAP. В нем мы определили конструктор по умолчанию и конструктор копирования, а также 3 виртуальных функции из базового класса: copy(), которая реализует паттерн Прототип и скоро нам понадобится, и saveFile(), loadFile(), которые мы определяли сами в заголовочном файле file_saver_service.h.

Реализация конструкторов выглядит следующим образом:

Думаю, что этот код не требует пояснений. Функция copy() не намного сложнее:

А теперь мы дошли до чего-то более интересного. Функция saveFile():

Сохранять присланные файлы мы будем прямо в файловой системе. Это крайне неэффективное и небезопасное решение, но для целей демонстрации вполне подойдет. В реальном приложении подобную функциональность мы бы создали на основе базы данных. Думаю, что в какой-то из будущих заметок мы еще вернемся к этому вопросу.

Обратите внимание, что в этой функции мы возвращаем значение SOAP_OK типа int. Это константа, которая определена в gSOAP и позволяет возвращать коды ошибок на уровне SOAP-протокола. Однако мы предусмотрели свои коды ошибок ns__ErrorCode, поэтому ими и будем пользоваться. Например, если файл fileName не удалось открыть на запись, то мы записываем в errorCode значение ns__COULD_NOT_OPEN_FILE и возвращаем завершаем работу возвратом SOAP_OK.

Класс xsd__base64Binary мы помним еще из заголовочного файла протокола. Мы сами определили его поля __ptr и __size. Как понятно по названию полей, в __ptr содержится указатель на обычный массив unsigned char*, а в __size находится его размер в байтах. Зная все этого, очень легко скомпоновать объект QByteArray и сохранить данные в открытом ранее файле.

Обратите внимание, что мы не делали никаких проверок переданного имени файла. Оно может оказаться пустым или указывать на какой-то системный путь, что никуда не годится. Поэтому будьте аккуратны и рассматривайте такую реализацию лишь в качестве упрощенной демонстрации.

Теперь пришло время заняться реализацией функции loadFile(). Вот она:

Этот код тоже использует лишь общие принципы работы с файлами в Qt без особых проверок, поэтому безопасным его назвать нельзя. Но обратите внимание на функцию initBase64(). Такой функции gSOAP не предоставляет и мы должны написать ее сами. Она просто записывает данные из объекта QByteArray в объект xsd__base64Binary. Кроме того, обратите внимание, что во втором параметры мы передаем указатель на this. Но все становится понятно, когда мы смотрим на реализацию initBase64() из файла src/include/shareddefs.h:

Сначала мы выделяем память на массив __ptr с помощью soap_malloc(). Первым аргументом мы передаем в эту функцию указатель на объект типа soap, который будет отвечать за автоматическое освобождение памяти. Во втором аргументе передается размер массива в байтах. Но как и в случае стандартной C-функции malloc(), вернется void*, который мы должны сами привести к нужному типу, что мы и делаем с помощью static_cast. Это был единственный новый момент. Дальше все по стандарту. Мы просто копируем содержимое из QByteArray в только что созданный массив и указываем значение __size.

С реализацией обработчиков сообщений на стороне сервера мы закончили. Приведу окончательное содержание файла filesaverserviceimpl.cpp:

Обратим внимание на директиву #include "FileSaver.nsmap" в самом верху. Не забудьте про нее, иначе на этапе линковки полезут ошибки сборки.

Теперь мы бы могли достаточно легко реализовать cgi-приложение, которые бы запускал сторонний веб-сервер, например Apache. Но мы пойдем более сложным путем и реализуем Standalone-приложение, которое возьмет на себя функции приема и отправки сообщений клиентам. Но даже здесь большую часть за нас уже сделал gSOAP. Чтобы иметь возможность обрабатывать сразу несколько запросов одновременно, реализация сервера должна быть многопоточной. За основу мы возьмем QThreadPool, который здесь идеально подходит. Я уже писал про многопоточность в Qt и про использование QThreadPool, поэтому если сомневаетесь или дальше вам будет что-то непонятно, то советую ознакомиться с тем материалом.

Посмотрим на реализацию, которая у меня получилась:

Основным классом в пространстве имен SOAPServiceController является Controller. Он, впрочем как и остальные классы, представляет собой шаблонный класс, параметром которого служит тип сервиса Service. Это дает нам возможность повторного его использования для любых SOAP-сервисов, созданных с помощью gSOAP. В конструкторе ему передается указатель на сервис, которым он должен управлять. Кроме того, он имеет функцию-член start(), которая запускает на выполнение в потоке объект задачи AcceptTask с помощью QThreadPool::start(). Неплохо было бы предусмотреть еще и возможность сделать stop(), но пока что я решил не вносить лишних усложнений.

Разберемся с тем, как работает AcceptTask:

После минимальных проверок происходит вызов бесконечного цикла, в котором мы начинаем слушать сообщения от клиентов с помощью gSOAP-функции accept(). Если подключение оказывается некорректным, то есть если проверка soap_valid_socket() не проходит, то мы снова уходим на прослушивание порта. Если же все нормально, то мы можем определить ip, с которого было принято соединение. Здесь мы эту информацию никак не учитываем, но могли бы делать какие-то дополнительные проверки или просто заносить информацию в лог-файл. Затем мы дублируем экземпляр сервиса с помощью copy() и отправляем эту копию для обработки в задачу ServeTask, выделяя для нее отдельный поток в QThreadPool, а сами вновь возвращаемся к приему сообщений от клиентов.

Вот что мы видим в ServeTask:

А здесь все предельно просто. Нам достаточно вызвать serve() сервиса и все остальное gSOAP сделает сам. В этом месте и произойдет вызов обработчиков saveFile() и loadFile(), которые мы заготовили раньше.

Теперь осталось лишь собрать все вместе в файле main.cpp и наконец запустить нашу веб-службу:

Первым делом мы создаем экземпляр реализации нашего сервиса. Затем устанавливаем значение флага bind_flags, чтобы не ждать освобождения порта при перезапуске приложения. Если этого не сделать, то какое-то время порт будет недоступен и функция bind(), которая вызывается дальше, вернет ошибку. В саму функцию bind() мы передаем хост и порт, на котором сервер должен принимать соединения от клиентов. В последнем параметре bind() мы указываем максимальный размер очереди из запросов, чтобы в случае высокой нагрузки на сервер не терять клиентов, а просто просить их немного подождать в очереди. Если вызов bind() заканчивается неудачей, то мы завершаем работу приложения. Например, это может произойти, если вы попытаетесь привязать сервис к порту, который уже занят и в этот момент используется другим приложением. Если же все прошло нормально, то мы создаем контроллер сервиса, который недавно разработали, и запускаем его с помощью start().

Вот и все. Серверная часть готова. Теперь нам нужны клиенты.

Реализуем SOAP-клиент

Каркас клиента генерируется той же утилитой soapcpp2:

Здесь мы видим единственное отличие. Оно заключается в том, что вместо параметра -S, мы передали параметр -C. Он говорит утилите, что теперь мы хотим получить клиентские классы. В результате будет создан практически такой же набор файлов, как и для сервера, но вместо soapFileSaverService.cpp и soapFileSaverService.h мы получим soapFileSaverProxy.cpp и soapFileSaverProxy.h. Скопируем эти и другие файлы в соответствующий подкаталог проекта src/FileSaverClient/.

В качестве клиентского приложения мы создадим небольшую программу с графическим интерфейсом, которая позволит сохранять и загружать файлы используя наш веб-сервис. Для большей наглядности ограничим работу с файлами изображений, чтобы видеть, что все работает.В результате получился следующий pro-файл:

В файле main.cpp все крайне просто и стандартно:

Описание класса виджета не намного сложнее:

Посмотрим на соответствующую реализацию:

В конструкторе мы просто компонуем UI виджета. Нам понадобится один лейбл для отображения изображений, одно текстовое поле для ввода имени файла, еще один лейбл для отображения кода ошибки и две кнопки Save и Load.

Разберемся со слотом onSave(). Сначала мы просим пользователя выбрать файл изображения с помощью стандартного диалогового окна. QFileDialog возвращает абсолютный путь к файлу, поэтому мы должны сами отделить его имя с помощью QFileInfo::fileName(). Имя файла мы отобразим в текстовом поле m_edit и отправим в запросе к серверу. После мы должны открыть выбранный файл и прочитать его содержимое. Если все прошло нормально, то мы создаем объект класса FileSaverProxy, который полностью сгенерировал gSOAP. Однако для него требуется небольшая настройка. Мы задаем значение поля soap_endpoint для указания местоположения сервера в сети, воспользовавшись определенной в начале файла константой ENDPOINT. Поскольку я планирую все запускать на одном компьютере, то инициализировал константу значением "127.0.0.1:8085". Однако вы можете решить вынести эту настройку в конфигурационный файл. Вообще, у proxy-объекта есть и другие настройки. Например, мы могли бы задать значения тайм-аутов на подключение и ожидание ответа от сервера. Но заметка итак получилась слишком большой, поэтому оставим это на потом. Далее мы создаем экземпляр класса xsd__base64Binary и пользуемся знакомой нам функцией initBase64(), которую определяли в файле shareddefs.h. Здесь в качестве второго аргумента мы передали наш объект proxy. Далее мы подготавливаем переменную errorCode, в которую ожидаем получить код ошибки. И наконец осуществляем запуск saveFile() для объекта proxy с соответствующими нашему протоколу параметрами. После этого запрос уходит на сервер и обрабатывается там. Если запрос отработает нормально и не произойдет никаких сетевых сбоев, то функция вернет значение SOAP_OK, но в этом случае остается возможность, что во время выполнения запроса на сервере произошла какая-либо внутренняя ошибка. Об этом мы можем узнать на основе заготовленного значения errorCode. Если же запрос не дошел, то мы просто выводим уведомление "SOAP_ERROR".

Теперь посмотрим на реализацию слота onLoad(). Здесь все не намного сложнее. Инициализация proxy-объекта полностью идентична. Далее мы подготавливаем объект класса ns__loadFileResponse и вызываем loadFile(), передавая ему имя файла, которое введено в текстовое поле. Если все нормально, то мы также выводим код ошибки. А если и код ошибки говорит о том, что загрузка прошла успешна, то мы пытаемся создать объект QPixmap на основе полученных данных и отобразить его на экране с помощью лейбла.

Вот и все. Теперь вы можете запустить в консоли серверную часть FileSaverServer, а затем вызвать клиент FileSaverClient. В клиенте нажать на кнопку Save и выбрать какой-нибудь файл с изображением. Если все пройдет без проблем, то в каталоге, где был запущен FileSaverServer, уже появится файл с таким же именем. Поэтому теперь вы можете нажать в клиентском приложении кнопку Load и отправленный ранее файл отобразится в графическом интерфейсе.

Заключение

Заметка получилась достаточно объемной, но я надеюсь, что вы ее дочитали. В ней мы подробно рассмотрели процесс получения и установки gSOAP. Разработали свой протокол веб-службы для сохранения и загрузки файлов. А затем реализовали серверное и клиентское приложение на C++, которые способны взаимодействовать по сети с помощью этого протокола.

Понравилась статья? Поделиться с друзьями:
Комментарии: 2
  1. Jorjio774

    Отличная статья. Для компиляции проекта под Win32 не забываем добавить в файл проекта

    LIBS += C:/{Qt-путь}/mingw/lib/libws2_32.a

  2. Sergei

    Статья просто супер, большое спасибо!

Добавить комментарий