.Net Core, обмен с 1C по TCP/IP между различными устройствами
Главное из этого для меня было 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 github.com/dotnet/coreclr/blob/ee680783778ed0abb186ae41a8c861d5cdcadccd/src/jit/ee_il_dll.cpp#L76.Failure to check for invalid descriptors can result in setmode triggering failfast.
Из-за этой ошибки вылетало исключение при вызове статического .Net метода в 64 разрядном клиенте 1С
Необработанное исключение по адресу 0×00007FFD76FB8528 (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 static readonly Encoding CurrentEncoder;//=Encoding.GetEncoding (1251);
static ДляОбменаПоТСП ()
{
//Вот здесо особенность .Net Core
// Нужно зарегистрировать провайдера
// и прописать в зависимости «System.Text.Encoding.CodePages»
Encoding.RegisterProvider (CodePagesEncodingProvider.Instance);
// CurrentEncoder = Encoding.GetEncoding («windows-1251»);
// Так как мы используем Руслиш то используем кодировку 1251
CurrentEncoder = Encoding.GetEncoding (1251);
}
public static byte[] РасжатьДанные (byte[] массивДанныхДляКоманды)
{
var memStream = new MemoryStream (массивДанныхДляКоманды);
var DecompressStream = new MemoryStream ();
using (GZipStream gzipStream = new GZipStream (memStream, CompressionMode.Decompress, false))
{
Byte[] buffer = new Byte[1 << 16];
int h;
while ((h = gzipStream.Read (buffer, 0, buffer.Length)) > 0)
{
DecompressStream.Write (buffer, 0, h);
}
}
return DecompressStream.ToArray ();
}
//
public static byte[] СжатьДанные (byte[] Value)
{
var memStream = new MemoryStream ();
memStream.Position = 0;
using (GZipStream gzipStream = new GZipStream (memStream, CompressionMode.Compress))
{
gzipStream.Write (Value, 0, Value.Length);
gzipStream.Flush ();
}
return memStream.ToArray ();
}
// Классичекое чтение из NetworkStream зная размер получаемых данных
private static byte[] МассивБайтовИзСтрима (NetworkStream стрим, int размерМассива)
{
byte[] result = new byte[размерМассива];
int количествоСчитанныхСимволов = 0;
while (размерМассива > количествоСчитанныхСимволов)
{
количествоСчитанныхСимволов += стрим.Read (result, количествоСчитанныхСимволов, размерМассива — количествоСчитанныхСимволов);
}
return result;
}
public static void ЗаписатьМассивБайтовВСтрим (NetworkStream стрим, byte[] Массив)
{
стрим.Write (Массив, 0, Массив.Length);
}
// Считываем из потока 1 байт и конвертируем в bool
public static bool ReadBool (NetworkStream стрим)
{
return BitConverter.ToBoolean (МассивБайтовИзСтрима (стрим,1), 0);
}
// Конвертирум bool в 1 байт и записываем в поток
public static void Write (NetworkStream стрим, bool Value)
{
ЗаписатьМассивБайтовВСтрим (стрим, BitConverter.GetBytes (Value));
}
// Считываем из потока 4 байта и конвертируем в int
public static Int32 ReadInt32(NetworkStream стрим)
{
return BitConverter.ToInt32(МассивБайтовИзСтрима (стрим,4), 0);
}
// Конвертирум int в 4 байта и записываем в поток
public static void Write (NetworkStream стрим, Int32 Value)
{
ЗаписатьМассивБайтовВСтрим (стрим, BitConverter.GetBytes (Value));
}
// Считываем строку. Сначала идет размер данных int
//затем считываем данные и получаем строку используя кодировку 1251
public static string ReadString (NetworkStream стрим)
{
int РазмерДанных=ReadInt32(стрим);
if (РазмерДанных == 0) return »;
return CurrentEncoder.GetString (МассивБайтовИзСтрима (стрим, РазмерДанных));
}
// Записываем строку. Сначала записываем размер строки, затем конвертируем в byte[] используя кодировку 1251
public static void Write (NetworkStream стрим, string Value)
{
if (Value.Length == 0)
{
Write (стрим, 0);
return;
}
byte[] result = CurrentEncoder.GetBytes (Value);
Write (стрим, result.Length);
ЗаписатьМассивБайтовВСтрим (стрим, result);
}
// Смотри WriteCompressedString это обратная операция
public static string ReadCompressedString (NetworkStream стрим)
{
// int РазмерДанных = ReadInt32(стрим);
// return CurrentEncoder.GetString (МассивБайтовИзСтрима (стрим, РазмерДанных));
bool ЭтоСжатаяСтрока = ReadBool (стрим);
if (! ЭтоСжатаяСтрока) return ReadString (стрим);
int РазмерДанныхДляКоманды = BitConverter.ToInt32(МассивБайтовИзСтрима (стрим, 4), 0);
byte[] массивДанныхДляКоманды = МассивБайтовИзСтрима (стрим, РазмерДанныхДляКоманды);
массивДанныхДляКоманды = РасжатьДанные (массивДанныхДляКоманды);
return CurrentEncoder.GetString (массивДанныхДляКоманды);
}
// Пытаемся сжать строку GZIP. Если размер сжатых данных меньше оригинала то записываем сжатые танные
//иначе оригинал
// Записываем данные в следующей последовательности
//bool флаг сжатия данных
//int размер данных
//byte[] данные
public static void WriteCompressedString (NetworkStream стрим, string Value)
{
if (Value.Length == 0)
{
Write (стрим, false);
Write (стрим, 0);
return;
}
byte[] result = CurrentEncoder.GetBytes (Value);
var СжатыеДанные=СжатьДанные (result);
if (result.Length>СжатыеДанные.Length)
{
Write (стрим, true);
Write (стрим, СжатыеДанные.Length);
ЗаписатьМассивБайтовВСтрим (стрим, СжатыеДанные);
}
else
{
Write (стрим, false);
Write (стрим, result.Length);
ЗаписатьМассивБайтовВСтрим (стрим, result);
}
}
// Отправляем данные на сервер
// string Команда имя метода который будет обрабатывать данные
// string ДанныеДляКоманды это сериализованные данные ввиде строки
// bool ЕстьОтвет признак функции или процедуры метода обрабатывающего данные
public static void ОтправитьКоманду (NetworkStream strim, string Команда, string ДанныеДляКоманды, bool ЕстьОтвет)
{
Write (strim, ЕстьОтвет);
Write (strim, Команда);
WriteCompressedString (strim, ДанныеДляКоманды);
}
// Прочитать данные с клиента
public static СтруктураСообщения ПринятьКоманду (NetworkStream strim)
{
bool ЕстьОтвет=ReadBool (strim);
string Команда=ReadString (strim);
string ДанныеДляКоманды=ReadCompressedString (strim);
return new СтруктураСообщения (Команда, ДанныеДляКоманды, ЕстьОтвет);
}
}
На сервере создается класс для прослушивания
// Класс для получения и отправки сообщений
public class TCPConnector
{
TcpListener Server;
// Будем записывать ошибки в файл
// Нужно прописать в зависимости "System.Diagnostics.TextWriterTraceListener"
// Файл будет рядом с этой DLL
TextWriterTraceListener myTextListener;
// Устанавливаем флаг при закрытии
bool ЭтоЗакрытие = false;
// Клиент для отпраки сообщений на сервер
Socket клиент;
// делегат для вызова внешнего события в 1С
// Который ставит сообщение в очередь событий в 1С
public Action ВнешнееСобытие1С;
//Делегат для вывода ошибки в окне сообщений
public Action СообщитьОбОшибкев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 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)) return new Action(ВызватьВнешнееСобытиеСОбъектом);
if (ReturnType == typeof(Action)) return new Action(AutoWrap.ВызватьВнешнееСобытие1С);
Конечно можно использовать события и динамическую компиляцию Разработка → 1С,.Net Core. Динамическая компиляция класса обертки для получения событий .Net объекта в 1С
Но раз пишем под 1С, то проще объявить делегаты нужного типа, и установить из 1С.
Для теста нужно использовать 3 клиентов 1С и вызвать ТестОбменПоTCPIP.epf для проверки очереди событий в 1С.
Исходники можно скачать Здесь