Практика — лучший способ глубокого понимания теории. Рассмотрим на примере, как работают веб-сервисы с протоколом SOAP, для чего нужен WSDL и как он связан с XML-документом в теле POST-запроса.

Для изучения работы веб-сервисов на основе протокола SOAP 1.1:

  • разработаем клиентскую и серверную части веб-приложения на Python

  • проанализируем структуру WSDL 1.1 (Web Services Description Language) описания веб-сервиса, сгенерированную серверной частью

  • создадим пользовательское пространство имен в формате XSD (XML Schema Definition)

  • проведем тестирование приложения с помощью Postman

SOAP, REST и немного теории.

Перед тем, как углубиться в работу нашего с вами сервиса, для себя в очередной раз вспомним чем SOAP отличается от REST и более подробно остановимся на особенностях SOAP.

REST (Representational State Transfer) — это архитектурный стиль, основанный на HTTP-протоколе, который используется для разработки приложений с фронтендом или интеграционными потоками. REST не предусматривает строгих стандартов использования типовых HTTP-методов, а лишь описывает рекомендации, которым должны следовать проектируемые сервисы API. Одной из ключевых особенностей REST API является представление данных в виде набора ресурсов, каждый из которых имеет свой уникальный унифицированный идентификатор (URI).

SOAP (Simple Object Access Protocol) — это протокол обмена сообщениями в формате XML, основанный на технологии удалённого вызова процедур. Он работает через различные протоколы передачи данных (HTTP, SMTP, TCP, UDP). Данный протокол представляет собой более строгий и формализованный подход к обмену данными, что делает его похожим на детально проработанное соглашение, практически исключающее вероятность ошибок в работе.

SOAP, в отличие от REST, не использует отдельные URI для каждого ресурса. SOAP работает как протокол обмена сообщениями, который может использовать различные транспортные протоколы. SOAP больше подходит для сложных операций, требующих поддержания состояния разговора и контекстной информации, в то время как REST эффективен для простых операций типа CRUD.
Важно помнить:

  • SOAP не является однонаправленным протоколом и не требует отправки обратного сообщения.

  • SOAP не является (исключительно) протоколом RPC (Remote Procedure Call), спецификация протокола не требует от сервера SOAP вызвать метод в результате получения SOAP.

Завершив теоретическую часть, приступим к анализу технологий, входящих в экосистему SOAP.

XML документ, от простого к сложному

Теперь опишем XML-документ, который будет использован как основа для разработки наших приложений.

В качестве примера рассмотрим веб-сервис доставки канцелярских товаров. Представим, что нам нужно передавать следующие данные в виде XML-документа:

XML-документ
<?xml version="1.1" encoding="UTF-8"?>
<tns:Shiporder orderid="12345"
    xmlns:tns="my.namespace"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="my.namespace shiporder.xsd">

    <tns:orderperson>Иван Петров</tns:orderperson>

    <tns:shipto>
        <tns:name>Петр Иванов</tns:name>
        <tns:address>ул. Ленина</tns:address>
        <tns:city>Москва</tns:city>
        <tns:country>Россия</tns:country>
    </tns:shipto>

    <tns:items>
        <tns:Item>
            <tns:title>Ручка гелевая</tns:title>
            <tns:note>Цвет: черный</tns:note>
            <tns:quantity>10</tns:quantity>
            <tns:price>25.00</tns:price>
        </tns:Item>
        <tns:Item>
            <tns:title>Калькулятор</tns:title>
            <tns:quantity>5</tns:quantity>
            <tns:price>350.00</tns:price>
        </tns:Item>
    </tns:items>
</tns:Shiporder>

Давайте разберем его подробней.

Приведенный выше XML документ начинается с Декларации XML. Это первая строка в XML-документе, которая указывает версию XML и кодировку документа. Она имеет следующий формат: <?xml version="1.0" encoding="UTF-8"?>

Далее следует корневой элемент Shiporder с обязательным атрибутом orderid.

Строка xmlns:tns="my.namespace" — объявляет пространство имён и задаёт область видимости.

Строка xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" даёт доступ к служебным атрибутам XSD, например к xsi:SchemaLocation.

Строка xsi:SchemaLocation="my.namespace shiporder.xsd" указывает, где именно находится схема (путь к файлу относительный или абсолютный) и включает проверку по XSD.

Как это работает:

  1. Парсер видит xsi:schemaLocation. Понимает, что документ нужно проверить по XSD.

  2. Сопоставляет пространство имён (my.namespace) с файлом схемы (shiporder.xsd).

  3. Загружает схему и проверяет структуру, типы данных, обязательность полей и т.д.

За пространством имен и атрибутами валидации схемы следует структура элементов

  • Shiporder содержит дочерние элементы: orderperson, shipto, items

  • shipto включает обязательные дочерние элементы: name, address, city, country

  • items является контейнером для элементов Item, каждый из которых содержит: title, note (необязательный элемент), quantity, price

