.Net Core, обмін з 1C по 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 github.com/dotnet/coreclr/blob/ee680783778ed0abb186ae41a8c861d5cdcadccd/src/jit/ee_il_dll.cpp#L76.

Failure to check for invalid is invalid 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С можна обмінюватися через Веб сервіси, але є завдання, де потрібно взаємодія з оператором для вибору даних, брати дані підготовлені на клієнті, друк на мобільний принтер.

Основні проблеми пов'язані з мережами з поганим з'єднанням на складах.

Тому, потрібно було зменшити трафік за рахунок стиснення даних.

Так при роботі в термінальних сесіях були проблеми з проброской портів в повільних мережах
Гальмує друк чека на фіскальний реєстратор через 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 ДанныеОтветаПоТСР ОтправитьКоманду(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 ДляОбменаПоТСП
{
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<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");

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

Якщо СерверТСР<>Визначено Тоді
повернення
КонецЕсли; 

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

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

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

// Встановимо розмір черги подій дорівнює подвоєному кількості
//обслуговуваних пристроїв
// Але потрібно врахувати, що запити без відповіді ставляться в чергу 1С 
// і відразу закривається з'єднання
// Клієнт не чекає
// Якщо будуть проблеми потрібно посилати запит з відповіддю
Повідомити(Врап.УстановитьРазмерОчередиСобытий(3*2));
Повідомити(Врап.УстановитьРазмерОчередиСобытий(3*2));
СоздатьСерверТСР();
СерверТСР.Відкрити(6891,3);

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

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

Процедура СканированШК(value Дані)

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

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

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

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

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


Процедура ВнешнееСобытие(Джерело, Подія, Дані)

Якщо Джерело="TCPConnector" Тоді
//Отримаємо об'єкт переданої по ссылке
Дані=ъ(Дані);
Повідомити("Дані="+Врап.ВСтроку(Дані.ПолучитьСсылку()));
// Тест зі звіту ТестNetObjectToIDispatch
Якщо Подія="Тест Відправки Повідомлення" Тоді

ПолучениеДанныхПоТСР(Дані) 
інакше
// Запускаємо метод переданий в коанде
Виконати(Подія+"(Дані)");

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

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

Процедура ОтправитьКоманду(value КлиентТСР,ServerAdress,порт,Команда,ДанныеДляКоманды,ЕстьОтвет)

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


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

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

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

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

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

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

Процедура ПриЗакрытии()
// Вставити вміст обробника.
Якщо СерверТСР<> визначено Тоді

СерверТСР.Закрити();
СерверТСР=Визначено;
КонецЕсли; 

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С і викликати ТестОбменПоТСРІР.epf для перевірки черзі подій в 1С.
Вихідні матеріали можна скачати Тут
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.