.Net Core, обмен с 1C по TCP/IP между различными устройствами

Часто нужно обмениваться данными с клиентом 1С из различных устройств, между клиентами 1С, а также при виртуализации между разными ОС на компьютере. Это кроссплатформенная ВК, позволяющая обмениваться сообщениями по TCP/IP между различными устройствами по определенному протоколу.

Начну с «Вести с полей»: вышло обновление Updates in .NET Core 1.0.1. Главное из этого для меня было Access violation on Windows – coreclr 6460:

In Jitstartup, JIT creates a file descriptor for stdout and unconditionally passes it to setmode, without checking for failures. This happens at. Failure to check for invalid descriptors can result in setmode triggering failfast.

Из-за этой ошибки вылетало исключение при вызове статического .Net-метода в 64 разрядном клиенте 1С.
В 

Необработанное исключение по адресу 0x00007FFD76FB8528 (ucrtbase.dll) в 1cv8.exe: Недопустимый параметр был передан функции, для которой недопустимые параметры вызывают неустранимую ошибку.

   Сейчас починили, и код прекрасно выполняется под 64 разрядным клиентом на 8.3.9. В примерах заменил библиотеки .NET Core на 1.0.1. Хотел написать про SignalR, но пока можно написать только сервер на .Net Core — ASP.NET Core SignalR for Windows 10 UWP App

aspnet/SignalR-Server

   Клиента пока нет. В WCF пока только клиент под Web сервис. ServiceHost нет. Есть стороннее решение .NET core cross platform remote service invocation

   Но решил написать решение из своего опыта 8 летней давности для обмена данными по TCP/IP между ТСД на Win CE и 1С еще 7-ки. Конечно, с 1С можно обмениваться через Web-сервисы, но есть задачи, где нужно взаимодействие с оператором для выбора данных, брать данные, подготовленные на клиенте, печать на мобильный принтер.

   Основные проблемы связаны с сетями с плохим соединением на складах. Поэтому, нужно было уменьшить трафик за счет сжатия данных. Так, при работе в терминальных сессиях были проблемы с проброской портов в медленных сетях — Тормозит печать чека на фискальный регистратор через RDP.

   Также были проблемы при считывании двумерного штрихкода. Медленная печать с терминального сервера. Для решения этих проблем на машине клиента устанавливалась локальная 1С, которая работала как клиент и сервер. Данные со сканеров отправлялись на терминальный сервер и там обрабатывались. Для печати на фискальный регистратор отправлялись данные с сервера по TCP/IP, и с локальной 1С печатался чек. При печати этикеток с сервера оправлялись данные, на основании которых на локальной 1С формировался документ и отправлялся на печать.

   Кроме того, под многое оборудование для Linux нет драйверов. Можно, используя виртуализацию, держать Linux и Windows на одной машине, на Windows считывать данные и обмениваться с Linux по TCP/IP.

   Сейчас много у кого есть ТСД под WinCe, WinMo (недавно предлагали работу по настройке обмена на них). Кроме того, можно использовать ТСД на других осях, используя UWP и Xamarin.

   Кроме того, можно обмениваться сообщениями между клиентами 1С, наподобие чата.

  В большом .Net я часто использую обмен по TCp/IP
Использование сборок .NET в 1С 7.x b 8.x. Создание внешних Компонент.

Использование ТСД на WM 6 как беспроводной сканер с получением данных из 1С

  Поэтому я решил написать этот же обмен, но на .Net Core, и добавить новый подход.

  Чистые 1С-ники могут пропустить вражеский код и перейти к родному в конце статьи, как использовать данную компоненту.

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

        // Отправляем команду на сервер
// Отправляем данные на сервер
// string Команда имя метода который будет обрабатывать данные
// string ДанныеДляКоманды это сериализованные данные в виде строки
// bool ЕстьОтвет признак функции или процедуры метода обрабатывающего данные
public ДанныеОтветаПоTCP ОтправитьКоманду(string АдресСервера, int порт, string Команда, string ДанныеДляКоманды, bool ЕстьОтвет)

   На стороне 1С принимается такой класс

 // Данные отправляемые в 1С для обработки запроса
public class ДанныеДляКлиета1С
{

public bool ЕстьОтвет;
public string Команда;
public string Данные;
TcpClient Клиент;
public ДанныеДляКлиета1С(СтруктураСообщения Даннные, TcpClient Клиент)
{

this.ЕстьОтвет = Даннные.ЕстьОтвет;
this.Команда = Даннные.Команда;
this.Данные = Даннные.Данные;

if (ЕстьОтвет)
this.Клиент = Клиент;
else // Если нет ответа то закрываем соединение
{
Клиент.Dispose();
this.Клиент = null;
}
}


// Отсылаем данные клиенту
//Создадим новую задачу, что бы основной поток 1С не ждал отпраки
//Ответ пытаемся сжать
public void Ответить(string Ответ)
{
Task.Run(() =>
{
var strim = Клиент.GetStream();
ДляОбменаПоТСП.WriteCompressedString(strim, Ответ);
// Закроем соединение
strim.Dispose();
Клиент.Dispose();

});

}

public override string ToString()
{
return $"ЕстьОтвет={ЕстьОтвет}, Команда={Команда}, Данные={Данные}";
}
}

   Модуль для формирования сообщений, который был написан 8 лет назад, с небольшими изменениями.
   Уже тогда я вовсю использовал Руслиш.

   На сервере создается класс для прослушивания

// Класс для получения и отправки сообщений
public class TCPConnector
{

TcpListener Server;

// Будем записывать ошибки в файл
// Нужно прописать в зависимости "System.Diagnostics.TextWriterTraceListener"
// Файл будет рядом с этой DLL
TextWriterTraceListener myTextListener;

// Устанавливаем флаг при закрытии
bool ЭтоЗакрытие = false;
// Клиент для отпраки сообщений на сервер
Socket клиент;

// делегат для вызова внешнего события в 1С
// Который ставит сообщение в очередь событий в 1С
public Action<string, string, object> ВнешнееСобытие1С;

//Делегат для вывода ошибки в окне сообщений
public Action<string> СообщитьОбОшибкев1С;

// Получаем директорию сборки содержащий данный класс
string AssemblyDirectory
{
get
{
string codeBase = typeof(TCPConnector).GetTypeInfo().Assembly.Location;
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
return Path.GetDirectoryName(path) + "\";
}
}

public TCPConnector()
{


myTextListener = null;

}

// Записываем ошибку a файл и сообщаем об ошибке в 1С
void ЗаписатьОшибку(string Ошибка)
{
if (myTextListener == null)
{
try
{
FileStream fs = new FileStream(AssemblyDirectory + @"ТрассировкаОтладки",
FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);

StreamWriter myOutputWriter = new StreamWriter(fs, Encoding.GetEncoding(1251));
myTextListener = new TextWriterTraceListener(myOutputWriter);
Trace.Listeners.Add(myTextListener);

}
catch (Exception)
{

// проглотим ошибку что бы приложение закрылось
}
}

Trace.WriteLine(Ошибка);
Trace.Flush();
СообщитьОбОшибкев1С?.DynamicInvoke(Ошибка);
}



// Откроем порт и количество слушющих задач которое обычно равно подсоединенным устройствам
// Нужно учитывть, что 1С обрабатывает все события последовательно ставя события в очередь
public void Открыть(int НомерПорта = 6891, int КоличествоСлушателей = 1)
{
ЭтоЗакрытие = false;

IPEndPoint ipEndpoint = new IPEndPoint(IPAddress.Any, НомерПорта);
Server = new TcpListener(ipEndpoint);
Server.Start();

// Создадим задачи для прослушивания порта
//При подключении клиента запустим метод ОбработкаСоединения
// Подсмотрено здесь https://github.com/imatitya/netcorersi/blob/master/src/NETCoreRemoveServices.Core/Hosting/TcpServerListener.cs
for (int i = 0; i < КоличествоСлушателей; i++)
Server.AcceptTcpClientAsync().ContinueWith(ОбработкаСоединения);

}


// Метод для обработки сообщения от клиента
private void ОбработкаСоединения(Task<TcpClient> task)
{

if (task.IsFaulted || task.IsCanceled)
{
// Скорее всего вызвано  Server.Stop();
return;
}

// Получим клиента
TcpClient client = task.Result;

// И вызовем метод для обработки данных
//
ВыполнитьКоманду(client);

// Если Server не закрыт то запускаем нового слушателя
if (!ЭтоЗакрытие)
Server.AcceptTcpClientAsync().ContinueWith(ОбработкаСоединения);

}




private void ВыполнитьКоманду(TcpClient client)
{

NetworkStream стрим = client.GetStream();
try
{

// Получим данные с клиента и на основании этих данных
//Создадим ДанныеДляКлиета1С котрый кроме данных содержит
//TcpClient для отправки ответа
var Данные = new ДанныеДляКлиета1С(ДляОбменаПоТСП.ПринятьКоманду(стрим), client);

// Вызвается метод 1С для постановки сообщения в очередь
// Которое будет обработано через ВнешнееСобытие
ВнешнееСобытие1С?.DynamicInvoke("TCPConnector", Данные.Команда, Данные);

}
catch (Exception e)
{
ЗаписатьОшибку(DateTime.Now.ToString() + e.ToString());

}
}


// Закроем ресурсы
public void Закрыть()
{
if (Server != null)
{
ЭтоЗакрытие = true;
Server.Stop();
Server = null;


}
if (myTextListener != null)
{

Trace.Listeners.Remove(myTextListener);
myTextListener.Dispose();
}

}

