При активном взаимодействии с внешними REST- и HTTP-сервисами часто возникает архитектурная задача: необходимо отправить десятки или сотни запросов к API (например, обход пачки измененных заказов). Если на каждую итерацию вызывать конструктор Новый HTTPСоединение, операционная система сталкивается с лавинообразным открытием сетевых сокетов, что ведет к накладным расходам и рискам блокировки. При этом «протаскивать» объект соединения через сигнатуры десятков вспомогательных методов бизнес-логики крайне неудобно. Разберем, как устроена работа сетевого стека платформы 1С и какими способами можно эффективно организовать повторное использование соединений.
Проанализируем механику работы платформы 1С. Конструктор Новый HTTPСоединение(...) выполняет так называемую ленивую инициализацию: физическое TCP-подключение и рукопожатие TLS происходят только в момент вызова одного из методов передачи данных (Получить, ОтправитьДляОбработки и т. д.).
По умолчанию 1С работает по стандарту HTTP/1.1 с включенным механизмом Keep-Alive. Если мы используем один и тот же созданный экземпляр объекта HTTPСоединение, платформа удерживает открытый сокет и отправляет повторные запросы внутри существующей TCP-сессии.
Если же создавать объект заново на каждом шаге цикла, ситуация на уровне операционной системы выглядит иначе. Посмотрим на сравнительный тест работы с локальным сервисом на 10 запросов.
Вариант 1 (один экземпляр соединения на цикл):
&НаСервереБезКонтекста
Процедура ТестНаСервере()
Соединение = Новый HTTPСоединение("localhost", 3000);
Для Счетчик = 1 По 10 Цикл
Запрос = Новый HTTPЗапрос("/hello");
Ответ = Соединение.Получить(Запрос);
КонецЦикла;
КонецПроцедуры
Проверка сетевых сокетов утилитой мониторинга соединений показывает единственную открытую сессию:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
ESTAB 0 0 127.0.0.1:39308 127.0.0.1:3000
Вариант 2 (создание нового объекта на каждой итерации):
&НаСервереБезКонтекста
Процедура ТестНаСервере()
Для Счетчик = 1 По 10 Цикл
Соединение = Новый HTTPСоединение("localhost", 3000);
Запрос = Новый HTTPЗапрос("/hello");
Ответ = Соединение.Получить(Запрос);
КонецЦикла;
КонецПроцедуры
В этом случае в операционной системе возникает пачка соединений, переходящих в состояние ожидания закрытия:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
TIME-WAIT 0 0 127.0.0.1:34790 127.0.0.1:3000
TIME-WAIT 0 0 127.0.0.1:34804 127.0.0.1:3000
...
TIME-WAIT 0 0 127.0.0.1:57646 127.0.0.1:3000
При большом потоке запросов создание объектов без переиспользования приводит к эффекту исчерпания диапазона динамических портов (Ephemeral Port Exhaustion), ошибкам сетевого стека ОС и блокировкам со стороны внешнего сервиса за превышение лимита параллельных сессий. Выясним, как архитектурно правильно решить проблему повторного использования в 1С.
Платформа 1С предоставляет встроенный механизм кэширования возвращаемых значений функций через свойства общих модулей. Мы можем вынести получение соединения в модуль со свойством Повторное использование возвращаемых значений: На время вызова или На время сеанса.
Создадим функцию в общем модуле ИнтеграцияПовтИсп:
Функция ПолучитьHTTPСоединение(ИмяСервера, Порт, ИдентификаторКэша = Неопределено) Экспорт
Возврат Новый HTTPСоединение(ИмяСервера, Порт);
КонецФункции
При вызове функции из любого места конфигурации 1С вернет уже существующий объект HTTPСоединение, если параметры вызова совпадают. Это позволяет не протаскивать переменную соединения через параметры прикладных процедур создания документов и проведения проверок.
Важные нюансы реализации:
ОбновитьПовторноИспользуемыеЗначения().Посмотрим на практический шаблон отказоустойчивой обертки:
Функция ВыполнитьЗапрос(Запрос, ИмяСервера, Порт, ИдентификаторКэша = Неопределено) Экспорт
Если ИдентификаторКэша = Неопределено Тогда
ИдентификаторКэша = Новый УникальныйИдентификатор;
КонецЕсли;
Соединение = ИнтеграцияПовтИсп.ПолучитьHTTPСоединение(ИмяСервера, Порт, ИдентификаторКэша);
Попытка
Возврат Соединение.Получить(Запрос);
Исключение
// В случае разрыва сессии генерируем новый ключ для создания свежего соединения
ИдентификаторКэша = Новый УникальныйИдентификатор;
Соединение = ИнтеграцияПовтИсп.ПолучитьHTTPСоединение(ИмяСервера, Порт, ИдентификаторКэша);
Возврат Соединение.Получить(Запрос);
КонецПопытки;
КонецФункции
Второй классический подход объектно-ориентированного проектирования — создание класса-клиента на базе внешней или встроенной обработки. В модуле объекта объявляются внутренние переменные, сохраняющие состояние на протяжении жизни экземпляра.
Разберем по шагам создание такого API-клиента:
ВнешнееАПИКлиент.
Перем мСоединение;
Процедура Инициализировать(ИмяСервера, Порт) Экспорт
мСоединение = Новый HTTPСоединение(ИмяСервера, Порт);
КонецПроцедуры
Функция ПолучитьЗаказ(ИдентификаторЗаказа) Экспорт
Запрос = Новый HTTPЗапрос("/api/orders/" + ИдентификаторЗаказа);
Ответ = мСоединение.Получить(Запрос);
Возврат Ответ.ПолучитьТелоКакСтроку();
КонецФункции
Функция ПолучитьОплату(ИдентификаторОплаты) Экспорт
Запрос = Новый HTTPЗапрос("/api/payments/" + ИдентификаторОплаты);
Ответ = мСоединение.Получить(Запрос);
Возврат Ответ.ПолучитьТелоКакСтроку();
КонецФункции
В вызывающем коде мы оперируем готовым клиентом API:
КлиентAPI = Обработки.ВнешнееАПИКлиент.Создать();
КлиентAPI.Инициализировать("api.service.com", 443);
Для Каждого ИдЗаказа Из МассивИдентификаторов Цикл
ДанныеЗаказа = КлиентAPI.ПолучитьЗаказ(ИдЗаказа);
// Обработка данных и запись в базу
КонецЦикла;
Такой подход обеспечивает изоляцию низкоуровневых параметров подключения (токены, заголовки, порты) внутри модуля объекта и гарантирует, что все обращения выполняются через один открытый сокет.
При проектировании высоконагруженных интеграций необходимо учитывать платформенные особенности 1С:
Попытка ... Исключение с логикой повторной отправки (retry).HTTPСоединение фиксирует IP-адрес хоста при первом обращении. Если удаленный сервис использует частую смену IP по механизму балансировки Round-Robin DNS, слишком долго живущий объект может приводить к ошибкам доставки пакетов до момента его пересоздания.