Структура XML-документа с учётом пространств имён
Структура XML-документа с учётом пространств имён

Атрибуты XML (например, xmlns:tnsxsi:schemaLocation) в таблицу не включены, так как она описывает структуру данных, а не метаданные документа.

XML Schema Definition, создание XML схемы

Следующим шагом подготовим XSD (XML Schema Definition) и определим элементы схемы shiporder.xsd, где описаны правила валидации для пространства имён my.namespace.

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

Для создания XML схемы будем следовать за структурой XML документа и определять каждый встреченный элемент. Начнем со стандартной XML декларации, за которой опишем элемент xs:schema, который и определяет саму схему:

<?xml version="1.1" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns="my.namespace"
targetNamespace="my.namespace"
elementFormDefault="qualified">
...
</xs:schema>

Строка <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> указывает на стандартное пространство имён, содержащее все встроенные типы данных и элементы для описания структуры XML.

Определяет префикс xs: (или xsd:) для элементов и типов XSD

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

  • Простые типы: xs:stringxs:integerxs:date.

  • Сложные типы: xs:complexTypexs:sequence.

  • Элементы и атрибуты: xs:elementxs:attribute.

Объявление xmlns:tns="my.namespace" в XSD-схеме служит для связи между XML-документом и его схемой (XSD):

  • Указывает, что схема описывает элементы из пространства имён my.namespace

  • Позволяет ссылаться на эти элементы внутри схемы через префикс tns:

  • Обеспечивает корректную привязку XML-документа к схеме при валидации.

Объявление targetNamespace="my.namespace" определяет пространство имён, к которому принадлежат все глобально объявленные элементы и типы в этой XSD-схеме. Это означает:

·         Все элементы (<xs:element>), объявленные на верхнем уровне схемы, автоматически попадают в это пространство имён

·         Все типы данных (<xs:complexType><xs:simpleType>) также ассоциируются с этим пространством

Атрибут elementFormDefault="qualified" в XSD-схеме управляет тем, должны ли локальные (вложенные) элементы в XML-документе явно указывать на пространство имён. В нашем случае должны, также должны иметь префикс. Другими словами атрибут обеспечивает строгий контроль над пространствами имён для всех элементов XML, что особенно важно в сложных или распределённых системах.

Далее перейдем к описанию типов и элементов.

ItemType, определяет позиции заказа. Поскольку этот тип содержит вложенные элементы и/или атрибуты, он описывается как сложный тип (complexType).

Определения дочерних элементов ItemType поместим в декларацию xs:sequence, что задает строгую последовательность входящих элементов:

<!-- Тип данных для объекта Позиция заказа -->
<xs:complexType name="ItemType">
<xs:sequence>
<xs:element name="title" type="xs:string" />
<xs:element name="note" type="xs:string" minOccurs="0" />
<xs:element name="quantity" type="xs:integer" />
<xs:element name="price" type="xs:decimal" />
</xs:sequence>
</xs:complexType>

Так как элемент note необязательный, то присваиваем ему атрибут minOccurs, который задает минимальное число вхождений. В нашем случае «0»
Подобным образом описываем тип ShiptoType

<!-- Тип данных для объекта Грузополучатель -->
<xs:complexType name="ShiptoType">
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="address" type="xs:string" />
<xs:element name="city" type="xs:string" />
<xs:element name="country" type="xs:string" />
</xs:sequence>
</xs:complexType>

Далее нам необходимо определить список товаров в заказе, для чего определим тип ItemArray. Внутри типа создадим элемент Item, присвоим ему атрибут type со ссылкой на именованный тип tns:ItemType, и второй атрибут maxOccurs="unbounded", который означает, что элемент Item может повторяться в XML неограниченное число раз.

<!-- Тип данных для объекта позиции заказа -->
<xs:complexType name="ItemArray">
<xs:sequence>
<xs:element name="Item" type="tns:ItemType" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>

Осталось все обернуть в корневой элемент ShiporderType на основе complexType. Внутрь, согласно нашей структуре xml документа, поместить элементы orderperson, shipto, items.

Завершаем определение элемента ShiporderType декларацией обязательного атрибута orderid с параметром use="required".

<xs:complexType name="ShiporderType">
<xs:sequence>
<xs:element name="orderperson" type="xs:string" />
<xs:element name="shipto" type="tns:ShiptoType" />
<xs:element name="items" type="tns:ItemsArray" />
</xs:sequence>
<xs:attribute name="orderid" type="xs:integer" use="required"/>
</xs:complexType>
<!-- Создание корневого элемента в пространстве имен пользователя -->
<xs:element name="Shiporder" type="tns:ShiporderType"/>

Для корректного определения корневого элемента ShiporderType в XML-схеме используем объявление <xs:element>.

Вот полный код файла схемы:

