1с переданное значение не может быть помещено во временное хранилище
В связи с новым подходом в программировании на платформе 8.2 возникла необходимость передачи данных между клиентом и сервером при этом прямая передача в виде параметров имеет ряд ограничений. И тогда был введен объект «Временное хранилище».
Чем полезно временное хранилище:
- Не надо создавать объекты в конфигурации
- Доступно и с сервера и с клиента
- Можно записать любой объект
Временное хранилище это не объект конфигурации, а объект платформы поэтому для разработчика он выглядит как черный ящик. Можно туда положить что-то получив псевдоним(адрес) и получить из него что-то указав псевдоним(адрес). Но узнать что находится во временном хранилище и сколько нельзя. В ниже рассмотренных примерах помещение в хранилище происходит на клиенте, а чтение на сервере. Хотя можно передавать данные и в обратном направлении.
Синтаксис работы с хранилищем простой:
Передав параметром, данные которые хотим поместить(Посылка) и УникальныйИдентификатор получаем Адрес, по которому потом можно будет обратиться. Причем передаваемый идентификатор не является получаемым в последствии адресом. Вместо идентификатора можно передать строку Адреса, по которому и будет помещено наше значение, но это не любая строка! Строка Адреса выглядит примерно вот так «e1cib/tempstorage/ae5c5472-0266-4892-9073-20392dd5a6a6» .
УникальныйИдентификатор может быть как новый так и существующей формы, в последнем случае значение в хранилище будет храниться пока существует форма. Если не передать уникальный идентификатор, то при следующем серверном вызове значение уже будет удалено.
Чтобы получить из хранилища нужно только указать адрес:
Также можно удалить помещенный объект, используя адрес
Если помещаемые данные не являются объектами конфигурации, то для того чтобы их поместить нужно перевести в доступный тип - «ДвоичныеДанные»
Помещение в хранилище
Здесь мы сначала преобразуем файл в формат двоичных данных и только потом помещаем их в хранилище
Чтение из хранилища
В интернете есть множество примеров как на сервере записать полученные данные в базу, поэтому ниже приводится другой пример, когда файл полученный на сервере открывается для работы. В качестве такого файла используется XML.
Если мы имеем дело с файлами, то может оказаться удобным метод ПоместитьФайл. Он преобразует файл в двоичные данные и записывает их во ВременноеХранилище. При этом параметром можно настроить открытие диалогового окна для выбора файла. Сделаю оговорку, этот метод нельзя использовать на сервере.
В параметре Адрес, можно указать Адрес в который нужно записать файл, если нужен новый адрес, то нужно передать пустую строку.
Истина – открывает окно для выбора папки и имени файла, при значении Истина есть возможность открыть файл."КопияФайла.xml" – Имя файла под которым будет сохранен файл.Адрес – это адрес в хранилище,ПолучитьФайл(Адрес,"Копияфайла.xml" , Истина);Чтобы сохранить файл из временного хранилища на Клиенте можно использовать метод
Итак, начнем с того, что в платформе 8.2 можно было сохранить соединение во временное хранилище. Как это делается:
Ну и получать COM-объект из временного хранилища:
Но в платформе 8.3 возникает ошибка "Переданное значение не может быть помещено во временное хранилище".
На 8.3 во временное хранилище можно помещать только те данные, для которых поддерживается сериализация. Д елается точно так же, но немного другой код:
Важно: получать из хранилища только на сервере (если на сервере помещали), или аналогично на клиенте.
На этом статья заканчивается.
Специальные предложения
(32) androgin, а если кластер состоит из нескольких рабочих серверов и произойдет переподключение с одного рабочего сервер на другой?
Есть другой способ: использовать модуль повторного использования, в котором получать функцией ком соединение. При первом обращении ком соединение кешируется
(7) а как потом закрывать соединение?В кэше данные хранятся 20 мин, спустя это время при повторном использовании новый сом объект создатся.
Мне нужно сохранять соединение между сеансами для работы веб-сервиса. По сути, разница с обычным КОМ-соединителем только в строке класса COM-объекта.
(11) baton_pk, когда планируешь допилить? Что будет если два сеанса одновременно запросят один и тот же CacheId? В какой момент уничтожается объект или он работает резидентно через COM+?
Для моих мелочных нужд оно, в принципе, уже работает. Если приводить к более-менее коробочному виду, то к концу следующей недели думал закончить.
Они получат один и тот же COM-объект. Потому у меня в качестве id используется имя пользователя, а одновременная работа двух пользователей с одним именем у меня отсекается на уровне логики приложения.
С этим сейчас у меня пока творческий ступор: как сделать лучше. Это COM+ (насколько я знаком с терминологией), то есть EXE-сервер. Сейчас он выключается по истечении некоторого таймаута после отключения последнего сеанса. То есть как-то так:
Первый подключился - запускается EXE-сервер.
второй-третий-. -тысячный - работают с тем же EXE-сервером и с его хранилищем 1С-соединений
как только все-все-все отсоединятся, запускается таймер-убийца (для отладки у меня 10 секунд стоит, но в боях я буду минут 10 выставлять). Если по истечении времени не было новых соединений, то EXE-сервер закрывается и закрывает все 1С-соединения. Следующее соединение уже запустит новый чистый EXE-сервер.
Не запутаться бы, где какое соединение :-)
(13) baton_pk, про COM+ описал правильно. Твоя реализация судя по описанию не позволяет конкурентно использовать пул внешних соединений. Про ограничение количества сеансов на одного пользователя я так понял, что в случае обслуживания операции веб-сервиса сразу от нескольких клиентов только один из них получит ответ, а остальные отказ.
(15) tormozit, Нет, у меня разные клиенты под разными пользователями входят. Соответственно, у каждого свой кэш получается.
Если тебе нужно, чтобы внутри одного сеанса хранился объект, то в качестве идентификатора кэша можно использовать имя пользователя и номер сеанса. А если всё-таки между сеансами перебрасывать, то тут либо пользователей разносить, либо какой-то механизм сессий вводить, когда ты клиенту передаёшь идентификатор при первом соединении и клиент с этим идентификатором дальше с тобой общается (типа сессий в PHP). Я пока в эту сторону думаю.
Тут надо ещё один момент уточнить: сопоставление идёт не CacheId => ComObject, а => ComObject.
То есть в одном кэше два подключения к разным базам (или под разным пользователем и т.д.) - это разные объекты, разумеется.
(11) baton_pk, а замерял на сколько падает производительность при использовании V8Pool.CacheConnector? В моей реализации подобного пула (больше похожего на то что описывал tormozit - конкурентное получение соединения из пула с фиксированным размером) производительность упала в 10-20 раз. Замерял время на сортировке, т.е. на коде который совершает много мелких вызовов. Даже осознавая присутствие маршалинга, я был сильно разочарован и "заморозил" разработку.
(17) rtnm, нет, замерами не занимался. мне просто даже в голову не пришло, что поиск соединения в пуле может быть в разы медленнее, чем создание нового соединения с Комплексной Автоматизацией.
(19) rtnm, тогда я не понимаю, что с чем надо сравнивать?
что через CacheConnector, что через родной ComConnector работа в итоге идёт через один и тот же класс внешнего соединения? Маршаллинг в любом случае присутствует.
Так вот 3 получался медленнее 1 примерно в 10-20 раз. 2 тоже был медленнее 1, но не так сильно.
Естественно для 2 и 3 речь идет о том, что сам алгоритм сортировки располагался в базе которая подключается, а не к которой подключаются.
Маршалинг в 2 и 3 присутствует, но он разный, в случае 3 он будет межпроцессным.
Было бы интересно увидеть ваши замеры.
(21) rtnm, померял.
Сделал две сортировки: одну "в лоб", одну "правильно".
В лоб разница существенная: 10 секунд против 88.
Правильно - это с сокращением количества "клиент-серверных" вызовов. Там разница 2 мс против 4 мс.
Не вижу поводов отчаиваться, при правильно построенной логике клиент-серверного взаимодействия разница в скорости будет мала.
Грубо говоря, вместо
PS. Но разницу в маршаллинге внутри процесса и между процессами зарубил себе на носу в любом случае. Спасибо.
(22) baton_pk, спасибо за замеры, появилась мысль "разморозить" разработку, найти бы только время :)
По поводу правильно построенной логике клиент-серверного взаимодействия.
Основная идея создания такой компоненты заключалась в том, чтобы не было необходимости снимать типовую конфигурацию с полной поддержки.
А если попробовать через внешнее соединение подключать внешнюю обработку и всю логику запихивать туда?
(24) baton_pk, этот вариант нужно всегда иметь на заметке. Конкретно в моем случае, я использую pool com-конектора, и обработку надо будет постоянно загружать заново, потому что она будет выгружаться при завершении соединения и помещении его в pool. В общем тут надо подумать :)
Мне одному кажется, что за попытку передать com-соединение на сервер надо люто минусовать этот бред?
И что эта, простигосспади, "статья" противоречит рекомендациям 1С по разработке?
(27) это не ответ. Сама попытка передачи COM-соединения с клиента на сервер - это грубейшая ошибка. В 8.2 защита ещё не была доведена до ума, поэтому позволяла такие хамские вольности, в 8.3 это, к счастью, пресекли.
Важно: получать из хранилища только на сервере (если на сервере помещали), или аналогично на клиенте.
(33) lf да ты стырил ВСЁ, только префиксы добавил.
Только мой вариант более живучий, чем твой)))
Догадайся почему))
(35) Если бы ты сам писал код, то имена переменных, вплоть до расстановки знаков, не были бы идентичны
А как быть с временем соединения. спустя минуту COM автоматически сбрасываться(
(Используются серверные базы)
Можно как-то настроить?
Есть еще такой метод v82Server.dll - Технология Microsoft COM+ для доступа к 1C82 , но посложнее в настройке конечно и нет готовых бинарников/исходников для 8.2+
В какой-то момент времени на сервере перестал работать данный метод. Структура в хранилище сохраняется, но после чтения из хранилища данных нет ( л_Структура.COMОбъект = неопределено ).
Проблема решилась добавлением фиксированной структуры:
л_Структура = Новый ФиксированнаяСтруктура( Новый Структура("COMОбъект", л_COMОбъект));
(46) infosoftvc, у меня сегодня внезапно тоже перестал сохраняться COM-объект в структуре. До этого больше года работало как часы. Вариант с фиксированной структурой не помог. Кто нибудь еще сталкивался с таким?
(47) Caliban, не все com объекты поддерживают сериализацию. Еще, возможно, сама 1С не сериализует com, а использует ссылку на существующий объект и если происходит переподключение с одного рабочего процесса на другой, то не факт, что будет сохранен com объект.
Я бы на нет сделал статическое свойство (синглетон) Dictioanary
8.3.8.1652.
Теперь нельзя помещать во временное хранилище COM подключение (покрайней мере на клиенте) :(
COM = Новый COMОбъект("V83.Application"); - Все ок
COM = Новый COMОбъект("V83.COMConnector"); - Ошибка.
у меня на 8.3.8.2088 аналогичная беда
только не пойму почему все тоже самое, но &НаСервере отрабатывает без ошибок?
Вот кто мне объяснит - зачем УФ и тонкий клиент на сервере где все юзеры работают по РДП или на локальной машине?
Какое облегчение приносит применение УФ.
Видя насколько увеличился объём кода и реально замеряя производительность системы, прихожу к выводу, что УФ в описываемых мной случаях бред полный.
Запустите УПП на обычных формах на нетбуке.
И, там же, запустите какое-то (правильно написанное уф) через тонкий клиент или через веббраузер.
(60)
А зачем заниматься извращениями? Есть определенные требования ПО к аппаратной части - Вы же не будете запускать скажем WarCraft II на смартфоне и требовать от него такой же производительности.
Если Ваша фирма доросла до УППырища, то уж запускать этого монстра на нетбуках - такой же бред как применение УФ в описанных мной случаях
(60) Вдогон.
Вы мой пост либо недочитали, либо не осознали прочитанное - советую перечитать и вникнуть в смысл описанного. Я запускаю 1Сину НА СЕРВЕРЕ. Довольно мощном и доступ пользователей по РДП протоколу - и на кой хрен мне ваши УФ при таком режиме работы?
В этом и вопрос. При правильной архитектуре и разработке сервер нужен только для хранения БД + публикация на вебсервере.
Итого. Есть один сервер где все сложное выполняется и есть клиентская часть где выполняется только отрисовка данных + простые вычисления.
На один сеанс клиента нужно +-500 мб памяти. При 100 пользователях объем не маленький. Все это нужно для сервера RDP. А в случае с тонким клиентом (по прямому подключению или через web) серверу нужно (в основном) только хранить подключение и выполнять запросы клиента.
Кстати, в (63) хороший пример приведен. Что бы не запускать тонкого клиента можно в 1С работать через веб-браузер. Нагрузка будет еще меньше (я про клиента).
p.s.: предлагаю почитать эту статью. Очень хорошо описаны проблемы клиент-серверного взаимодействия. Да есть нюансы, но присмотревшись на картинки видно что в случае с обычными формами "библиотека" будет находится возле каждого клиента. А в случае с управляемыми формами "библиотека" одна и клиенты только запрашивают простые данные.
(64)
И с чего это вы взяли что РДП жрет 500 мегабайт на сессию? Максимум 50 Мб. И уж если у меня будет 100 пользователей, то сервер у меня будет на порядок мощнее хотя бы для обеспечения работы СУБД. Вы сами себе противоречите - если надо экономить ресурсы сервера за счет терминальных подключений, то эта экономия сожрётся 1Синой на УФ.
В моём случае и я уверен у многих аналогичная ситуация, когда ресурсов сервера вполне хватает на все терминальные сеансы с запасом, а интернет весьма нестабильный у удаленных клиентов (раскиданы на площади примерно равной площади Франции) запускать УФ в любом виде, что в браузере, что в варианте тонкого клиента бредятина полная - 1Сина тупит на таких соединениях безбожно. А плюс РДП в том, что оборвался коннект - и ничего - восстановился и продолжаем работу в том же сеансе на том же месте.
Я уж не говорю про удобство доработки конф и отладки.
(67) Еще раз убеждаюсь, что апологеты 1С или не читают вообще или не хотят ничего слышать критического.
Мой первый вопрос был: Какое облегчение приносит применение УФ? В конкретном случае - мне например непонятна политика 1С, соответственно франчей и большинства внедренцев везде впендюрить УФ. И ведь впендюривают, даже там где от УФ больше вреда чем пользы.
Я не спорю, есть моменты когда УФ возможно более удобны чем обычные, всякое бывает, но по своему опыту и практической работы увы вижу, что в 90% это чистый маркетинг.
(68) Да легко - например: http://infostart.me/public/313737/ или http://infostart.ru/public/197612/ да и маппинг портов тоже никто не отменял. ПОсмотреть отчеты тоже проще по РДП - быстрее шевелиться чем УФ через кучу провайдеров, а ещё есть разработки (правда не 1С, вот например: http://bureautics.ru/ на платформе хоровод) так там это все работает на пару порядков эффективней чем УФ - вот бы 1Сники посмотрели и опыт бы переняли.
Я к чему - да единого универсального решения нет и не будет, поэтому стремление всех запихнуть в "прокрустово ложе" идеологии 1С мне лично очень не нравится.
(65) и как поживают ККМ на РДП? А если это не локальная сеть по своему кабелю? Иногда нужно директору с планшета, находясь в Америке, посмотреть пару отчетов?
Задач много. Единого универсального решения нет. Если устраивает текущая система работы, то и работать дальше на ней. Но не стоит говорить, что это решение единственно правильное для всех.
(0) Всё это работает ТОЛЬКО В ПРЕДЕЛАХ ОДНОГО СЕРВЕРА..
Если у Вас кластер - то считанное на другом сервере Хранилище с СОМ-объектом не работает..
(72) Это актуально и 2021 году :) Появление oData и прочих модных нынче фишек не отменяет кодовую базу, наработанную кровью и потом в древние времена! :)
(74) Только что проверил на 8.3.18.1433, все работает.
Только помещать надо не СОМ объект как таковой, а именно соединение.
Все это на сервере.
(75) Ясно, спасибо. Ну, 20 минут максимум поживёт. Да и всё равно это очень зависит от прихотей реализации работы с хранилищем.
В статье разобран пример, как найти несериализуемые значения в случае помещения в хранилище коллекций, содержащих вложенные элементы.
В качестве хранилищ рассмотрены временное хранилище значений и переменные типа ХранилищеЗначения.
Введение
Известно, что в хранилище значений можно поместить только переменные сериализуемого типа. В документации по встроенному языку и в синтакс-помощнике в описании объектов возможность сериализации указывается отметкой «Сериализуется».
При помещении во временное хранилище значений, сериализация которых не поддерживается, возникает ошибка. Текст ошибки зависит от типа переменной. Рассмотрим подробнее каждый из типов.
Хранилище значения
При помещении значения в хранилище конструктором Новый ХранилищеЗначения(, ) может возникать ошибка «Переданное значение не может быть помещено в ХранилищеЗначения, поскольку не сериализуется или содержит вложенный несериализуемый элемент».
Если в качестве помещаемого значения выступает несериализуемый элемент, то проблем не возникает — мы сразу видим, что он не сериализуется и предпринимаем необходимые действия в зависимости от потребности автоматизации.
Если в качестве помещаемого значения выступает сериализуемая коллекция (например, Структура) с вложенными элементами, то поиск конкретного не сериализуемого значения может принести немало хлопот.
Ниже представлена функция, позволяющая определить, где в коллекциях находятся несериализуемые элементы.
Временное хранилище
При помещении значения во временное хранилище методом ПоместитьВоВременноеХранилище(, ) может возникать ошибка « Переданное значение не может быть помещено во временное хранилище «.
Здесь казалось бы все по аналогии с хранилищем значения, но возник нюанс: если в качестве данных выступает сериализуемая коллекция с вложенными элементами, сериализация которых не поддерживается, то ошибки не возникает.
Т.е. вот такой код выдает ошибку:
Не знаю, это баг или фича, пока отправил письмо на v8 с просьбой признать ошибку. По результатам ответа, пополню статью.
В нижепредставленной функции организована проверка и на помещение во временное хранилище. Т.к. механизм очевидно другой, то реализован специальный параметр, который явно указывает какое хранилище проверяем.
Функции поиска несериалиуемого значения
Их две. Первая — собственно вызов с передачей проверяемого объекта:
Вторая — служебная рекурсия для определения путей несериализуемых объектов:
&НаСервере
Функция ПолучитьMD5_Сервер ( Текст )
ОбъектХеш = Новый ХешированиеДанных ( ХешФункция . MD5 );
ОбъектХеш . Добавить ( Текст );
Возврат ОбъектХеш . ХешСумма ;
Подробности ищите под крышечками в синтакс-помощнике.
Во-вторых, теперь можно управлять представлением. Да-да! Теперь есть событие в модуле менеджера таких объектов, как Справочник, Документ и пр., в котором можно самому сформировать представление. Сделать это можно, например, так:
Команда «Показать ссылку» на форме списка справочника:
&НаКлиенте
Процедура ПоказатьСсылку ( Команда )
Модуль менеджера справочника «Номенклатура» (не могу не поблагодарить Sax-mmS за ценное замечание):
Процедура ОбработкаПолученияПредставления ( Данные , Представление , СтандартнаяОбработка )
СтандартнаяОбработка = Ложь;
Представление = «(» + Данные . Артикул + «) » + Данные . Наименование ;
КонецПроцедуры
Процедура ОбработкаПолученияПолейПредставления ( Поля , СтандартнаяОбработка )
СтандартнаяОбработка = Ложь;
Поля . Добавить ( «Артикул» );
Поля . Добавить ( «Наименование» );
КонецПроцедуры
Смотрите, что получаем в итоге:
Только не забывайте, что представление в конфигурации получается очень часто, а значит, вызовов этого события будет ОЧЕНЬ много. Не стоит писать туда тяжелые запросы к остаткам. =)
Еще улучшена работа с каталогами. Например, теперь можно запросто получить путь к каталогу документов пользователя функцией (какой? правильно!) КаталогДокументов().
Написано, что оно все работает и в веб-клиенте тоже. Так что теперь проблема «куда бы сохранить пользователю отчет, ни о чем его не спрашивая» перестала существовать.
Добавили тип «ФорматированнаяСтрока». Не мутабельный, сериализуемый, позволяет делать вот такие прекрасные предупреждения:
Причем зеленая надпись — ссылка. По ней можно кликнуть и попасть в элемент справочника. Правда, работает это все пока несколько странно, например, мне не удалось запихать в одну строку ссылки на несколько элементов справочника сразу.
А еще при попытке создать по-настоящему сложную форматированную строку оно ругается на тип первого параметра, хотя тип первого параметра правильный. А когда я однажды действительно напутал с типами, ошибочным у меня был вовсе не первый, а пятый параметр конструктора, но ругалось на первый.
В общем, кому интересно — флаг в руки и мое почтение. Разберетесь, может, и мне расскажете потом.
Наконец-то колонке таблицы на управляемой форме можно задать тип «Произвольный». Не помню, где мне это нужно было, но помню, что очень горевал, что нельзя. Теперь вот можно.
Для интернетов появились функции КодироватьСтроку() и РаскодироватьСтроку(). Это тот самый URL encode/decode, о котором я банановый и в 1С!
Примечательно то, что codepage тоже указывается. Это безусловно хорошо.
И вот по администрированию еще. Процитирую:
Реализована возможность создания самораспаковывающегося архива (SFX-архива) клиентского приложения для размещения на веб-сайте. В состав архива можно поместить конфигурационный файл 1CEStart.cfg, содержащий настройки, необходимые для установки, запуска и обновления клиентского приложения. Созданный исполняемый файл предназначен для использования только в ОС Windows.
Для создания архива используется утилита 1CEClientSetupMake.exe, которая публикуется на диске ИТС.
Вот это действительно праздник! Теперь админам будет намного проще разворачивать 1С у пользователя.
Там на самом деле еще очень много изменений. Смотрите официальный ман. Я вам привел то, за что у меня глаз зацепился, а вам, может, совсем другое интересно.
Там и про СКД, и про навигационные ссылки, и про внешние источники данных. Очень много всего интересного.
В связи с новым подходом в программировании на платформе 8.2 возникла необходимость передачи данных между клиентом и сервером при этом прямая передача в виде параметров имеет ряд ограничений. И тогда был введен объект «Временное хранилище».
Чем полезно временное хранилище:
- Не надо создавать объекты в конфигурации
- Доступно и с сервера и с клиента
- Можно записать любой объект
Временное хранилище это не объект конфигурации, а объект платформы поэтому для разработчика он выглядит как черный ящик. Можно туда положить что-то получив псевдоним(адрес) и получить из него что-то указав псевдоним(адрес). Но узнать что находится во временном хранилище и сколько нельзя. В ниже рассмотренных примерах помещение в хранилище происходит на клиенте, а чтение на сервере. Хотя можно передавать данные и в обратном направлении.
Синтаксис работы с хранилищем простой:
Передав параметром, данные которые хотим поместить(Посылка) и УникальныйИдентификатор получаем Адрес, по которому потом можно будет обратиться. Причем передаваемый идентификатор не является получаемым в последствии адресом. Вместо идентификатора можно передать строку Адреса, по которому и будет помещено наше значение, но это не любая строка! Строка Адреса выглядит примерно вот так «e1cib/tempstorage/ae5c5472-0266-4892-9073-20392dd5a6a6» .
УникальныйИдентификатор может быть как новый так и существующей формы, в последнем случае значение в хранилище будет храниться пока существует форма. Если не передать уникальный идентификатор, то при следующем серверном вызове значение уже будет удалено.
Чтобы получить из хранилища нужно только указать адрес:
Также можно удалить помещенный объект, используя адрес
Если помещаемые данные не являются объектами конфигурации, то для того чтобы их поместить нужно перевести в доступный тип - «ДвоичныеДанные»
Помещение в хранилище
Здесь мы сначала преобразуем файл в формат двоичных данных и только потом помещаем их в хранилище
Чтение из хранилища
В интернете есть множество примеров как на сервере записать полученные данные в базу, поэтому ниже приводится другой пример, когда файл полученный на сервере открывается для работы. В качестве такого файла используется XML.
Если мы имеем дело с файлами, то может оказаться удобным метод ПоместитьФайл. Он преобразует файл в двоичные данные и записывает их во ВременноеХранилище. При этом параметром можно настроить открытие диалогового окна для выбора файла. Сделаю оговорку, этот метод нельзя использовать на сервере.
В параметре Адрес, можно указать Адрес в который нужно записать файл, если нужен новый адрес, то нужно передать пустую строку.
Истина – открывает окно для выбора папки и имени файла, при значении Истина есть возможность открыть файл."КопияФайла.xml" – Имя файла под которым будет сохранен файл.Адрес – это адрес в хранилище,ПолучитьФайл(Адрес,"Копияфайла.xml" , Истина);Чтобы сохранить файл из временного хранилища на Клиенте можно использовать метод
Читайте также: