Чтобы показать, как работают веб-сервисы, обращаться к которым можно по протоколу SOAP, зачем нужен WSDL, и как это связано с XML-документом в теле полезной нагрузки POST-запроса, сегодня я напишу простое Python-приложение для работы с поставщиками.
Еще раз о том, что такое SOAP и чем он отличается от REST
Хотя вопрос «Чем отличается REST от SOAP» является чуть ли не самым популярным в собеседованиях на роль системного аналитика, мне он напоминает попытку сравнить круглое с зеленым. Начнем с того, что REST API – это архитектурный стиль разработки веб-приложения, тогда как SOAP – это строгий протокол обмена структурированными XML-сообщениями в распределённой вычислительной среде (Simple Object Access Protocol). Он расширяет протокол XML-RPC и работает поверх протоколов прикладного уровня: SMTP, FTP, HTTP, HTTPS и пр.
REST как архитектурный стиль не имеет строгих стандартов использования типовых HTTP-методов (GET, POST, PUT, PATCH, DELETE) для операций над ресурсами, позволяя разработчику реализовать бизнес-логику приложения в рамках такой ресурсной модели. SOAP имеет множество стандартов и расширений (WS-Security для безопасности, WS-AtomicTransaction для транзакций), что делает его более подходящим для приложений со сложной бизнес-логикой и транзакционных операций.
В качестве примера рассмотрим веб-сервис оценки поставщика, который вычисляет его рейтинг надежности в зависимости от характеристик компании и назначает зону доставки в зависимости от адреса. Процесс взаимодействия пользователя с такой клиент-серверной системой выглядит так.
PlantUML-скрипт для этой диаграммы:
@startuml title Оценить поставщика\n(рейтинг надежности\nили зону доставки) actor User participant "Client System" as Client participant "SOAP Server" as Server User -> Client: оценить поставщика\n(данные поставщика) activate Client Client -> Server: POST-запрос(SOAP-сообщение\nс нужной функцией\nи ее параметрами) activate Server Server -> Server: выполнить\nфункцию(функция\nи ее параметры\nиз SOAP-сообщения) Server -> Server: упаковать результат\nв SOAP-сообщение Server --> Client: ответное SOAP-сообщение deactivate Server Client --> User: результат оценки deactivate Client @enduml
Веб-сервис, общение с которым реализуется через SOAP, обычно имеет 1 конечную точку, т.е. один URL-адрес, на который отправляется HTTP-запрос POST с полезной нагрузкой в виде SOAP-сообщения. Это SOAP-сообщение всегда представляется в формате XML и имеет древовидную тегированную структуру. Например, для оценки надежности поставщика в моем SOAP-сервисе оно выглядит так.
Формат XML позволяет описывать сложные структуры данных с помощью иерархии вложенных тегов. Можно расширять XML-документы, определяя собственные теги, и строить из них сложные иерархические структуры, соблюдая принцип декомпозиции. Общая схема данных для XML-документа описывается в XSD (XML Schema Definition), где задаются элементы и атрибуты XML, их типы, порядок, количественные и содержательные ограничения. Подробнее о том, что такое схема данных и чем она отличается от формата сериализации, я писала здесь.
Поскольку XML является довольно строгим, хотя и расширяемым форматом данных, неудивительно, что именно он используется в строгом протоколе SOAP. Поскольку межсистемная интеграция по SOAP реализует принципы удаленного вызова процедур (RPC, Remote Procedure Call), клиенту, инициирующему взаимодействие, надо знать, какие функции он может вызвать на удаленном сервере и с какими параметрами. Это описывается в WSDL-документе (Web Services Description Language), тоже представленном в формате XML. WSDL содержит определение типов данных отправляемых и получаемых веб-сервисом SOAP-сообщений, какие операции могут быть с ними выполнены, конечную точку обращения к веб-сервису и привязки – способы доставки сообщений.
В отличие от схемы полезной нагрузки, т.е. SOAP-сообщения, WSDL-документ не создается аналитиком или разработчиком вручную, а генерируется из программного кода веб-сервиса. Например, Python-библиотека Spyne автоматически генерирует WSDL-документ на основе определённых в коде сервисов и типов данных. Пример этого кода и тестирование сервиса через Postman рассмотрим далее.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.
Реализация SOAP-сервиса на Python
Разумеется, лучше вести разработку в локальной IDE, например, PyCharm или VSCode, разворачивая приложения на локальном хосте. Но, чтобы вы могли повторить эту работу без установки IDE, приведу Python-код для запуска в интерактивной среде Google Colab. Сперва установим библиотеки, необходимые для работы с XML, создания веб-приложений и веб-сервисов, а также для туннелирования сокета, на котором развернуто SOAP-приложение, запущенное на конкретном порту локального хоста виртуальной машины Google Colab, чтобы к нему можно было обратиться извне. При работе на локальном хосте ngrok не нужен, а инструкции установки пакетов зависят от самой IDE. Однако, для Colab это выглядит так:
!pip install lxml !pip install spyne flask !pip install flask-ngrok #библиотека для тунелирования, чтобы достучаться к веб-серверу извне !pip install pyngrok
Далее импортируем модули и функции для создания и запуска веб-сервиса.
import os from flask import Flask, request, Response from spyne import Application, rpc, ServiceBase, Unicode, Integer from spyne.protocol.soap import Soap11 from spyne.server.wsgi import WsgiApplication from lxml import etree from pyngrok import ngrok
Установим личный токен утилиты тунелирования ngrok, полученный на платформе https://dashboard.ngrok.com/
!ngrok authtoken 'your own ngrok token'
Создадим публичный URL-адрес с помощью ngrok, чтобы веб-сервис с локального хоста был доступен извне:
public_url = ngrok.connect(addr="5000", proto="http") #Вывод публичного URL для доступа к веб-интерфейсу print("Адрес:", public_url)
Объявим класс SupplierService, который наследуется от базового класса ServiceBase. В библиотеке Spyne класс ServiceBase является базовым для всех определений служб, т.е. содержит общие свойства и методы, необходимые для работы с веб-сервисом.
class SupplierService(ServiceBase): @rpc(Unicode, _returns=Integer) def assess_reliability(ctx, supplier_data): try: # Разбор SOAP-сообщения data = etree.fromstring(supplier_data) # Инициализация рейтинга rating = 0 # Проверка формы бизнеса business_form = data.find('business_form').text if business_form == 'ЗАО' or business_form == 'ОАО': rating += 3 elif business_form == 'ИП': rating += 1 elif business_form == 'ООО': rating += 2 # Проверка длительности существования age = int(data.find('age').text) if age > 3: rating += 3 elif age > 2: rating += 2 elif age > 1: rating += 1 # Проверка количества сотрудников employees = int(data.find('employees').text) if employees > 5: rating += 1 # Проверка наличия офиса office = data.find('office').text.lower() == 'true' if office: rating += 1 # Проверка наличия склада warehouse = data.find('warehouse').text.lower() == 'true' if warehouse: rating += 2 # Оценка благонадежности if rating > 7: return 1 # Надежный else: return 0 # Ненадежный except etree.XMLSyntaxError as e: print(f"Ошибка парсинга XML: {e}") return -1 # Ошибка в XML except Exception as e: print(f"Общая ошибка: {e}") return -1 # Общая ошибка @rpc(Unicode, _returns=Unicode) def assign_delivery_zone(ctx, supplier_data): try: xml_data = etree.fromstring(supplier_data) address = xml_data.find('address').text # Пример простой логики назначения зоны доставки if 'ЦАО' in address: return 'Зона 1' elif 'САО' or 'ВАО' or 'ЗАО' or 'ЮАО' in address: return 'Зона 2' elif 'СВАО' or 'ЮВАО' or 'ЮЗАО' or 'СЗАО' in address: return 'Зона 3' elif 'ЗелАО' or 'НАО' or 'ТАО' in address: return 'Зона 4' else: return 'Неизвестно' except etree.XMLSyntaxError as e: print(f"Ошибка парсинга XML: {e}") return 'Ошибка парсинга XML'
Этот класс содержит два метода:
- assess_reliability – оценка надежности поставщика по различным критериям (организационно-правовая форма бизнеса, время существования в годах, количество сотрудников, наличие офиса и склада);
- assign_delivery_zone – назначение зоны доставки на основе адреса поставщика с указанием административного округа Москвы.
Декоратор @rpc выставляет методы как удаленные вызовы процедур и объявляет типы данных, которые он принимает и возвращает, а также передает экземпляр контекста метода Spyne-библиотеки spyne.MethodContext в качестве первого аргумента вызываемой функции.
Далее создадим Flask-приложение и свяжем его с библиотекой Spyne для обработки SOAP-запросов.
app = Flask(__name__) application = Application( [SupplierService], tns='spyne.examples.supplier', in_protocol=Soap11(validator='lxml'), out_protocol=Soap11() )
В этом коде помимо указания протокола SOAP версии 1.1 для входящих и исходящих сообщений веб-сервиса, валидации входящих сообщений с помощью библиотеки lxml также указано целевое пространство имен, tns (target namespace). В данном случае tns=’spyne.examples.supplier’ обозначает уникальный идентификатор пользовательского пространства имен, которое далее будет указываться во входящих и исходящих SOAP-сообщениях, что мы посмотрим далее. Задавать пространство имен нужно, чтобы избежать конфликтов между разными веб-сервисами.
Для взаимодействия веб-сервера и серверного веб-приложения на Python используется стандарт интерфейса WSGI (Web Server Gateway Interface). Он позволяет веб-приложениям взаимодействовать с веб-серверами независимым от конкретного сервера способом.
wsgi_app = WsgiApplication(application)
Для синхронного взаимодействия с веб-сервисом определим функцию start_response, которая создает и возвращает объект Response, используя параметры, переданные при вызове функции.
def start_response(status, response_headers, exc_info=None): response = Response(status=status, headers=dict(response_headers)) return response
Далее определим конечные точки SOAP-сервиса, т.е. маршруты с помощью декоратора @app.route, и методы обращения к ним.
@app.route('/', methods=['POST', 'GET']) def combined_service(): if request.method == 'POST': return Response(wsgi_app(request.environ, start_response), content_type='text/xml') elif request.method == 'GET': return """ <b>Это веб-сервис проверки поставщиков</b><br> UI очень не красивый, зато работает! :) <br> Отправьте через Postman сюда SOAP-сообщение POST-запросом с данными поставщика <br> и получите рейтинг его благонадежности или зону доставки <br> <br> <br> <b>Пример SOAP-сообщения проверки благонадежности поставщика:</b><br> <pre> <?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:spy="spyne.examples.supplier"> <soapenv:Header/> <soapenv:Body> <spy:assess_reliability> <spy:supplier_data><![CDATA[ <data> <business_form>ООО</business_form> <age>10</age> <employees>50</employees> <office>true</office> <warehouse>false</warehouse> </data> ]]></spy:supplier_data> </spy:assess_reliability> </soapenv:Body> </soapenv:Envelope> </pre> <br> <b>Пример SOAP-сообщения вычисления зоны доставки поставщика:</b><br> <pre> <?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:spy="spyne.examples.supplier"> <soapenv:Header/> <soapenv:Body> <spy:assign_delivery_zone> <spy:supplier_data><![CDATA[ <data> <address>ЦАО, ул. Ленинский проспект, д.17</address> </data> ]]></spy:supplier_data> </spy:assign_delivery_zone> </soapenv:Body> </soapenv:Envelope> </pre> """ @app.route('/wsdl', methods=['GET']) def wsdl(): wsdl_environ = request.environ.copy() wsdl_environ['PATH_INFO'] = '/soap?wsdl' wsdl_environ['QUERY_STRING'] = 'wsdl' response = wsgi_app(wsdl_environ, start_response) return Response(response, content_type='text/xml')
Хотя для удаленных процедур, которые клиент может вызвать на сервере, используется 1 конечная точка, для доступа к WSDL-документу этой SOAP-службы добавлена вторая.
Для непосредственного развертывания Flask-приложения запустим его на порту 5000:
if __name__ == "__main__": app.run(host='0.0.0.0', port=5000)
Весь код можно, как обычно, посмотреть в моем Github-репозитории: https://github.com/AnnaVichugova/PythonApps/blob/main/SOAP.
После запуска представленного скрипта можно кликнуть на туннелированный URL, новый при каждом запуске, и в браузере откроется веб-сервис.
Для удобства работы с этой демонстрацией при отправке GET-запроса к главному маршруту на HTML-странице я решила вывести примеры SOAP-сообщений, которые можно отправить POST-запросом к веб-сервису.
Далее можно протестировать этот веб-сервис через Postman, отправив POST-запрос к этому URL с полезной нагрузкой в виде XML-сообщения SOAP. Например, для получения оценки надежности поставщика это SOAP-сообщение может выглядеть так:
<?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:spy="spyne.examples.supplier"> <soapenv:Header/> <soapenv:Body> <spy:assess_reliability> <spy:supplier_data><![CDATA[ <data> <business_form>ООО</business_form> <age>10</age> <employees>50</employees> <office>true</office> <warehouse>false</warehouse> </data> ]]></spy:supplier_data> </spy:assess_reliability> </soapenv:Body> </soapenv:Envelope>
В этом сообщении определены атрибуты пространства имен xmlns (XML namespace – пространство имен): универсальное пространство имен SOAP и пользовательское пространства имен spyne.examples.supplier, ранее указанное в коде самого приложения.
Поскольку непосредственно сама полезная нагрузка данных о поставщике может включать спецсимволы, например, кавычки и пр., которые интерпретируются как часть синтаксиса XML, эти поля заключены в секцию CDATA-секция (Character Data), которая НЕ обрабатывается XML-парсером, а разбираются методами Python-библиотеки lxml и ее модуля etree.
Для вычисления зоны доставки надо отправить такое SOAP-сообщение:
<?xml version="1.0" encoding="utf-8"?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:spy="spyne.examples.supplier"> <soapenv:Header/> <soapenv:Body> <spy:assign_delivery_zone> <spy:supplier_data><![CDATA[ <data> <address>ЦАО, ул. Ленинский проспект, д.17</address> </data> ]]></spy:supplier_data> </spy:assign_delivery_zone> </soapenv:Body> </soapenv:Envelope>
Разумеется, теги в теле SOAP-сообщения, в которых заключены передаваемые на сервер данные, должны соответствовать объектам в коде, полученным из строковых значений после парсинга входящего XML-документа. В частности, функция etree.fromstring() из библиотеки lxml принимает строку XML и парсит её в дерево XML-элементов, чтобы работать с XML-структурой как с объектами. Метод find() дерева XML-элементов ищет в нем первый элемента с указанным тегом, а его атрибут text возвращает текстовое содержимое найденного элемента.
Таким образом, веб-сервис разбирает (парсит) входящее SOAP-сообщение, извлекает содержащиеся в нем данные и передает их в соответствующие методы серверного приложения. Результаты этих вызовов упаковываются в ответное SOAP-сообщение, которое отправляется обратно клиенту. Сопоставление значений тегов SOAP-сообщения с переменными в коде выполняется с помощью десериализаторов, которые преобразуют XML-данные в объекты. При этом можно валидировать типы передаваемых данных, например, с помощью XSD-схемы XML-документа, передаваемого в теле SOAP-сообщения. Все это: названия удаленных методов, их параметры, типы передаваемых и возвращаемых значений, отражается в WSDL-документе. Просмотреть этот WSDL-документ в моем примере можно, просто обратившись GET-запросом HTTP к ресурсу /wsdl, добавив это к URL-адресу, сгенерированного с помощью утилиты ngrok для запущенного SOAP-приложения.
Этот WSDL-документ, сгенерированный из кода SOAP-приложения средствами Python-библиотеки Spyne, можно далее использовать для интеграции внешних систем с разработанным веб-сервисом. Причем не только для этапа проектирования как документацию, но и для непосредственной реализации, сгенерировав классы клиентского приложения на основе WSDL, например, с помощью SOAP-клиента zeep для Python или модуля wsdl2java фреймворка Apache CXF для Java.
Надеюсь, что эта небольшая демонстрация с разбором особенностей реализации веб-сервиса помогла вам понять основные принципы межсистемной интеграции по протоколу SOAP, а также разобраться с назначением и содержанием XML, XSD и WSDL-документов.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.
Больше примеров и подробностей про архитектуру и интеграцию информационных систем вы узнаете на моих курсах в Школе прикладного бизнес-анализа на базе нашего лицензированного учебного центра обучения и повышения квалификации системных и бизнес-аналитиков в Москве: