Пишем SOAP-клиента на C++, используя gSOAP

в 3:47, , рубрики: c++, soap, Программирование, метки: ,

Так сложилось, что работа с gSOAP на хабре описана очень слабо. Всего лишь один пост, если быть честным. Но там описано создание веб-сервиса, а как же быть с клиентскими приложениями? Не так давно передо мной встала задача организовать работу с удаленным сервером, использующими SOAP — и я решил написать небольшую статью об этом.
Т.к. предоставить wsdl-файлы, с которыми велась работа, я не могу (NDA и все такое), то я задался поиском сервисов, пригодных для тестирования. Интересными мне показались два:

http://www.webservicex.net/ValidateEmail.asmx?WSDL
http://www.webservicex.net/country.asmx?WSDL

Я так и не нашел, где скачать wsdl-файлы, поэтому скопировал их содержимое и сохранил под именами ValidateEmail.wsdl и country.wsdl
Скачать gSOAP можно тут — http://www.cs.fsu.edu/~engelen/soap.html. По этому же адресу можно и почитать о gSOAP.

Последняя версия на момент написания статьи — 2.8.14

Приступим к работе. В папке gsoapbinwin32 живут две очень важные утилиты. Сначала нас интересует wsdl2h.exe. Узнать о ней побольше можно с помощью справки:
>wsdlh2.exe -h
Запустим ее со следующими параметрами:
wsdl2h.exe -o emailAndCountry.h ValidateEmail.wsdl country.wsdl
Все довольно просто, мы указали лишь имя выходного файла и список wsdl-файлов, с которыми хотим работать.
После этого генерируем непосредственно код классов C++:
soapcpp2.exe -C -dgSoap -j -L -x -I«ADDRESS_TO_GSOAPgsoap-2.8gsoapimport» emailAndCountry.h
Ключ -C говорит, что нужно генерировать только клиентский код.
-dgSoap просит сложить все файла в папку gSoap (ее нужно предварительно создать). Мы не генерируем lib-файлы и не наследуемся от soap-структуры; ключ -x просит не генерировать XML-файлы с примерами сообщений. Указываем адрес до папки с gSOAP и файл, который будем парсить и на основе которого генерируем код.
gSOAP разных версий может генерировать разный код (что логично, в общем-то), причем даже состав файлов будет различаться. Это важно помнить, если вы вдруг захотите генерировать gSOAP-файлs на билд-сервере, а не хранить их в системе контроля версий.
После всех манипуляций в папке gSoap видим много новых файлов. Это плюсовые -h и -cpp файла, а так же countrySoap.nsmap и ValidateEmailSoap.nsmap. Они совпадают, можно сохранить их содержимое в одном (например, namespaces.nsmap), а их удалить. namespaces.nsmap нужно заинклудить в проект. Как правило, это делается в каком-нибудь вспомогательном классе, который будет рабоать с gSOAP. Да, такой класс наверняка будет существовать.

После этого добавляем в папку gSoap stdsoap2.h и stdsoap2.cpp — они инклудятся в soapStub.h

Добавляем всю папку в проект и начинаем работать:)
Смотрим классы soapcountrySoapProxy.cpp и soapValidateEmailSoapProxy.cpp; в методах *_init(soap_mode imode, soap_mode omode) — удаляем namespases (мы же не зря инклудили наш namespaces.nsmap).

Начнем с возможности валидации е-мейла — gSoap/soapValidateEmailSoapProxy.h
Нас интересует метод
virtual int IsValidEmail(_ns1__IsValidEmail *ns1__IsValidEmail, _ns1__IsValidEmailResponse *ns1__IsValidEmailResponse)

Смотрим описание аргументов этой функции:

class SOAP_CMAC _ns1__IsValidEmail
{
public:
	std::string *Email;	/* optional element of type xsd:string */
	struct soap *soap;	/* transient *
…./

Поле *Email нам наверняка пригодится.

Создаем объект этого класса и указываем объект, который будем проверять


	_ns1__IsValidEmail isValidEmailRequest;
	std::string CHECKED_E_MAIL("pisem@sovsem.net");
	isValidEmailRequest.Email = &CHECKED_E_MAIL;

Сразу создадим объект, в который будет приходить результат:

_ns1__IsValidEmailResponse isValidEmailResponse;

Мы видим, что результат будет в поле bool IsValidEmailResult.

Отправляем запрос на сервер:

const int gSoapResult = validateEmailProxy.IsValidEmail(&isValidEmailRequest, &isValidEmailResponse);

Сниффером смотрим, что именно мы отправили:

<?xml version="1.0" encoding="UTF-8" ?>
- <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:SOAP-ENC="http://www.w3.org/2003/05/soap-encoding" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://www.webservicex.net" xmlns:ns2="http://www.webserviceX.NET">
- <SOAP-ENV:Body>
- <ns1:IsValidEmail>
<ns1:Email>pisem@sovsem.net</ns1:Email>
</ns1:IsValidEmail>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Смотрим ответ — видим, что в поле result находится false. Сниффер это подтверждает.

Пробуем другой адрес — press@fsb.ru

Сразу замечу, что сервер думает довольно-таки долго, но с проверяемым адресом это вряд ли связано.
Но ответ все же приходит, и этот адрес у нас считается невалидным.
Что ж, пробуем заведомо “плохой” адрес — “1354@”

Время выполнения результата — вечность. Работает действительно очень долго.
Примерно через минуту мне ждать надоело, и я решил снова попробовать проверить валидный адрес — adv@thematicmedia.ru
Ок, результат получен — false.
adv@thematicmedia — false
adv@l — false
Попробовал один из своих древних почтовых ящиков — сервер опять задумался. Надолго. Но все-таки ответил — false.

Ну что ж, получается, что проблема лишь в сервере, наш код ответы сервера передает верно. Код работает, но абсолютно бесполезен.

Пробуем второй класс — countrySoapProxy
Количество его методов значительно больше, их можно посмотреть в хидере.
Будем тестировать их по порядку, начиная с GetCountryByCountryCode:


	_ns2__GetCountryByCountryCode getCountryByCountryCodeRequest;
	std::string COUNTRY_CODE("GB");
	getCountryByCountryCodeRequest.CountryCode = &COUNTRY_CODE;
	_ns2__GetCountryByCountryCodeResponse getCountryByCountryCodeResponse;
	countrySoapProxy countryProxy;

	const int gSoapResult = countryProxy.GetCountryByCountryCode(&getCountryByCountryCodeRequest, &getCountryByCountryCodeResponse);
	if (gSoapResult != SOAP_OK)
	{
		std::cout << "FAIL" << std::endl;
		return 0;
	}

Ответ получен:

<NewDataSet>
  <Table>
    <countrycode>gb</countrycode>
    <name>Great Britain</name>
  </Table>
  <Table>
    <countrycode>gb</countrycode>
    <name>Great Britain</name>
  </Table>
</NewDataSet>

Я, признаться, ожидал, в ответе будет лишь страна, а не здоровый DataSet, ну да это придирки. Страну с кодом RU этот сервис тоже знает, ура!

Переходим к другому методу:


	_ns2__GetISD getISDRequest;
	std::string COUNTRY_NAME("Russian Federation");
	getISDRequest.CountryName = &COUNTRY_NAME;
	_ns2__GetISDResponse getISDResponse;

	countrySoapProxy countryProxy;

	const int gSoapResult = countryProxy.GetISD(&getISDRequest, &getISDResponse);

И этот метод работает как надо:


<NewDataSet>
  <Table>
    <code>7</code>
    <name>Russian Federation</name>
  </Table>
  <Table>
    <code>7</code>
    <name>Russian Federation</name>
  </Table>
</NewDataSet>

Что ж, для этого сервиса все работает, как ожидалось, для учебных целей он подходит чуть больше, чем первый. На этом эксперименты можно пока и прекратить.

Такой подход замечательно работает, когда мы собираемся работать только с одним сервисом, который расположен по заданному адресу и не требует авторизации.
Но мы же можем развернуть веб-сервис где угодно и как угодно!
На самом деле, gSoap умеет многое. Так, например, задать логин-пароль можно через soap-структуру. Пример простейшей basic-авторизации:

soap.userid = login;
soap.passwd =password;

Описание soap-структуры находится в stdsoap2.h:

  const char *userid;		/* HTTP Basic authorization userid */
  const char *passwd;		/* HTTP Basic authorization passwd */

Изменить адрес, на который будут отправляться запросы, можно двумя способами: задавать его при самом запросе либо задавать при создании SOAP-прокси.
Все это довольно очевидно из самих хидеров:


	virtual	int GetISD(_ns2__GetISD *ns2__GetISD, _ns2__GetISDResponse *ns2__GetISDResponse) { return this->GetISD(NULL, NULL, ns2__GetISD, ns2__GetISDResponse); }
	virtual	int GetISD(const char *endpoint, const char *soap_action, _ns2__GetISD *ns2__GetISD, _ns2__GetISDResponse *ns2__GetISDResponse);

В случае второго способа нам надо создавать объект примерно так:


	countrySoapProxy countryProxy("http://www.webservicex.net/country.asmx");

Тоже ничего сложного, не так ли? Здесь можно задавать и нормальный IP-адрес с портом.

Еще я не учел, как самому задавать нэймспейсы. Такой подход позволит получать не ns1__IsValidEmail в качестве имен классов, а что-то вроде email__IsValidEmail. Когда классов много, то можно запутаться. Наверное. Это делается тоже без проблем. Создаем файл typeMap.dat c содержимым следующего формата:
myCustomNamespace = «www.webservicex.net»
Т.е. все просто: указываем имя нэймспейса и его адрес.

Вообще, работа с gSoap не составляет особой трудности. Но есть несколько моментов, которые всплывают при работе с ним. Так, например, при задании логина и пароля они могут меняться после выполнения запроса. Т.е. код может выглядеть примерно так:


	countrySoapProxy countryProxy("http://www.webservicex.net/country.asmx");
	countryProxy.soap->userid = "login";
	countryProxy.soap->passwd = "password";
	countryProxy.GetISD(&getISDRequest, &getISDResponse);	
	countryProxy.soap->userid = "login";
	countryProxy.soap->passwd = "password";
	countryProxy.GetISD(&anotherGetISDRequest, &anotherGetISDResponse);

Выглядит это почему-то сомнительно.
Возможно, я чего-то не учел, но краткий опыт работы с gSOAP оставляет смешанное впечатление: высокая скорость разработки, относительно небольшое количество кода — все это в плюс. Но вот мешанина кода, смесь С и С++ — это минус. Но я допускаю, что это минус не для всех, а плюсов значительно больше. В любом случае, достойных альтернатив gSOAP мне найти не удалось — хотя я искал не очень тщательно, доверившись проверенному в нашей компании решению.
Эта статья — скорее вводная, которая кратко рассказывает о начале работы с gSOAP. Я не привел здесь разных способов авторизации, не изучил вопросы наследования от SOAP-структур (это же для чего-то нужно?). Нераскрытых вопросов много, но краткий экскурс в gSOAP я все же дал, надеюсь. Удачной работы!

Автор: vovochkin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js