Появилась потребность реализовать интеграцию с внутренними сервисами через Kafka, используя протокол Protobuf. Почему именно Protobuf? Потому что целевые сервисы-приемники реализованы на golang, для которого Protobuf как КД 2/КД 3 для 1С, и в целом в контуре был принят внутренний стандарт по использованию proto. Хотя скорее всего разработчики просто ленивые и не хотели сами писать парсер сообщений.
Что такое Protobuf?
Protocol Buffers (Protobuf) — протокол сериализации данных, разработанный компанией Google. Предназначен для эффективного и быстрого обмена структурированными данными между разными компьютерными системами, языками программирования и платформами.
Источник
Почему Protobuf?
- Компактность по сравнению с текстовыми форматами, такими как XML или JSON.
- Скорость сериализации и десериализации данных из-за оптимизированной структуры данных.
- Языковая независимость — протокол поддерживает генерацию кода на множестве популярных языков программирования.
- Обратная совместимость — можно добавлять новые поля или изменять схему данных, не разрушая работу уже существующих систем, которые используют старую версию схемы.
Источник
Как работает Protobuf?
Протокол работает в несколько этапов:
- Описание структуры данных в файле с расширением .proto. В этом файле используется простой синтаксис, независимый от языка программирования.
- Генерация кода с помощью компилятора protoc. Он генерирует код для работы с этой структурой на нужном языке.
- Сериализация и десериализация данных — после генерации кода можно работать с данными в бинарном формате, используя методы сериализации и десериализации, созданные компилятором.
Источник
Т.е. если вкратце, то протокол Protobuf - это такой стильный-модный-молодежный а-ля SOAP со строгой типизацией, структурой и т.д. Сам себя проверяет, позволяет генерировать парсеры сообщений под нужный стек на основании описанной структуры, сам моет сам убирает, короче огонь! Но тут на сцену выходит платформа 1С.. вся в белом желтом.. и средств для работы с протоколом конечно же нет. Почти.
На помощь загрустившим нам и платформе в желтом выходит, например, python + возможность вызывать скрипты средствами платформы.
Идея не новая, реализация следующая:
готовим скрипт на python, который на вход получает файл JSON + proto-описание сообщения
И тут на помощь приходит какой-нибудь ИИ, которого мы вежливо просим накидать нужный нам функционал на python. Конечно чутка все равно придется изменить/поправить/подрихтовать, но в целом вопрос закрываем.
скрипт выполняет проверку входных данных и кодировку сообщения
В параметрах командной строки будем передавать скрипту путь до файла с входными JSON-данными, путь до предварительно скомпилированного файла pb2 под нашу структуру сообщения + имя proto-класса из нашего сообщения.
на выходе получаем файл с двоичными данными результата кодировки
Файл читаем именно как двоичные данные, а не текст, иначе очень вероятна потеря данных, что повлечет невозможность корректного декодирования.
Вопрос из зала: "Почему тут везде всё на соплях файлах?". Потому что в моем случаем использовалась ОС Ubuntu Linux, а ее крутейшая парадигма гласит: "все есть файлы". Файлы - это файлы, каталоги - это файлы, устройства - это файлы, каналы/потоки ввода/вывода - это файлы, все есть файлы. В любом случае, даже если речь не о Linux, то проблемы не вижу. А для скорости достаточно выделить RAM-диск, через который гонять файлы.
Итак, что же у нас получилось?
Ставим python + protobuf. В зависимости от ОС есть варианты, но в моем случае это:
apt update
apt install -y protobuf-compiler
protoc --version
apt install python3
pip3 install protobuf
pip3 show protobuf
Обращаем внимание на версии протокола компилятора protoc и компонентов работы с протоколом того стека, под который будем писать скрипты. Бывают расхождения и неожиданности.
Создаем класс-обработку Protobuf. В макете такой обработки, например, можно хранить скрипты кодирования/декодирования, а так же proto-описание сообщений + сгенерированный код обработчиков. Опять же удобно хранить это все вместе с кодовой базой, а так же реализовывать версионирование.

Тексты скриптов содержат элементы шаблонов, вместо которых будут подставляться конкретные значения.
import sys
import json
import configparser
from google.protobuf.json_format import Parse
import #protoFilePb2_
message_source = sys.argv[1]
try:
with open(message_source, 'r') as f:
json_data = json.load(f)
except json.JSONDecodeError as e:
#print("Invalid JSON content.")
raise e
except FileNotFoundError as e:
#print("File not found.")
raise e
value_json = json.dumps(json_data, separators=(',', ':'), indent=None).encode('UTF-8')
value_proto = Parse(value_json, #protoFilePb2_.#protoClassName_())
value_bytes = value_proto.SerializeToString()
with open(sys.argv[2], 'wb') as w:
w.write(value_bytes)
import sys
import json
import configparser
from google.protobuf.json_format import MessageToJson
import #protoFilePb2_
message_source = sys.argv[1]
try:
protoClass = #protoFilePb2_.#protoClassName_()
with open(message_source, 'rb') as f:
protoClass.ParseFromString(f.read())
value_json = MessageToJson(protoClass)
except json.JSONDecodeError as e:
print("Invalid JSON content.")
raise e
except FileNotFoundError as e:
print("File not found.")
raise e
with open(sys.argv[2], 'wt') as w:
w.write(value_json)
Готовим proto-описание сообщения. Пусть это будет JSON-объект с единственным полем, содержащим числовое значение.
syntax = "proto3";
message TestClass {
int32 test = 1;
}
Кстати, для proto-сообщения, как уже было выше указано, важен порядок и типизация полей. Однако, допускается расширение состава полей путем их добавления "вниз", и все это без необходимости менять proto в приемнике. Хоть какая-то радость.
Сгенерируем код обработки нашего сообщения, и положим его в макет обработки. Генерация кода выглядит как:
protoc --python_out=. testclass.proto
На выходе получаем файл vb2 примерно такого содержания
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# NO CHECKED-IN PROTOBUF GENCODE
# source: testclass.proto
# Protobuf Python Version: 5.28.2
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.PUBLIC,
5,
28,
2,
'',
'testclass.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ftestclass.proto\"\x19\n\tTestClass\x12\x0c\n\x04test\x18\x01 \x01(\x05\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'testclass_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_TESTCLASS']._serialized_start=19
_globals['_TESTCLASS']._serialized_end=44
# @@protoc_insertion_point(module_scope)
Ничего непонятно, но очень интересно.
Далее как в туториалах по рисованию: рисуем первый кружок, потом второй кружок, потом ахалай-махалай, и вуаля: у нас получается обработка, которая умеет вызывать скрипт на python, передавать в него параметрами входные данные, и читать результат кодирования/декодирования.
Ладно-ладно.. примерно такой ахалай-махалай получается. Не все сервисные методы приведены, сорян, но по смыслу названия вроде понятны, можно заменить на аналоги.
Перем Слеш;
Перем Кавычки;
Перем МассивОшибок Экспорт;
Перем ТаймаутОжиданияГотовности Экспорт;
// python3
// pip3 install protobuf
// pip3 show protobuf
// protoc --python_out=. testclass.proto
//
//
Функция НовыйПараметры() Экспорт
СтруктураПараметры = Новый Структура;
СтруктураПараметры.Вставить("Ключ", СокрЛП(Новый УникальныйИдентификатор));
СтруктураПараметры.Вставить("РабочийКаталог", РабочийКаталог());
СтруктураПараметры.Вставить("ProtoPb2Макет", Неопределено); // текстовый документ с прото-описанием класса
СтруктураПараметры.Вставить("ProtoИмяКласса", Неопределено); // текстовый документ с прото-описанием класса
Возврат СтруктураПараметры;
КонецФункции
//
//
Функция РазделительАтрибутаЗначения() Возврат "::"; КонецФункции
//
//
Функция ИмяСкрипта() Возврат СтрШаблон("json2proto_%1.py", НомерСеансаИнформационнойБазы()); КонецФункции
//
//
Функция ИмяФайлаСкрипта(пРабочийКаталог) Возврат пРабочийКаталог + ИмяСкрипта(); КонецФункции
//
//
Функция ИмяКаталогаСкрипта() Возврат "json2proto"; КонецФункции
//
//
Функция РабочийКаталог()
лКаталогВременныхФайлов = СокрЛП(МодификаторЗначений.Получить(ПараметрыСеанса.systemSettings, "env.tmp.protobuf"));
Если ЗначениеЗаполнено(лКаталогВременныхФайлов) И ФайловаяСистема.КаталогСуществует(лКаталогВременныхФайлов) Тогда
лКаталогВременныхФайлов = Строки.ДополнитьСправа(лКаталогВременныхФайлов, Слеш);
Иначе
лКаталогВременныхФайлов = КаталогВременныхФайлов();
КонецЕсли;
Возврат лКаталогВременныхФайлов + СтрШаблон("protobuf_%1%2", НомерСеансаИнформационнойБазы(), Слеш);
КонецФункции
//
//
Функция ИсполняемыйФайл()
СистемнаяИнформация = Новый СистемнаяИнформация;
Возврат "python" + ?(СтрНайти(НРег(СистемнаяИнформация.ТипПлатформы), "wind") > 0, "", "3");
КонецФункции
//
//
Процедура ПодготовитьФайлСкрипта(СтруктураПараметры, ИмяМакета)
ФайловаяСистема.СоздатьКаталогНовый(СтруктураПараметры.РабочийКаталог);
Попытка УдалитьФайлы(СтруктураПараметры.РабочийКаталог, "*"); Исключение КонецПопытки;
Если НЕ МодификаторЗначений.Получить(СтруктураПараметры, "ФайлСкриптаПодготовлен") = Истина Тогда
лИмяФайлаСкрипта = ИмяФайлаСкрипта(СтруктураПараметры.РабочийКаталог);
ТекстСкрипта = ЭтотОбъект.ПолучитьМакет(ИмяМакета).ПолучитьТекст();
Если ЗначениеЗаполнено(СтруктураПараметры.ProtoPb2Макет)
И ЗначениеЗаполнено(СтруктураПараметры.ProtoИмяКласса) Тогда
ИмяМодуля = СтрШаблон("%1_%2", СтруктураПараметры.ProtoPb2Макет, НомерСеансаИнформационнойБазы());
ИмяФайлаОписаниеПротоКласса = СтруктураПараметры.РабочийКаталог + СтрШаблон("%1.py", ИмяМодуля);
ОписаниеПротоКласса = ЭтотОбъект.ПолучитьМакет(СтруктураПараметры.ProtoPb2Макет).ПолучитьТекст();
СтруктураПараметры.Вставить("ИмяФайлаОписаниеПротоКласса", ИмяФайлаОписаниеПротоКласса);
ФайлОписаниеПротоКласса = Новый Файл(СтруктураПараметры.ИмяФайлаОписаниеПротоКласса);
Если НЕ ФайлОписаниеПротоКласса.Существует() Тогда
СохранитьВФайлДляОбработкиСкриптом(ИмяФайлаОписаниеПротоКласса, ОписаниеПротоКласса);
Иначе
ТекстовыйДокументОписаниеПротоКласса = Новый ТекстовыйДокумент;
ТекстовыйДокументОписаниеПротоКласса.Прочитать(ИмяФайлаОписаниеПротоКласса, КодировкаТекста.UTF8);
Если НЕ ОписаниеПротоКласса = ТекстовыйДокументОписаниеПротоКласса.ПолучитьТекст() Тогда
СохранитьВФайлДляОбработкиСкриптом(ИмяФайлаОписаниеПротоКласса, ОписаниеПротоКласса);
КонецЕсли;
КонецЕсли;
лИмяФайлаСкрипта = СтрЗаменить(лИмяФайлаСкрипта, ИмяСкрипта(), СтрШаблон("%1_%2", СтруктураПараметры.ProtoPb2Макет, ИмяСкрипта()));
ТекстСкрипта = СтрЗаменить(ТекстСкрипта, ТегОписаниеПротоКлассаВФайлеСкрипта(), ИмяМодуля);
ТекстСкрипта = СтрЗаменить(ТекстСкрипта, ТегИмяПротоКлассаВФайлеСкрипта(), СтруктураПараметры.ProtoИмяКласса);
КонецЕсли;
ФайлСкрипта = Новый Файл(лИмяФайлаСкрипта);
Если НЕ ФайлСкрипта.Существует() Тогда
СохранитьФайл(лИмяФайлаСкрипта, ТекстСкрипта);
Иначе
ТекстовыйДокумент = Новый ТекстовыйДокумент;
ТекстовыйДокумент.Прочитать(лИмяФайлаСкрипта, КодировкаТекста.UTF8);
СтруктураПараметры.Вставить("ФайлСкриптаПодготовлен", (ТекстСкрипта = ТекстовыйДокумент.ПолучитьТекст()));
Если НЕ СтруктураПараметры.ФайлСкриптаПодготовлен = Истина Тогда
СохранитьФайл(лИмяФайлаСкрипта, ТекстСкрипта);
СтруктураПараметры.ФайлСкриптаПодготовлен = Истина;
КонецЕсли;
КонецЕсли;
СтруктураПараметры.Вставить("ИмяФайлаСкрипта", лИмяФайлаСкрипта);
КонецЕсли;
КонецПроцедуры
//
//
Функция ТегИмяПротоКлассаВФайлеСкрипта() Возврат "#protoClassName_"; КонецФункции
//
//
Функция ТегОписаниеПротоКлассаВФайлеСкрипта() Возврат "#protoFilePb2_"; КонецФункции
//
//
Процедура СохранитьФайл(ИмяФайла, Данные)
ФайлСкрипта = Новый ТекстовыйДокумент;
ФайлСкрипта.УстановитьТекст(Данные);
ФайлСкрипта.Записать(ИмяФайла, КодировкаТекста.UTF8);
ФайлСкрипта = Неопределено;
КонецПроцедуры
//
//
Функция Обернуть(Значение, Символ = "'") Возврат СтрШаблон("%1%2%1", Символ, Значение); КонецФункции
//
//
Функция КомандаОтправитьСообщение(ВременныеФайлыКонфигурации, СтруктураПараметры)
Возврат СтрШаблон("%1 -B %2 %3 %4 2>%5"
, ИсполняемыйФайл()
, Обернуть(СтруктураПараметры.ИмяФайлаСкрипта, Кавычки)
, Обернуть(ВременныеФайлыКонфигурации.ИмяВременногоФайлаЗначения, Кавычки)
, Обернуть(ВременныеФайлыКонфигурации.ИмяВременногоФайлаВывода, Кавычки)
, Обернуть(ВременныеФайлыКонфигурации.ИмяВременногоФайлаStdErr, Кавычки));
КонецФункции
//
//
Процедура СохранитьВФайлДляОбработкиСкриптом(ИмяФайла, Сообщение)
ТипЗнч_Сообщение = ТипЗнч(Сообщение);
Если ТипЗнч_Сообщение = Тип("ДвоичныеДанные") Тогда
Сообщение.Записать(ИмяФайла);
Иначе
ТекстовыйДокумент = Новый ТекстовыйДокумент;
ТекстовыйДокумент.УстановитьТекст(Сообщение);
ТекстовыйДокумент.Записать(ИмяФайла, "CESU-8");
ТекстовыйДокумент = Неопределено;
КонецЕсли;
КонецПроцедуры
//
//
Функция ПодготовитьВременныеФайлыКонфигурации(Сообщение, СтруктураПараметры) Экспорт
Результат = Новый Структура;
Результат.Вставить("ИмяВременногоФайлаЗначения", ФайловаяСистема.НовыйИмяВременногоФайла("val", СтруктураПараметры.РабочийКаталог));
Результат.Вставить("ИмяВременногоФайлаВывода", ФайловаяСистема.НовыйИмяВременногоФайла("out", СтруктураПараметры.РабочийКаталог));
Результат.Вставить("ИмяВременногоФайлаStdErr", ФайловаяСистема.НовыйИмяВременногоФайла("err", СтруктураПараметры.РабочийКаталог));
СохранитьВФайлДляОбработкиСкриптом(Результат.ИмяВременногоФайлаЗначения, Сообщение);
Возврат Результат
КонецФункции
//
//
Функция ВыполнитьСкрипт(ИмяМакета, Сообщение, СтруктураПараметрыДоп) Экспорт
Результат = Неопределено;
СтруктураПараметры = ЭтотОбъект.НовыйПараметры();
Если НЕ СтруктураПараметрыДоп = Неопределено Тогда
ЗаполнитьЗначенияСвойств(СтруктураПараметры, СтруктураПараметрыДоп);
КонецЕсли;
ПодготовитьФайлСкрипта(СтруктураПараметры, ИмяМакета);
ОписаниеОшибки = "";
Ключ = СтруктураПараметры.Ключ;
ВременныеФайлыКонфигурации = ПодготовитьВременныеФайлыКонфигурации(Сообщение, СтруктураПараметры);
Если ФайловаяСистема.ВыполнитьКоманду(КомандаОтправитьСообщение(ВременныеФайлыКонфигурации, СтруктураПараметры), ОписаниеОшибки, СтруктураПараметры.РабочийКаталог, Истина) Тогда
ОжидатьГотовности = ТаймаутОжиданияГотовности;
Пока ОжидатьГотовности > 0 Цикл
Если ФайловаяСистема.ФайлСуществует(ВременныеФайлыКонфигурации.ИмяВременногоФайлаВывода) Тогда
ОжидатьГотовности = 0;
Результат = Новый ДвоичныеДанные(ВременныеФайлыКонфигурации.ИмяВременногоФайлаВывода);
Иначе
ОжидатьГотовности = ОжидатьГотовности - 1;
ФоновыеЗаданияСервер.Пауза(1);
КонецЕсли;
КонецЦикла;
РезультатStdErr = Неопределено;
Если ФайловаяСистема.ФайлСуществует(ВременныеФайлыКонфигурации.ИмяВременногоФайлаStdErr) Тогда
РезультатStdErr = ПрочитатьДанныеФайла(ВременныеФайлыКонфигурации.ИмяВременногоФайлаStdErr);
КонецЕсли;
Если Результат = Неопределено Тогда
ОписаниеОшибки = ПарсерJSON.Кодировать(Новый Структура("protobufErr, stdErr, value", ОписаниеОшибки, РезультатStdErr, ПредставлениеСообщения(Сообщение)));
МассивОшибок.Добавить(ОписаниеОшибки);
ЛогированиеСервер.Вывести(ЛогированиеСобытия.ОшибкаЭкспорт(),, ЭтотОбъект.Метаданные(),, ОписаниеОшибки);
КонецЕсли;
ИначеЕсли ЗначениеЗаполнено(ОписаниеОшибки) Тогда
ОписаниеОшибки = ПарсерJSON.Кодировать(Новый Структура("stdErr, value",ОписаниеОшибки, "", ПредставлениеСообщения(Сообщение)));
МассивОшибок.Добавить(ОписаниеОшибки);
ЛогированиеСервер.Вывести(ЛогированиеСобытия.ОшибкаЭкспорт(),, ЭтотОбъект.Метаданные(),, ОписаниеОшибки);
КонецЕсли;
Для Каждого ЭлементВременныефайлы Из ВременныеФайлыКонфигурации Цикл Если НЕ ЭлементВременныефайлы.Значение = КаталогВременныхФайлов() Тогда ФайловаяСистема.УдалитьВременныйФайл(ЭлементВременныефайлы.Значение); КонецЕсли; КонецЦикла;
Если НЕ СтруктураПараметры.РабочийКаталог = КаталогВременныхФайлов() Тогда ФайловаяСистема.УдалитьВременныйФайл(СтруктураПараметры.РабочийКаталог); КонецЕсли;
Возврат Результат;
КонецФункции
//
//
Функция Кодировать(Сообщение, СтруктураПараметрыДоп) Экспорт
Возврат ВыполнитьСкрипт("json2proto", Сообщение, СтруктураПараметрыДоп);
КонецФункции
//
//
Функция Декодировать(Сообщение, СтруктураПараметрыДоп) Экспорт
Возврат ВыполнитьСкрипт("proto2json", Сообщение, СтруктураПараметрыДоп);
КонецФункции
//
//
Функция ПредставлениеСообщения(Сообщение)
Результат = Сообщение;
ТипЗнч_Сообщение = ТипЗнч(Сообщение);
Если ТипЗнч_Сообщение = Тип("ДвоичныеДанные") Тогда
Результат = "{binary_data}";
КонецЕсли;
Возврат Результат;
КонецФункции
//
//
Функция ПрочитатьДанныеФайла(ИмяФайла)
Результат = Неопределено;
Если ФайловаяСистема.ФайлСуществует(ИмяФайла) Тогда
ТекстовыйДокумент = Новый ТекстовыйДокумент;
ТекстовыйДокумент.Прочитать(ИмяФайла);
Результат = СокрЛП(ТекстовыйДокумент.ПолучитьТекст());
КонецЕсли;
Возврат Результат;
КонецФункции
//
//
Процедура ОчиститьДанные() Экспорт
ЭтотОбъект.МассивОшибок.Очистить();
КонецПроцедуры
//
//
Процедура УстановитьНастройкиПоУмолчанию() Экспорт
КонецПроцедуры
//
//
Функция date2googleTimestamp(ПараметрДата) Экспорт
Результат = Новый Структура("seconds, nanos", ?(ЗначениеЗаполнено(ПараметрДата), unixTimestamp(ПараметрДата), 0), 0);
Возврат Результат;
КонецФункции
//
//
Функция unixTimestamp(ПараметрДата) Экспорт
Возврат УниверсальноеВремя(ПараметрДата) - unixStartDate();
КонецФункции
//
//
Функция unixStartDate() Экспорт
Возврат '19700101';
КонецФункции
ТаймаутОжиданияГотовности = 2;
Слеш = ПолучитьРазделительПутиСервера();
Кавычки = ФайловаяСистема.КавычкиСистемные();
МассивОшибок = Новый Массив;
УстановитьНастройкиПоУмолчанию();
Тестируем наше чудо-юдо. Все норм.

В нашем примере размер исходных JSON-данных 13 байт, а размер закодированного сообщения 2 байта. Разница существенная с точки зрения трафика и хранения, но есть и цена.
Для ориентира скорострельности: имеем среднее значение по кодированию JSON-сообщений в proto на продуктовой среде порядка 40-50 rps. Предел специально не замеряли.
Отправку сообщений в Kafka реализуем абсолютно аналогично: скрипт + входной файл + выходной файл результата. Ахалай-махалай и вот это все.
В итоге констатируем успех реализации и использования в бою обмена Protobuf -> Kafka под 1С-ный проект. Схема платформа + pyhton|go|Исполнитель|что-то еще вполне норм.
Со смешанными чувствами констатируем успехи ИИ по написанию кода. Рабочая стратегия: сидеть на берегу реки и ждать... осталось узнать, кто проплывет.