   Все достаточно просто. При соединении считываем данные, создаем объект для отправки в 1С. Запускаем нового слушателя.

Отправка сделана на голых сокетах, можно посмотреть в исходниках.

Упрощенно это выглядит так

IPEndPoint ipEndpoint = new IPEndPoint(IPAddress.Parse(АдресСервера), порт); //6891 по умолчанию
клиент = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
клиент.Connect(ipEndpoint);

var поток= new NetworkStream(клиент);
ДляОбменаПоТСП.ОтправитьКоманду(поток, Команда, ДанныеДляКоманды, ЕстьОтвет);

// считываем сжатые данные в строку
if (ЕстьОтвет) result = ДляОбменаПоТСП.ReadCompressedString(strim);

поток.Dispose();
клиент.Dispose();

Вот как это обрабатывается в 1С

// В Net core для NetStandard System.Threading.Tasks не существует
Task=ъТип("System.Threading.Tasks.Task","System.Threading.Tasks");

Процедура СоздатьСерверTCP()

Если СерверTCP<>Неопределено Тогда
возврат
КонецЕсли;

TCPConnector=ъТип("TCPConnectTo1C.TCPConnector","ОбменПоTCPIPCore.dll");
СерверTCP=ъНовый(TCPConnector.ПолучитьСсылку());
Ссылка=СерверTCP.ПолучитьСсылку();
Врап.УстановитьДелегатДляВызоваВнешнегоСобытия(Ссылка,"ВнешнееСобытие1С");
Врап.УстановитьДелегатДляСообщенииОбОшибке(Ссылка,"СообщитьОбОшибкев1С");

КонецПроцедуры// СоздатьTCP()

Процедура ТестTCPConnectНажатие(Элемент)

// Установим размер очереди событий равный удвоенному количеству
//обслуживаемых устройств
// Но нужно учесть, что запросы без ответа ставятся в очередь 1С
// и сразу закрывается соединение
// Клиент не ждет
// Если будут проблемы нужно посылать запрос с ответом
Сообщить(Врап.УстановитьРазмерОчередиСобытий(3*2));
Сообщить(Врап.УстановитьРазмерОчередиСобытий(3*2));
СоздатьСерверTCP();
СерверTCP.Открыть(6891,3);

ЭлементыФормы.ДанныеДляОтправки.Видимость=ложь;
ЭлементыФормы.ОтправитьКоманды.Видимость=ложь;
ЭлементыФормы.НадписьДанныеДляОтправки.Видимость=ложь;

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

Процедура СканированШК(знач Данные)

// Съэмулируем долгую обработку для проверки очереди событий
ъ(Task.Delay(1000)).Wait();
Ответ="Ответ на команду "+Данные.Команда+"
|Данные "+Данные.Данные+"
|ВремяНаСервере="+XmlСтрока(ТекущаяДата());
Данные.Ответить(Ответ);
КонецПроцедуры

Процедура ВыполнитьБезОтвета(знач Данные)
// Съэмулируем долгую обработку для проверки очереди событий
ъ(Task.Delay(1000)).Wait();
КонецПроцедуры

// Для теста из других компонент
Процедура ПолучениеДанныхПоTCP(знач Данные)
Сообщить("Команда="+Данные.Команда);
Сообщить("Данные="+Данные.Данные);
Сообщить("ЕстьОтвет="+Данные.ЕстьОтвет);

ъ(Task.Delay(1000)).Wait();
Если Данные.ЕстьОтвет Тогда
Ответ="Ответ на команду "+Данные.Команда+"
|Данные "+Данные.Данные+"
|ВремяНаСервере="+XmlСтрока(ТекущаяДата());
Данные.Ответить(Ответ);
КонецЕсли;

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


Процедура ВнешнееСобытие(Источник, Событие, Данные)

Если Источник="TCPConnector" Тогда
//Получим объект по переданной ссылке
Данные=ъ(Данные);
Сообщить("Данные="+Врап.ВСтроку(Данные.ПолучитьСсылку()));
// Тест из отчета  ТестNetObjectToIDispatch
Если Событие="Тест Отправки Сообщения" Тогда

ПолучениеДанныхПоTCP(Данные)
иначе
// Запускаем метод переданный в коанде
Выполнить(Событие+"(Данные)");

КонецЕсли;
КонецЕсли;

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

Процедура ОтправитьКоманду(знач КлиентTCP,ServerAdress,порт,Команда,ДанныеДляКоманды,ЕстьОтвет)

резулт=ъ(КлиентTCP.ОтправитьКоманду(ServerAdress,порт,Команда,ДанныеДляКоманды,ЕстьОтвет));
Сообщить(Врап.ВСтроку(резулт.ПолучитьСсылку()));
Если резулт.ОшибкаСоединения Тогда
СтрОшибки="ОшибкаСоединения
|"+резулт.Данные;
Предупреждение(СтрОшибки);
КонецЕсли;


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

Процедура ОтправитьКомандыНажатие(Элемент)
СоздатьСерверTCP();
КлиентTCP=СерверTCP;
ServerAdress="127.0.0.1";
порт=6891;
Команда="Тест Отправки Сообщения";
ДанныеДляКоманды=XmlСтрока(ТекущаяДата());

ЕстьОтвет=истина;
ЗакрытьСоединение=истина;
ОшибкаСоединения=false;

Для сч=1 По 3 Цикл

ОтправитьКоманду(КлиентTCP,ServerAdress,порт,Команда,ДанныеДляКоманды,истина);
ОтправитьКоманду(КлиентTCP,ServerAdress,порт,"ВыполнитьБезОтвета",ДанныеДляОтправки,ложь);
ОтправитьКоманду(КлиентTCP,ServerAdress,порт,"СканированШК","12345678901",истина);
КонецЦикла;

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

Процедура ПриЗакрытии()
// Вставить содержимое обработчика.
Если СерверTCP<> неопределено Тогда

СерверTCP.Закрыть();
СерверTCP=Неопределено;
КонецЕсли;

GC=ъТип("System.GC");
GC.Collect();
GC.WaitForPendingFinalizers();
Врап=Неопределено;

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