shiporder.xsd
<?xml version="1.1" encoding="UTF-8"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:tns="my.namespace"
targetNamespace="my.namespace"
elementFormDefault="qualified">
<!-- Тип данных для объекта Позиция заказа -->
<xs:complexType name="ItemType">
<xs:sequence>
<xs:element name="title" type="xs:string" />
<xs:element name="note" type="xs:string" minOccurs="0" />
<xs:element name="quantity" type="xs:integer" />
<xs:element name="price" type="xs:decimal" />
</xs:sequence>
</xs:complexType>
<!-- Тип данных для объекта Грузополучатель -->
<xs:complexType name="ShiptoType">
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="address" type="xs:string" />
<xs:element name="city" type="xs:string" />
<xs:element name="country" type="xs:string" />
</xs:sequence>
</xs:complexType>
<!-- Тип данных для объекта позиции заказа -->
<xs:complexType name="ItemsArray">
<xs:sequence>
<xs:element name="Item" type="tns:ItemType" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<!-- Тип данных для объекта Заказ -->
<xs:complexType name="ShiporderType">
<xs:sequence>
<xs:element name="orderperson" type="xs:string" />
<xs:element name="shipto" type="tns:ShiptoType" />
<xs:element name="items" type="tns:ItemsArray" />
</xs:sequence>
<xs:attribute name="orderid" type="xs:integer" use="required"/>
</xs:complexType>
<!-- Создание корневого элемента в пространстве имен пользователя -->
<xs:element name="Shiporder" type="tns:ShiporderType"/>
</xs:schema>

Для проверки корректности сформированной схемы воспользуемся любым онлайн-сервисом для проверки XML на соответствие XSD, например XML Schema Validator.

WSDL (Web Services Description Language)

Следующим этапом разберем WSDL —язык описания веб-сервисов и доступа к ним, основанный на XML. Он используется для описания функциональности, предоставляемой веб-сервисом, и определяет, как можно вызвать сервис, какие параметры он принимает и какие данные возвращает.

Как происходит взаимодействие?

  1. Сервер предоставляет WSDL

  2. Клиент читает WSDL и формирует запрос

  3. Клиент отправляет SOAP-запрос

  4. Сервер обрабатывает запрос и возвращает SOAP-ответ

  5. Клиент парсит ответ и работает с данными

Структура WSDL 1.1 включает следующие основные компоненты:

  • Типы данных (types) — определение формата XML-сообщений, которые отправляет и получает сервис

  • Сообщения (message) — описание данных, используемых веб-сервисом

  • Порт-типы (portType) — список операций, которые может выполнять сервис

  • Связывание (binding) — определение способа доставки сообщений

  • Сервис (service) — адрес (эндпоинт), по которому доступен сервис

Схема WSDL-сервиса
Схема WSDL-сервиса

Забегая немного вперед WSDL, который мы будем рассматривать, автоматически сгенерирован веб-приложением, которое мы разработаем с вами чуть позже.

Начнем с описания с компонента описания сервиса <service>.

<wsdl:service name="OrderService">
        <wsdl:port name="Application" binding="tns:Application">
            <wsdlsoap11:address location="http://localhost:8000/" />
        </wsdl:port>
    </wsdl:service>

Элементы в WSDL-документе принадлежат пространству имен wsdl (обычно объявляемому как xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"), потому что они являются частью стандарта WSDL (Web Services Description Language) — языка описания веб-служб на основе XML.

Элемент <wsdl:service>

  • name="OrderService" - определяет имя сервиса как "OrderService". Это имя, по которому клиенты могут ссылаться на этот сервис.

Элемент <wsdl:port>

  • name="Application" - задаёт имя порта (точки входа) как "Application".

  • binding="tns:Application" - указывает, что этот порт использует привязку (binding) с именем "Application" из текущего пространства имён (tns - this namespace).

Элемент <wsdlsoap11:address>

  • location="http://localhost:8000/" - определяет физический адрес (URL), по которому доступен этот сервис. В данном случае сервис доступен по адресу http://localhost:8000/ на локальной машине.

<wsdl:binding name="Application" type="tns:Application">
        <wsdlsoap11:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="Shiporder">
            <wsdlsoap11:operation soapAction="Shiporder" style="document" />
            <wsdl:input name="Shiporder">
                <wsdlsoap11:body use="literal" />
            </wsdl:input>
            <wsdl:output name="ShiporderResponse">
                <wsdlsoap11:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>

Следующий компонент binding описывает привязку веб-службы, которая определяет, как абстрактные операции (из <portType>) преобразуются в конкретный протокол (SOAP 1.1 в нашем случае).

Элемент <wsdl:binding>

  • name="Application" — имя этой привязки (используется в <port> для ссылки).

  • type="tns:Application" — указывает на <portType>, который описывает абстрактные операции сервиса.

Элемент <wsdlsoap11:binding>

  • style="document" — стиль SOAP-сообщений: document или RPC.

  • transport="http://schemas.xmlsoap.org/soap/http" — протокол передачи: HTTP (указывает, что используется SOAP over HTTP).

Операция <wsdl:operation name="Shiporder">

  • <wsdlsoap11:operation> — настройки операции для SOAP:

    • soapAction="Shiporder" — HTTP-заголовок SOAPAction, который идентифицирует операцию (используется сервером для маршрутизации).

    • style="document" совпадает с стилем привязки в <wsdl:binding>.

  • <wsdl:input> и <wsdl:output> — описывают формат входящих/исходящих сообщений:

    • <wsdlsoap11:body use="literal" /> — означает, что SOAP-тело содержит данные в точном соответствии с XSD-схемой (без SOAP-специфичного кодирования).

<wsdl:portType name="Application">
        <wsdl:operation name="Shiporder" parameterOrder="Shiporder">
            <wsdl:input name="Shiporder" message="tns:Shiporder" />
            <wsdl:output name="ShiporderResponse" message="tns:ShiporderResponse" />
        </wsdl:operation>
    </wsdl:portType>

Компонент Порт-типы (portType) определяет абстрактный интерфейс веб-сервиса, описывающий его операции и обмен сообщениями, без привязки к конкретной реализации или протоколу.

  • Элемент <wsdl:portType>

    • name="Application" - задает имя порт-типа, которое будет использоваться для ссылки в других частях WSDL (например, в <binding>)

  • Элемент <wsdl:operation> определяет операцию с именем "Shiporder"

    • parameterOrder="Shiporder" - указывает порядок параметров

  • Элементы обмена сообщениями:

    • <wsdl:input> - входящее сообщение

      name="Shiporder" - имя входного сообщения

      message="tns:Shiporder" - ссылка на определение сообщения в пространстве имен tns

    • <wsdl:output> - исходящее сообщение

      name="ShiporderResponse" - имя выходного сообщения

      message="tns:ShiporderResponse" - ссылка на определение ответного сообщения

<wsdl:message name="Shiporder">
        <wsdl:part name="Shiporder" element="tns:Shiporder" />
    </wsdl:message>
    <wsdl:message name="ShiporderResponse">
        <wsdl:part name="ShiporderResponse" element="tns:ShiporderResponse" />
    </wsdl:message>

Элемент <wsdl:message> определяет формат данных, которыми обмениваются клиент и сервер при вызове операций веб-сервиса. Это абстрактное описание сообщений, которые будут передаваться через сеть.

  • Атрибут name:

    • Уникальное имя сообщения в рамках WSDL-документа

    • Используется для ссылки в элементах <portType>

  • Элемент <wsdl:part>:

    • Определяет составные части сообщения

    • name - имя части сообщения

    • element (или type в старых версиях) - ссылка на XML-элемент или тип, определённый в секции <types>

  • Пространство имён tns::

    • Ссылается на типы, определённые в текущем WSDL (target namespace)

    • tns:Shiporder и tns:ShiporderResponse должны быть определены в секции <types>

Завершающий компонент <wsdl:types> - является ключевым разделом WSDL-документа, который определяет структуры данных (типы и элементы XML), используемые веб-сервисом. Это основа для описания формата сообщений в операциях сервиса.

Вся структура данных описывается с использованием XML Schema Definition, которую мы с вами подготовили ранее. Система включила в этот компонент элемент с описанием ответа.

<xs:complexType name="ShiporderResponse">
          <xs:sequence>
                <xs:element name="ShiporderResult" type="xs:string" />
          </xs:sequence>
</xs:complexType>

 Итоговый WSDL выглядит следующим образом:

Итоговый WSDL
<?xml version='1.1' encoding='UTF-8'?>
<wsdl:definitions xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:plink="http://schemas.xmlsoap.org/ws/2003/05/partner-link/"
    xmlns:wsdlsoap11="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:wsdlsoap12="http://schemas.xmlsoap.org/wsdl/soap12/"
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:soap11enc="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:soap11env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:soap12env="http://www.w3.org/2003/05/soap-envelope"
    xmlns:soap12enc="http://www.w3.org/2003/05/soap-encoding"
    xmlns:wsa="http://schemas.xmlsoap.org/ws/2003/03/addressing"
    xmlns:xop="http://www.w3.org/2004/08/xop/include"
    xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:tns="my.namespace"
    targetNamespace="my.namespace" name="Application">
    <wsdl:types>
        <xs:schema targetNamespace="my.namespace" elementFormDefault="qualified">
            <xs:complexType name="Item">
                <xs:sequence>
                    <xs:element name="title" type="xs:string" />
                    <xs:element name="note" type="xs:string" minOccurs="0" />
                    <xs:element name="quantity" type="xs:integer" />
                    <xs:element name="price" type="xs:decimal" />
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="Shipto">
                <xs:sequence>
                    <xs:element name="name" type="xs:string" />
                    <xs:element name="address" type="xs:string" />
                    <xs:element name="city" type="xs:string" />
                    <xs:element name="country" type="xs:string" />
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="ShiporderResponse">
                <xs:sequence>
                    <xs:element name="ShiporderResult" type="xs:string" />
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="ItemArray">
                <xs:sequence>
                    <xs:element name="Item" type="tns:Item" maxOccurs="unbounded" />
                </xs:sequence>
            </xs:complexType>
            <xs:complexType name="Shiporder">
                <xs:sequence>
                    <xs:element name="orderperson" type="xs:string" />
                    <xs:element name="shipto" type="tns:Shipto" />
                    <xs:element name="items" type="tns:ItemArray" />
                </xs:sequence>
                <xs:attribute name="orderid" type="xs:integer" />
            </xs:complexType>
            <xs:element name="Item" type="tns:Item" />
            <xs:element name="Shipto" type="tns:Shipto" />
            <xs:element name="ShiporderResponse" type="tns:ShiporderResponse" />
            <xs:element name="ItemArray" type="tns:ItemArray" />
            <xs:element name="Shiporder" type="tns:Shiporder" />
        </xs:schema>
    </wsdl:types>
<!-- Сообщения процедуры -->
    <wsdl:message name="Shiporder">
        <wsdl:part name="Shiporder" element="tns:Shiporder" />
    </wsdl:message>
    <wsdl:message name="ShiporderResponse">
        <wsdl:part name="ShiporderResponse" element="tns:ShiporderResponse" />
    </wsdl:message>
<!-- Определение сервиса -->
    <wsdl:service name="OrderService">
        <wsdl:port name="Application" binding="tns:Application">
            <wsdlsoap11:address location="http://localhost:8000/" />
        </wsdl:port>
    </wsdl:service>
<!-- Привязка процедуры к сообщениям -->
    <wsdl:portType name="Application">
        <wsdl:operation name="Shiporder" parameterOrder="Shiporder">
            <wsdl:input name="Shiporder" message="tns:Shiporder" />
            <wsdl:output name="ShiporderResponse" message="tns:ShiporderResponse" />
        </wsdl:operation>
    </wsdl:portType>
<!-- Формат процедур веб-сервиса -->
    <wsdl:binding name="Application" type="tns:Application">
        <wsdlsoap11:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="Shiporder">
            <wsdlsoap11:operation soapAction="Shiporder" style="document" />
            <wsdl:input name="Shiporder">
                <wsdlsoap11:body use="literal" />

Для наглядности представим структуру WSDL-документа в виде иерархического дерева. Это поможет лучше понять взаимосвязь компонентов и их назначение.

Дерево взаимосвязи компонентов
Дерево взаимосвязи компонентов

SOAP-сообщение

SOAP-сообщение — это структурированный XML-документ, используемый для обмена данными в распределённых вычислительных средах по протоколу SOAP (Simple Object Access Protocol).

Данное сообщение представляет собой стандартизированный формат передачи информации между приложениями и состоит из следующих обязательных и необязательных элементов:

  • Envelope (конверт) - корневой элемент. Определяет начало и конец SOAP-сообщения, содержит все необходимые пространства имен (namespace)

  • Header (заголовок) - необязательная часть. Может включать информацию об аутентификации, идентификаторы транзакций, данные о маршрутизации, прочие служебные данные

  • Body (тело) - основная часть сообщения. Содержит сообщение, вызов RPC или информацию об ошибке

  • Ошибка (fault) — необязательный элемент для передачи информации об ошибках обработки

Схема SOAP-сообщения
Схема SOAP-сообщения

Наше с вами сообщение будет иметь следующий вид:

Конверт SOAP
<?xml version="1.1" encoding="UTF-8"?>
<soap:Envelope 
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:tns="my.namespace"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    
    <soap:Header>
        <!-- Здесь могут быть дополнительные заголовки SOAP -->
    </soap:Header>
    
    <soap:Body>
        <tns:Shiporder orderid="12345" 
            xsi:schemaLocation="my.namespace shiporder.xsd">
            
            <tns:orderperson>Иван Петров</tns:orderperson>
            
            <tns:shipto>
                <tns:name>Петр Иванов</tns:name>
                <tns:address>ул. Ленина</tns:address>
                <tns:city>Москва</tns:city>
                <tns:country>Россия</tns:country>
            </tns:shipto>
            
            <tns:items>
                <tns:Item>
                    <tns:title>Ручка гелевая</tns:title>
                    <tns:note>Цвет: черный</tns:note>
                    <tns:quantity>10</tns:quantity>
                    <tns:price>25.00</tns:price>
                </tns:Item>
                <tns:Item>
                    <tns:title>Калькулятор</tns:title>
                    <tns:quantity>5</tns:quantity>
                    <tns:price>350.00</tns:price>
                </tns:Item>
            </tns:items>
        </tns:Shiporder>
    </soap:Body>
</soap:Envelope>

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

Создаем SOAP-сервер

Теперь перейдем к разработке серверной части веб-приложения.

В качестве транспорта мы будем использовать HTTP протокол. Процесс взаимодействия пользователя с системой выглядит следующим образом.

Диаграмма последовательности взаимодействия с SOAP-сервисом
Диаграмма последовательности взаимодействия с SOAP-сервисом

Диаграмма создана при помощи PlantUML-скрипта:

Скрытый текст
@startuml
actor User
participant Client
participant Server

User -> Client: Запрос на отгрузку
activate Client
Client -> Server: GET /service/?wsdl HTTP/1.1\nHost: example.com\nAccept: text/xml
activate Server
Server --> Client: 200 OK HTTP/1.1\nContent-Type: text/xml\nWSDL-схема
deactivate Server

Client -> Client: Сформировать SOAP-запрос
Client -> Server: POST /service HTTP/1.1\nHost: example.com\nContent-Type: text/xml\nSOAPAction: "urn:shiporder"
activate Server
Server -> Server: Валидация XML
Server -> Server: Обработка Shiporder
Server -> Server: Формирование ShiporderResponse
Server --> Client: 200 OK HTTP/1.1\nContent-Type: text/xml\nSOAPAction: "urn:shiporderResponse"
deactivate Server
Client --> User: Статус отгрузки
deactivate Client

alt Ошибка обработки
    Server -> Server: Обнаружена ошибка\n(XMLSyntaxError/BusinessError)
    activate Server
    Server --> Client: 500 Internal Server Error\nContent-Type: text/xml\nSOAP Fault
    deactivate Server
    Client --> User: Сообщение об ошибке:\nкод и описание
end
@enduml

Для разработки SOAP-сервера на Python мы будем использовать библиотеки Spyne и Zeep. Перед началом работы необходимо убедиться, что установлена версия Python 3.11.9, поскольку библиотека Spyne несовместима с более новыми версиями.

После того, как все установлено и настроено, импортируем модули и функции для создания и запуска веб-сервиса.

from spyne import Application, rpc, ServiceBase, Iterable, Unicode, Decimal
from spyne.model.complex import ComplexModel
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication
from wsgiref.simple_server import make_server
from spyne.model.primitive import Integer, String
from spyne.model import XmlAttribute

Основные компоненты импорта:

  • Application — базовый класс для создания приложения

  • ServiceBase — базовый класс для создания сервисов

  • rpc — декоратор для определения удалённых процедур

  • Iterable, Unicode, Decimal — типы данных для работы с различными форматами

  • Integer, String — примитивные типы данных

  • XmlAttribute — класс для работы с XML-атрибутами

  • ComplexModel — базовый класс для создания сложных моделей данных

  • Soap11 — протокол SOAP версии 1.1

  • WsgiApplication — адаптер для работы с WSGI-сервером

  • make_server — функция для создания простого WSGI-сервера

Как мы с вами говорили ранее, для функционирования SOAP-сервиса ему необходим WSDL-файл, который наш сервер генерирует автоматически. Поэтому одной из наших задач будет описание элементов компонента <types> в соответствии с разработанной нами схемой XSD.

# Определение объекта Shipto
class Shipto(ComplexModel):
    name = String(min_occurs=1, nillable=False)
    address = String(min_occurs=1, nillable=False)
    city = String(min_occurs=1, nillable=False)
    country = String(min_occurs=1, nillable=False)

# Определение объекта Item
class Item(ComplexModel):
    title = String(min_occurs=1, nillable=False)
    note = String(nillable=False)
    quantity = Integer(min_occurs=1, nillable=False)
    price = Decimal(min_occurs=1, nillable=False)

# Определение атрибутов объектов
Orderperson = String(min_occurs=1, nillable=False)
Shipto = Shipto.customize(min_occurs=1, nillable=False)
Item = Item.customize(min_occurs=1, nillable=False)
Items = Iterable(Item, min_occurs=1, nillable=False)
Orderid = XmlAttribute(Integer, use="required")

Классы Shipto и Item представляют собой модели сложных типов (complexType) XSD-схемы и наследуются от базового класса ComplexModel. Параметры атрибутов XML-элементов задаются через метод customize экземпляров этих классов.

Для работы с коллекциями в Spyne используется специальный тип Iterable. В отличие от complexType, параметры атрибутов элементов передаются напрямую в конструктор класса.

Аналогичным образом функционируют классы, описывающие примитивные типы данных (IntegerString).

Для определения параметров компонента <xs:attribute> применяется класс XmlAttribute, где все необходимые параметры задаются непосредственно в конструкторе класса.

            Далее переходим к созданию самого сервиса:

# Сервис
class OrderService(ServiceBase):
    @rpc(Orderid, Orderperson, Shipto, Items, _returns=Unicode(min_occurs=1, nillable=False))
    def Shiporder(ctx, orderid, orderperson, shipto, items):
        return f"Заказ успешно обработан. ID: {orderid}"

# Создание приложения
app = Application(
    [OrderService],
    'my.namespace',
    in_protocol=Soap11(validator='schema'),  
    out_protocol=Soap11()
)

wsgi_app = WsgiApplication(app)

# Запуск сервера
if __name__ == '__main__':
    server = make_server('0.0.0.0', 8000, wsgi_app)
    print('For WSDL type http://localhost:8000/?wsdl')
    server.serve_forever()

Класс OrderService представляет собой основной сервис, наследуемый от базового класса ServiceBase.

Метод Shiporder определен с помощью декоратора @rpc. Конструктор принимает параметры для формирования структуры WSDL. Свойство _returns является не обязательным и отвечает за ответ сервиса

Application — это ядро SOAP-сервиса. Принимает следующие параметры: список сервисов ([OrderService]), пространство имён (‘my.namespace’), входящий протокол (Soap11 с валидатором схемы), исходящий протокол (Soap11).

Класс WsgiApplication обеспечивает интеграцию с WSGI-сервером. Создает адаптер для работы с веб-сервером.

Методу make_server передаем адрес сервиса, порт и экземпляр адаптера WsgiApplication. Запускаем сервер в бесконечном цикле (serve_forever()).

После, WSDL-документ нашего сервиса будет доступен по адресу http://localhost:8000/?wsdl

Обмен данными с сервером осуществляется через http://localhost:8000.

Полный код серверной части:

SOAP-сервер
from spyne import Application, rpc, ServiceBase, Iterable, Unicode, Decimal
from spyne.model.complex import ComplexModel
from spyne.protocol.soap import Soap11
from spyne.server.wsgi import WsgiApplication
from wsgiref.simple_server import make_server
from spyne.model.primitive import Integer, String
from spyne.model import XmlAttribute

# Определение объекта Shipto
class Shipto(ComplexModel):
    name = String(min_occurs=1, nillable=False)
    address = String(min_occurs=1, nillable=False)
    city = String(min_occurs=1, nillable=False)
    country = String(min_occurs=1, nillable=False)

# Определение объекта Item
class Item(ComplexModel):
    title = String(min_occurs=1, nillable=False)
    note = String(nillable=False)
    quantity = Integer(min_occurs=1, nillable=False)
    price = Decimal(min_occurs=1, nillable=False)

# Определение атрибутов объектов
Orderperson = String(min_occurs=1, nillable=False)
Shipto = Shipto.customize(min_occurs=1, nillable=False)
Item = Item.customize(min_occurs=1, nillable=False)
Items = Iterable(Item, min_occurs=1, nillable=False)
Orderid = XmlAttribute(Integer, use="required")

# Сервис
class OrderService(ServiceBase):
    @rpc(Orderid, Orderperson, Shipto, Items, _returns=Unicode(min_occurs=1, nillable=False))
    def Shiporder(ctx, orderid, orderperson, shipto, items):
        return f"Заказ успешно обработан. ID: {orderid}"

# Создание приложения
app = Application(
    [OrderService],
    'my.namespace',
    in_protocol=Soap11(validator='schema'),  
    out_protocol=Soap11()
)

wsgi_app = WsgiApplication(app)

# Запуск сервера
if __name__ == '__main__':
    server = make_server('0.0.0.0', 8000, wsgi_app)
    print('For WSDL type http://localhost:8000/?wsdl')
    server.serve_forever()

Тестирование серверной части сервиса

Создав серверную часть, хочется ее конечно протестировать. Что же у нас с вами получилось?

Генерацию WSDL-документа проверяем по адресу http://localhost:8000/?wsdl. Для тестирования обмена данными можно воспользоваться, например Postman или SoapUI.

Остановим свой выбор на Postman. Создаём новый HTTP-запрос через меню «New» → «HTTP».

В поле адреса указываем URL SOAP-сервиса: http://localhost:8000, и метод POST.

Вкладка Body
Вкладка Body

Переходим во вкладку Body и выбираем формат raw (XML). В области тела запроса указываем наш SOAP-запрос.

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

 Ответ сервера
Ответ сервера

Ответ пришел. SOAP-сервис работает.

Создаем SOAP-клиент

И последний шаг. Создадим SOAP-клиент, который запросит WSDL-описание у сервера, сформирует SOAP-запрос, и обменяется с сервером сообщениями.

Импортируем модули и функции для создания и запуска клиента:

import logging
from zeep import Client
from zeep.plugins import HistoryPlugin
from lxml import etree

Основные компоненты импорта:

  • logging - стандартная библиотека Python для ведения логов

  • zeep - популярная Python-библиотека для работы с SOAP веб-сервисами

  • Client - основной класс библиотеки, который используется для создания соединения с SOAP-сервисом

  • HistoryPlugin - плагин для библиотеки zeep

  • lxml - мощная библиотека для обработки XML в Python

  • etree - модуль для работы с XML-документами

Для отслеживания входящих и исходящих SOAP-сообщений сконфигурируем функцию логирования.

# Логгирование
logging.basicConfig(level=logging.INFO)
logging.getLogger('zeep.transports').setLevel(logging.DEBUG)

# История сообщений
history = HistoryPlugin()

Создаем экземпляр клиента, передав в конструктор адрес ресурса WSDL-схемы и функцию логирования

# URL WSDL
wsdl_url = "http://localhost:8000/?wsdl"

# Клиент с плагином
client = Client(wsdl=wsdl_url, plugins=[history])

Готовим данные запроса в формате JSON

# Данные запроса
shiporder_data = {
    "orderid": "12345",
    "orderperson": "Иван Петров",
    "shipto": {
        "name": "Петр Иванов",
        "address": "ул. Ленина",
        "city": "Москва",
        "country": "Россия"
    },
    "items": {
        "Item": [
            {
                "title": "Ручка гелевая",
                "note": "Цвет: черный",
                "quantity": 10,
                "price": 25.00
            },
            {
                "title": "Калькулятор",
                "quantity": 5,
                "price": 350.00
            }
        ]
    }
}

Выполняем вызов метода Shiporder через SOAP-клиент, передавая данные запроса в виде именованных параметров. Анализируем логирование и проверяем успешность выполнения операции.

# Вызов метода
response = client.service.Shiporder(**shiporder_data)

# Печать сырого SOAP-запроса и ответа
print("SOAP Request:")
print(etree.tostring(history.last_sent["envelope"], pretty_print=True, encoding="unicode"))

# Печать полученного SOAP-ответа
print("\nSOAP Response:")
print(etree.tostring(history.last_received["envelope"], pretty_print=True, encoding="unicode"))

# Результат
print("\nParsed Response:")
print(response)

Полный код клиентской части сервиса:

SOAP-клиент
import logging
from zeep import Client
from zeep.plugins import HistoryPlugin
from lxml import etree

# Логгирование
logging.basicConfig(level=logging.INFO)
logging.getLogger('zeep.transports').setLevel(logging.DEBUG)

# История сообщений
history = HistoryPlugin()

# URL WSDL
wsdl_url = "http://localhost:8000/?wsdl"

# Клиент с плагином
client = Client(wsdl=wsdl_url, plugins=[history])

# Данные запроса
shiporder_data = {
    "orderid": "12345",
    "orderperson": "Иван Петров",
    "shipto": {
        "name": "Петр Иванов",
        "address": "ул. Ленина",
        "city": "Москва",
        "country": "Россия"
    },
    "items": {
        "Item": [
            {
                "title": "Ручка гелевая",
                "note": "Цвет: черный",
                "quantity": 10,
                "price": 25.00
            },
            {
                "title": "Калькулятор",
                "quantity": 5,
                "price": 350.00
            }
        ]
    }
}

# Вызов метода
response = client.service.Shiporder(**shiporder_data)

# Печать сырого SOAP-запроса и ответа
print("SOAP Request:")
print(etree.tostring(history.last_sent["envelope"], pretty_print=True, encoding="unicode"))

# Печать полученного SOAP-ответа
print("\nSOAP Response:")
print(etree.tostring(history.last_received["envelope"], pretty_print=True, encoding="unicode"))

# Результат
print("\nParsed Response:")
print(response)

Результат нашего запроса к SOAP-сервису.

Результат работы SOAP-клиента
Результат работы SOAP-клиента

Заключение

Вот и все! Теперь, когда мы разобрали основы SOAP и умеем настраивать взаимодействие между системами с помощью этого протокола, можно продолжить самостоятельно экспериментировать с данными, параметрами протокола и кодом, совершенствуя сервис и конечно свои навыки.

Что освоено:

  • XML, XSD и WSDL – у вас появилось понимание, как устроены эти документы и зачем они нужны.

  • SOAP-запросы и ответы – вы знаешь, как формируются сообщения и какие у них особенности.

  • Клиент-серверное взаимодействие – вы разобрались, как системы общаются через SOAP.

Также получены ценные практические навыки:

  • Разработка SOAP-клиентов – умеете подключаться к внешним сервисам и отправлять запросы.

  • Создание SOAP-серверов – можете настроить свою службу, чтобы другие системы могли к ней обращаться.

  • Работа с инструментами – освоили библиотеки для работы с SOAP

Эти знания могут послужить фундаментом для работы с корпоративными системами, банковскими API, ERP-интеграциями и другими сложными сервисами.

Комментарии (2)


  1. Politura
    29.06.2025 21:16

    Просто интересно, кто-нибудь SOAP сейчас использует за пределами дремучего легаси? Мне кажется последний раз его видел на живых проектах лет 10 назад.


    1. Grrr5
      29.06.2025 21:16

      Это основа, это база!

      Мне было интересно почитать, автору респект, что не просто туториал - а вникал, разбирал.