.Net в 1С. Асинхронные HTTP запросы, отправка Post нескольких файлов multipart/form-data, сжатие трафика с использованием gzip, deflate, удобный парсинг сайтов и т.д.

Опубликовал Serginio в раздел Программирование - Практика программирования

Очень часто нужно при работе с HTTP сервисами или сайтами использовать Асинхронные HTTP запросы, отправку на сервер нескольких файлов, использование сжатия трафика.  Эта статья про то, как этого легко добиться.

Это продолжение статей

Использование классов .Net в 1С для новичков

Использование сборок .NET в 1С 7.x b 8.x. Создание внешних Компонент

1C Messenger для отправки сообщений, файлов и обмена данными между пользователями 1С, вэб страницы, мобильными приложениями а ля Skype, WhatsApp

.NET(C#) для 1С. Динамическая компиляция класса обертки для использования .Net событий в 1С через ДобавитьОбработчик или ОбработкаВнешнегоСобытия

 

Продолжение статьи лежит здесь .Net в 1С. На примере использования HTTPClient,AngleSharp.Удобный парсинг сайтов с помощью библиотеки AngleSharp в том числе с авторизацией аля JQuery с использованием CSS селекторов. Динамическая компиляция

Это статья будет полезна не только 7-кам, но и 8-кам. Да, многое умеет HTTPСоединение и HTTPзапрос. Но многого нет.

Например, нет асинхронных запросов, сжатия трафика, отправки составного содержимого и т.д.

В том числе часто можно найти примеры на C# и иногда быстрее их адаптировать с использованием в 1С HTTPClient.

Про HTTPClient можно почитать здесь

https://msdn.microsoft.com/ru-ru/library/windows/apps/xaml/dn440594.aspx

https://msdn.microsoft.com/ru-ru/library/system.net.http.httpclient(v=vs.118).aspx

Итак, начнем.

Клиент=Врап.СоздатьОбъект(HttpClient);
ДанныеРесурса=Клиент.GetStringAsync("https://msdn.microsoft.com/ru-ru/library/hh551745(v=vs.118).aspx").Result;

Сообщить(Врап.ВСтроку(ДанныеРесурса));

Все очень просто. Но это умеет и HTTPСоединение.

Для начала объявим используемые типы при открытии.

врап=новый COMОбъект("NetObjectToIDispatch45");
	HttpClient=Врап.ПолучитьТипИзСборки("System.Net.Http.HttpClient","System.Net.Http.dll");
	HttpClientHandler = врап.ПолучитьТип("System.Net.Http.HttpClientHandler");

	// Контенты для Post
	
	MultipartFormDataContent=Врап.ПолучитьТип("System.Net.Http.MultipartFormDataContent");
	StreamContent  =Врап.ПолучитьТип("System.Net.Http.StreamContent");
	StringContent  =Врап.ПолучитьТип("System.Net.Http.StringContent");
	ByteArrayContent=Врап.ПолучитьТип("System.Net.Http.ByteArrayContent");
	FormUrlEncodedContent =Врап.ПолучитьТип("System.Net.Http.FormUrlEncodedContent");

	 DecompressionMethods= Врап.ПолучитьТип("System.Net.DecompressionMethods");

    ServicePointManager=врап.ПолучитьТип("System.Net.ServicePointManager");
	Dictionary=Врап.ПолучитьТип("System.Collections.Generic.Dictionary`2[System.String,System.String]");
	StringBuilder=Врап.ПолучитьТип("System.Text.StringBuilder");
    String=Врап.ПолучитьТип("System.String");	
	HttpUtility=Врап.ПолучитьТипИзСборки("System.Web.HttpUtility","System.Web.dll");
	
	
	// Сборку AngleSharp.dll поместить в каталог программы
	//Для использования Scripting Api
    КатаогПрограммы=Врап.ПолучитьТип("System.AppDomain").CurrentDomain.BaseDirectory;
	ИмяСборкиAngleSharp=Врап.ПолучитьТип("System.IO.Path").Combine(КатаогПрограммы,"AngleSharp.dll");
	AngleSharp_Configuration=Врап.ПолучитьТипИзСборки("AngleSharp.Configuration",ИмяСборкиAngleSharp);
	
	IO_File =Врап.ПолучитьТип("System.IO.File");
	Encoding=Врап.ПолучитьТип("System.Text.Encoding");

// В релизе нужно отлавливать ошибки	
//	врап.ВыводитьСообщениеОбОшибке=ложь;

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

Начнем с асинхронного программирования. Часто нужно использовать долгие запросы, или нужно одновременно использовать несколько запросов. К сожалению, 1С этого не позволяет. Поэтому в свой разработке я постарался исправить этот недочет.

Выполнитель=Врап.ПолучитьАсинхронныйВыполнитель();
ДобавитьОбработчик Выполнитель.ПриОкончанииВыполненияЗадачи, ПриОкончанииВыполнения;

//Обработчик события выглядит так
Процедура ПриОкончанииВыполнения(Задача,ДанныеКЗадаче)

    // Обязательно нужно отлавливать ошибку в 1С
    // Иначе она передается в .Net где обрабатывается там
    Попытка
Так как задача может завершиться с ошибкой
Мы должны проверить, и если ошибка нужно предпринять какие то действия
        Если (Задача.IsFaulted) Тогда  // Ошибка выполнения

            Сообщить("Ошибка "+Врап.ВСтроку(Задача.Exception));
            Сообщить("Данные к задаче "+Врап.ВСтроку(ДанныеКЗадаче));

        иначе
            Сообщить("=====Выполнена задача ====");
            Сообщить("Данные к задаче "+Врап.ВСтроку(ДанныеКЗадаче));
            Сообщить(Врап.ВСтроку(Задача.Result));


        КонецЕсли;

    Исключение
        Сообщить("Ошибка в процедуре");
        Сообщить(ОписаниеОшибки());
    КонецПопытки

КонецПроцедуры

Вызываем задачу так

    Клиент=Врап.СоздатьОбъект(HttpClient);
    Задача=Клиент.GetStringAsync("https://msdn.microsoft.com/ru-ru/library/hh551745(v=vs.118).aspx");
    Выполнитель.Выполнить(задача,ТекущаяДата());

 

 Для тестов я сделал тестовый Вэб сервис на ASP.Net

 

Для теста  множества асинхронных запросов сделал функцию

       [HttpGet]
        public async Task<string> GetIdAsync(string id)
        {
            await Task.Delay(1000);
            return id;
 

        }

Имитируем задержку в 1 секунду. Например, нам нужно выполнить 100 запросов.

Процедура TestAsyncНажатие(Элемент)
	// Вставить содержимое обработчика.

    handler = врап.СоздатьОбъект(HttpClientHandler);

    Сообщить(ServicePointManager.DefaultConnectionLimit);
    ServicePointManager.DefaultConnectionLimit=100;


	        Клиент = Врап.СоздатьОбъект(HttpClient,handler);
// Использование заголовков не обязательно
// В данном случае это пример их использования
	        Клиент.DefaultRequestHeaders.Connection.Add("keep-alive");
			CacheControl=Врап.СоздатьОбъект("System.Net.Http.Headers.CacheControlHeaderValue");
			CacheControl.MaxAge = Врап.ПолучитьТип("System.TimeSpan").Zero;
            Клиент.DefaultRequestHeaders.CacheControl = CacheControl;
            Клиент.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,*/*");
            Клиент.DefaultRequestHeaders.Add("Accept-Language", "ru-Ru");
            Клиент.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko");
			
		    uriSources =ПолучитьСтрокуЗапроса("http://localhost.fiddler:40320/api/values/GetIdAsync/");
	
           Клиент.BaseAddress =Врап.СоздатьОбъект("System.Uri",uriSources); 
		   
		   Выполнитель=Врап.ПолучитьАсинхронныйВыполнитель();
	ДобавитьОбработчик Выполнитель.ПриОкончанииВыполненияЗадачи, ПриОкончанииВыполнения;
	
	ВыполненоЗадач=0;
    МассивОтветов=новый массив;
	
	stopWatch = Врап.СоздатьОбъект("System.Diagnostics.Stopwatch");
	stopWatch.Start();
			Для сч=1 По 100 Цикл
			
				Задача=Клиент.GetStringAsync(СокрЛП(сч));

	             Выполнитель.Выполнить(задача,ТекущаяДата());

			
			 КонецЦикла; 
			 
КонецПроцедуры

Процедура ВывестиВремя(stopWatch,толькоВремя=ложь)
	ts = stopWatch.Elapsed;
	String=Врап.ПолучитьТип("System.String");
	// Format and display the TimeSpan value.
	elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
	ts.Hours, ts.Minutes, ts.Seconds,
	ts.Milliseconds / 10,0);
	Сообщить(elapsedTime);
	Если толькоВремя Тогда
		возврат
	КонецЕсли;
	Для каждого стр  Из МассивОтветов Цикл
	
		   сообщить(стр);
	
	КонецЦикла; 
КонецПроцедуры	

Процедура ПриОкончанииВыполнения(Задача,ДанныеКЗадаче)
	
	// Обязательно нужно отлавливать ошибку в 1С
	// Иначе она передается в .Net где обрабатывается там
	ВыполненоЗадач=ВыполненоЗадач+1;
	
	
	
	Попытка
		
		Если (Задача.IsFaulted) Тогда  // Ошибка выполнения
			
			Сообщить("Ошибка "+Врап.ВСтроку(Задача.Exception));
			Сообщить("Данные к задаче "+Врап.ВСтроку(ДанныеКЗадаче));
			
		иначе
			//Сообщить("=====Выполнена задача ====");
			//Сообщить("Данные к задаче "+Врап.ВСтроку(ДанныеКЗадаче));
			//Сообщить(Врап.ВСтроку(Задача.Result));
			МассивОтветов.Добавить(Задача.Result);
                          Если ВыполненоЗадач=100 Тогда
		
		             ВывестиВремя(stopWatch)	
		
	                  КонецЕсли; 
		КонецЕсли; 
		
	Исключение
		Сообщить("Ошибка в процедуре");
		Сообщить(ОписаниеОшибки());
	КонецПопытки	
	
КонецПроцедуры

Если бы при синхронном выполнении нам понадобилось бы минимум 100 сек, то при асинхронном уходит порядка 1,5 сек.

Отдельно нужно отметить использование ServicePointManager. Более подробно можно посмотреть здесь

Почему одновременно происходит только два соединения с сайтом

Тест сетевой нагрузки


Есть еще вариант дождаться всех запросов

 

лист=Врап.СоздатьОбъект("System.Collections.Generic.List`1[System.Threading.Tasks.Task]");
	Для сч=1 по 10 Цикл
           Задача=Клиент.GetStringAsync(СокрЛП(сч));
           лист.Add(задача); 
       КонецЦикла;

      Task=Врап.ПолучитьТип("System.Threading.Tasks.Task");
      массив=лист.ToArray(); 
      Task.WaitAll(массив);
      Для каждого задача из лист Цикл 
         Сообщить(задача.Result); 
      КонецЦикла


Но здесь нужно учитывать, что например в выполнитель испольует контекс синхронизации, что может приводить к блокировкам  https://habrahabr.ru/post/257221/

Проверил асинхронные методы HttpClient не зависят от контекста синхронизации.


Перед тем как перейти к использованию multipart/form-data, покажу примеры отправки Post запросов.

Процедура ЗакрытьРесурс(Ресурс) 
	Врап.ПолучитьИнтерфейс(Ресурс,"IDisposable").Dispose();
КонецПроцедуры

Функция ПолучитьСтрокуОтвета(стрОриг)

Стр=СтрЗаменить(стрОриг,"""","");
возврат СтрЗаменить(стр,"\r\n",Символы.ПС);
	

КонецФункции // ПолучитьСтрокуОтвета()

Функция ПолучитьСтрокуЗапроса(uriSources)
	Если не ФлИспользоватьФиддлер Тогда
		возврат СтрЗаменить(uriSources,".fiddler","");
	КонецЕсли;
	
	Возврат uriSources
	КонецФункции
Функция ВыполнитьПост(uriSources,Клиент)

  //  Контент=Врап.СоздатьОбъект("System.Net.Http.StringContent","Тестовая Строка",Encoding.UTF8,"text/plain");
    Контент=Врап.СоздатьОбъект("System.Net.Http.StringContent","Тестовая Строка "+uriSources);
	резулт=Клиент.PostAsync(uriSources,Контент).Result;
	 
	
	Сообщить("===================================");
	
	Сообщить(резулт.IsSuccessStatusCode);
	Сообщить(Врап.Встроку(резулт.StatusCode));

	стр=резулт.Content.ReadAsStringAsync().Result;
   Сообщить(ПолучитьСтрокуОтвета(стр));
	

КонецФункции // ВыполнитьПост()
 
Процедура TestGetНажатие(Элемент)
	// Вставить содержимое обработчика.
	 HttpClient=Врап.ПолучитьТипИзСборки("System.Net.Http.HttpClient","System.Net.Http.dll");
	
	HttpUtility=Врап.ПолучитьТипИзСборки("System.Web.HttpUtility","System.Web.dll");

	
	Попытка

	uriSources =ПолучитьСтрокуЗапроса("http://localhost.fiddler:40320/api/values/");
   	handler = врап.СоздатьОбъект(HttpClientHandler);
	
    cookieContainer = Врап.СоздатьОбъект("System.Net.CookieContainer");
  
   handler.AutomaticDecompression=Врап.OR(DecompressionMethods.GZip,DecompressionMethods.Deflate);
// Используем cookieContainer для задания куков со стороны клиента. Куки со стороны сервера автоматически сохраняются.
cookieContainer.Add(Врап.СоздатьОбъект("System.Net.Cookie","TestCookie", "TruLyaLya", "/", "localhost"));
   handler.CookieContainer=cookieContainer;
   handler.UseCookies=истина;

	    Клиент = Врап.СоздатьОбъект(HttpClient,handler);
	    DefaultRequestHeaders=Клиент.DefaultRequestHeaders;
	    DefaultRequestHeaders.Connection.Add("keep-alive");
			CacheControl=Врап.СоздатьОбъект("System.Net.Http.Headers.CacheControlHeaderValue");
			CacheControl.MaxAge = Врап.ПолучитьТип("System.TimeSpan").Zero;
            DefaultRequestHeaders.CacheControl = CacheControl;
			
            DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,*/*");
            DefaultRequestHeaders.Add("Accept-Language", "ru-Ru");
            DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko");


        //Использум BaseAddress, что бы в дальнейшем задавать адрес только ресурса
	Клиент.BaseAddress =Врап.СоздатьОбъект("System.Uri",uriSources);
        Стр=Клиент.GetStringAsync("GetHeaders").Result;
// В данном случае запрос отправится
//
	Сообщить(ПолучитьСтрокуОтвета(стр));
	
	ВыполнитьПост("SendStr",Клиент);
	ВыполнитьПост("SendStr2",Клиент);
	
	 ЗакрытьРесурс(Клиент);
	 
 Исключение
	 Сообщить(ОписаниеОшибки());
	 
	 Если Врап.ПоследняяОшибка<>Неопределено Тогда
	 
	 	ПоказатьПредупреждение(,Врап.ПоследняяОшибка.Message);

	 
	 КонецЕсли; 
			Врап.ВывестиПоследнююОшибку();

 КонецПопытки
 

	
КонецПроцедуры

Процедура ПослатьStreamНажатие(Элемент)
	// Вставить содержимое обработчика.
	uriSources =ПолучитьСтрокуЗапроса("http://localhost.fiddler:40320/api/values/SendStream");
	
	

	Клиент=Врап.СоздатьОбъект(HttpClient);

	// Создаем поток в памяти и записываем в него данные строки в кодировке UTF8
        поток=врап.СоздатьОбъект(MemoryStream,Encoding.UTF8.GetBytes("Отсылаемая строка"));


	
	Контент=Врап.СоздатьОбъект(StreamContent,поток);
	резулт=Клиент.PostAsync(uriSources,Контент).Result;
	 
	
	Сообщить("===================================");
	
	Сообщить(резулт.IsSuccessStatusCode);
	Сообщить(Врап.Встроку(резулт.StatusCode));

	стр=резулт.Content.ReadAsStringAsync().Result;
        Сообщить(ПолучитьСтрокуОтвета(стр));
   
       ЗакрытьРесурс(Клиент);

КонецПроцедуры

Иногда нужно не только отправить но и получить куки


 

	// Вставить содержимое обработчика.
	cookieContainer = Врап.СоздатьОбъект("System.Net.CookieContainer");
	
  handler=Врап.СоздатьОбъект(HttpClientHandler);
  handler.AutomaticDecompression=Врап.OR(DecompressionMethods.GZip,DecompressionMethods.Deflate) ;
  handler.CookieContainer=cookieContainer;
  
  

	Клиент = Врап.СоздатьОбъект(HttpClient,handler);
    uriSources ="http://www.telerik.com";
    Ури=Врап.СоздатьОбъект("System.Uri",uriSources);
	Клиент.BaseAddress =Ури;
	
   Стр=Клиент.GetStringAsync("/UpdateCheck.aspx?isBeta=False").Result;
   
   // Пострим все куки присоединенные по этому аресу
    Куки = cookieContainer.GetCookies(Ури);
	Для  каждого кук  из Куки Цикл
		Сообщить(кук.Name + ": " + кук.Value);
	КонецЦикла;

	// Можем получить конкретное значение
	кук=Куки.get_Item("sid");
	Сообщить(кук.Name + ": " + кук.Value);


Отправляемый контекст можно задавать пятью способами

  • MultipartFormDataContent,
  • StreamContent,
  • StringContent,
  • ByteArrayContent,
  • FormUrlEncodedContent

Выбирайте тот, который удобен. FormUrlEncodedContent это аналог отправики данных Form при Post Submit.

Теперь перейдем к отправке multipart/form-data. 

Процедура Multi_PartНажатие(Элемент)
	// Вставить содержимое обработчика.
	uriSources =ПолучитьСтрокуЗапроса("http://localhost.fiddler:40320");
	//uriSources ="http://localhost:40320";
	HttpClient=Врап.ПолучитьТипИзСборки("System.Net.Http.HttpClient","System.Net.Http.dll");
	MultipartFormDataContent=Врап.ПолучитьТип("System.Net.Http.MultipartFormDataContent");

	
	Клиент = Врап.СоздатьОбъект(HttpClient);
    Контент = Врап.СоздатьОбъект(MultipartFormDataContent);
    Клиент.BaseAddress =Врап.СоздатьОбъект("System.Uri",uriSources);
	
	
	// Вариант отправки Ключ-Значение
    Значения = Врап.СоздатьОбъект("System.Collections.Generic.Dictionary`2[System.String,System.String]");
 
     Значения.Add("Name", "name");
     Значения.Add("id", "id");
  

                //      content.Add(new FormUrlEncodedContent(values));
                Для каждого КлючЗначение из Значения Цикл

                    Контент.Add(Врап.СоздатьОбъект("System.Net.Http.StringContent",КлючЗначение.Value),КлючЗначение.Key);
				КонецЦикла;
				
				
				// Вариант отправки двоичных данных из массива
				Encoding=Врап.ПолучитьТип("System.Text.Encoding");
				
                СтроковыйКонтент =Врап.СоздатьОбъект("System.Net.Http.ByteArrayContent",Encoding.UTF8.GetBytes("Тестовая строка"));

				ContentDisposition=Врап.СоздатьОбъект("System.Net.Http.Headers.ContentDispositionHeaderValue","form-data");
				ContentDisposition.FileName ="ПростоСтрока";
				ContentDisposition.Name ="attachment";


                СтроковыйКонтент.Headers.ContentDisposition = ContentDisposition;
				СтроковыйКонтент.Headers.ContentType = Врап.СоздатьОбъект("System.Net.Http.Headers.MediaTypeHeaderValue","text/plain");
                Контент.Add(СтроковыйКонтент);

				
				// Вариант отправки двоичных данных из файла
                ИмяФайла ="C:/ТестXML";
			
				ПотокФайла =Врап.ПолучитьТип("System.IO.File").OpenRead(ИмяФайла);
                ФайловыйКонтент =Врап.СоздатьОбъект("System.Net.Http.StreamContent",ПотокФайла);
				ContentDisposition=Врап.СоздатьОбъект("System.Net.Http.Headers.ContentDispositionHeaderValue","form-data");
				ContentDisposition.FileName = Врап.ПолучитьТип("System.IO.Path").GetFileName(ИмяФайла);
				
                ФайловыйКонтент.Headers.ContentDisposition = ContentDisposition;
				ФайловыйКонтент.Headers.ContentType = Врап.СоздатьОбъект("System.Net.Http.Headers.MediaTypeHeaderValue","application/octet-stream");
                Контент.Add(ФайловыйКонтент);

                 // Вариант отправки двоичных данных из файла но более краткий
				 ПотокФайла2 =Врап.ПолучитьТип("System.IO.File").OpenRead(ИмяФайла);
                 ФайловыйКонтент2 =Врап.СоздатьОбъект("System.Net.Http.StreamContent",ПотокФайла2);
				 Контент.Add(ФайловыйКонтент2,"attachment","TestXml");

                requestUri = "api/values/SendFiles";

                Результат = Клиент.PostAsync(requestUri, Контент).Result;
                стр = Результат.Content.ReadAsStringAsync().Result;
				
				Сообщить(стр);
				ЗакрытьРесурс(Клиент);
				ЗакрытьРесурс(Контент);
				ЗакрытьРесурс(ПотокФайла);
// Вот как выглядит отправляемый запрос				
//POST http://localhost:40320/api/values/SendFiles HTTP/1.1
//Content-Type: multipart/form-data; boundary="9f2d525a-7383-46ab-8fc7-419d73486c02"
//Host: localhost:40320
//Content-Length: 811
//Expect: 100-continue
//Connection: Keep-Alive

//--9f2d525a-7383-46ab-8fc7-419d73486c02
//Content-Type: text/plain; charset=utf-8
//Content-Disposition: form-data; name=Name

//name
//--9f2d525a-7383-46ab-8fc7-419d73486c02
//Content-Type: text/plain; charset=utf-8
//Content-Disposition: form-data; name=id

//id
//--9f2d525a-7383-46ab-8fc7-419d73486c02
//Content-Disposition: form-data; filename="=?utf-8?B?0J/RgNC+0YHRgtC+0KHRgtGA0L7QutCw?="; name=attachment
//Content-Type: text/plain

//Тестовая строка
//--9f2d525a-7383-46ab-8fc7-419d73486c02
//Content-Disposition: form-data; filename="=?utf-8?B?0KLQtdGB0YJYTUw=?="
//Content-Type: application/octet-stream

//12345
//--9f2d525a-7383-46ab-8fc7-419d73486c02
//Content-Disposition: form-data; name=attachment; filename=TestXml; filename*=utf-8''TestXml

//12345
//--9f2d525a-7383-46ab-8fc7-419d73486c02--

КонецПроцедуры


Запрос достаточно просто отправить. Вот как это приходится делать на чистом 1С Передача файлов и данных на веб-сервер средствами 1С:Предприятие 8.X методом POST

multipart/form-data

Стоит отметить использование 

handler.AutomaticDecompression=Врап.OR(DecompressionMethods.GZip,DecompressionMethods.Deflate);
который позволяет хорошо сжимать HTML странцы. Например
uriSources ="https://msdn.microsoft.com/en-us/library/system.net.decompressionmethods(v=vs.110).aspx";

    handler = врап.СоздатьОбъект(HttpClientHandler);
    handler.AutomaticDecompression=Врап.OR(DecompressionMethods.GZip,DecompressionMethods.Deflate) ;
  
 Клиент=Врап.СоздатьОбъект(HttpClient,handler);
Стр=Клиент.GetStringAsync(uriSources).Result;
 

Сжимает трафик в 5 раз.

Content-Length: 17129 упакованной, против 87 624 неупакованной

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

См. также

Комментарии

1. Жолтокнижниг 10.03.2016 19:10
(0) Я не нашел, какие требования к Framework?
Ответили: (2)
# Ответить
2. Serginio 10.03.2016 22:53
3. bonv 07.04.2016 09:28
(0) Перед публикацией могли хотя бы отформатировать код (Alt+Shift+F)
Ответили: (4)
# Ответить
4. Serginio 07.04.2016 12:15
(3) bonv,
Форматировал. Только вот вставка в редактор и редактирование на этом сайте не совсем тривиальная задача
# Ответить
Внимание! За постинг в данном форуме $m не начисляются.
Внимание! Для написания сообщения необходимо авторизоваться
Текст сообщения*
Прикрепить файл