   Ответ передаем через полученный объект

 Данные.Ответить(Ответ);

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

Так как можно работать с несколькими устройствами, то нужно установить нужный размер очереди через

Врап.УстановитьРазмерОчередиСобытий(размер очереди));

Который возвращает текущий размер очереди.

Конечно, можно запустить несколько приложений 1С и запустить TCP/IP сервер под разными портами, но на практике операторы путаются. Чем проще для них, тем лучше.

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

Врап.УстановитьДелегатДляВызоваВнешнегоСобытия(Ссылка,"ВнешнееСобытие1С");
Врап.УстановитьДелегатДляСообщенииОбОшибке(Ссылка,"СообщитьОбОшибкев1С");

В зависимости от типа делегата устанавливается нужный делегат

   if (ReturnType == typeof(Action<string, string, object>)) return new Action<string, string, object>(ВызватьВнешнееСобытиеСОбъектом);

if (ReturnType == typeof(Action<string, string, string>)) return new Action<string, string, string>(AutoWrap.ВызватьВнешнееСобытие1С);

Конечно, можно использовать события и динамическую компиляцию 1С,.Net Core. Динамическая компиляция класса обертки для получения событий .Net объекта в 1С

Но раз пишем под 1С, то проще объявить делегаты нужного типа, и установить из 1С.

Для теста нужно использовать 3 клиентов 1С и вызвать ТестОбменПоTCPIP.epf для проверки очереди событий в 1С.

Исходники можно скачать Здесь

8 Comments

  1. akrub

    1c-ка падает намертво при выполнении Врап.СоздатьОбертку(CoreClrDir,ДиректорияNetObjectToNative,»»);

    что я делаю не так?

    Reply
  2. Serginio

    1C случайно не 64 разрядная?

    Reply
  3. Serginio

    Да и пока сделана только под Windows

    Так же есть под виндовс http://infostart.ru/public/238584/ там есть пример обмена по Tcp/IP

    Reply
  4. Darklight

    (3) Говорим о кросплатформенности, но работает только по windows, в чём прикол?

    В NativeAPI как раз интересна работа на Linux, на MacOS и ещё Mobile 1C: Android, iOS, Windows RT

    Reply
  5. Serginio

    (4) Нужно перекомпилировать нативную часть на С++ под нужную ось.

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

    Просто мало кому в итоге эта кроссплатформенность оказалась нужна.

    Reply
  6. Serginio

    (4) Вот здесь есть ссылки на реализацию доступа к .Net классам из натива на разных осях

    Кроссплатформенное использование классов .Net из неуправляемого кода. Или аналог IDispatch на Linux

    Reply
  7. Xershi
    ОберткаКомпонент.СоздатьОбертку(ДобавочныйКаталогКомпоненты, КаталогКомпоненты, «»);
    ОберткаКомпонент.ЗагрузитьDLL(ПолныйПутьКомпоненты);
    

    Падает платформа 1С:Предприятие 8.3 (8.3.10.2650) файловый вариант, вин7 64-бита.

    Так же не работает установка, если делать из макета:

    // Через двоичные данные не работает!
    //УстановитьВнешнююКомпоненту(МестоположениеКомпоненты);
    
    //Если ПодключитьВнешнююКомпоненту(МестоположениеКомпоненты, «NetObjectToNative», ТипВнешнейКомпоненты.Native) Тогда
    

    Компоненту брал по ссылке в статье!

    Reply
  8. Xershi
    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *