Конечно, задачу можно решить и через Web Api с WebSockets для вызова событий. Но, я просто предлагаю альтернативное решение на маршалинге по TCP/IP и создание объектов, и вызов методов на стороне сервера с помощью Reflection.
Вот как выглядит удаленный вызов методов и свойств. Пример взят отсюда Основы перегрузки операторов:
// Выведем сообщение в консоли сервера
string typeStr = typeof(Console).AssemblyQualifiedName;
var _Console = wrap.GetType(typeStr);// Получим тип на сервере по имени
// "Hello from Client" будет выведено в консоле сервера
_Console.WriteLine("Hello from Client");
// получим тип по имени класса TestDllForCoreClr.MyArr
// Из сборки TestDll.dll
var MyArr = wrap.GetType("TestDllForCoreClr.MyArr", "TestDll");
// Создадим объекты на стороне сервера
// и получим ссылки на них
var Point1 = MyArr._new(1, 12, -4); // new MyArr(1, 12, -4);
var Point2 = MyArr._new(0, -3, 18); // new MyArr(0, -3, 18);
// Все операции с объектами PointX происходят на стороне сервера
Console.WriteLine("Координаты первой точки: "+Point1.x+" "+Point1.y+" "+Point1.z);
Console.WriteLine("Координаты второй точки: "+Point2.x+" "+Point2.y + " "+ Point2.z);
var Point3 = Point1 + Point2;
Console.WriteLine("\nPoint1 + Point2 = " + Point3.x + " " + Point3.y + " " + Point3.z);
Point3 = Point1 - Point2;
Console.WriteLine("Point1 - Point2 = "+ Point3.x + " " + Point3.y + " " + Point3.z);
Point3 = -Point1;
Console.WriteLine("-Point1 = " + Point3.x + " " + Point3.y + " " + Point3.z);
Point2++;
Console.WriteLine("Point2++ = "+ Point2.x + " " + Point2.y + " " + Point2.z);
Point2--;
Console.WriteLine("Point2-- = " + Point2.x + " " + Point2.y + " " + Point2.z);
Непонятны только методы wrap.GetType и MyArr._new и _Console не родной. Все остальное один в один работа с объектами на C#.
На самом деле, Point1 и Point2 и Point3 это наследники DynamicObject с переопределенными методами TryXXX, а внутри них происходит упаковка типа метода, имя метода и параметров в Stream и передача его на Сервер по протоколу Tcp/IP, где он распаковывается и вызывается метод, который ищется по типу, названию метода и параметрам. После получения результата, те же процедуры но, только с сервера на клиента.
Само решение очень близко с COM out process взаимодействием на IDispatch. Помню с удовольствием разбирался с внутренностями TSocketConnection.
Но, в отличие от Idispatch, используется перегрузка методов и операторов, вызов Generic методов с выводом типов или с заданием Generic аргументов. Поддержка методов расширений для классов, находящихся в одной сборке и для Linq методов.
Также поддержка асинхронных методов и подписка на события, ref и out параметры, доступ по индексу [], поддержка итераторов в foreach.
В отличии от Web Api, не нужно писать специально серверный код Controller, Hub ы.
Это близко к AppDomain c Remouting но, в отличие от Remoting, каждый класс является аналогом MarshalByRefObject. То есть, мы можем создать любой объект на стороне сервера и вернуть ссылку на него (некоторые языки из чисел поддерживают только double).
При вызове методов, напрямую сериализуются параметры только следующих типов: числа, строки, дата, Guid и byte[]. Для остальных типов нужно их создать на стороне сервера, а в параметрах методов уже передаются ссылки на них.
Так примеры можно посмотреть на TypeScript, который по синтаксису близок к C#
CEF, ES6, Angular 2, TypeScript использование классов .Net Core. Создание кроссплатформенного GUI для .Net с помощью CEF
CEF, Angular 2 использование событий классов .Net Core
Вызов метода на стороне сервера можно посмотреть здесь Кроссплатформенное использование классов .Net из неуправляемого кода. Или аналог IDispatch на Linux.
В этой статье я сосредоточусь на особенностях использования DinamicObject, маршалинга для вызова объектных и статических методов удаленных объектов.
Первое, с чего начнем — это с загрузки нужной сборки и получения типа. В первом примере мы получали тип по полному имени типа, по имени типа и имени сборки.
// Получим ссылку на сборку
//вызывается метод на сервере
//public static Assembly GetAssembly(string FileName, bool IsGlabalAssembly = false)
//Если IsGlabalAssembly == true? то ищется сборка в каталоге typeof(string).GetTypeInfo().Assembly.Location
//Иначе в каталоге приложения Server
var assembly = wrap.GetAssembly("TestDll");
// Получим из неё нужный тип
var @TestClass = assembly.GetType("TestDllForCoreClr.TestClass");
// Можно получить тип , зная имя класса и имя сборки. Удобно, когда нужен только один тип
//Метод на сервере
//public static Type GetType(string type, string FileName = "", bool IsGlabalAssembly = false)
//var @TestClass = wrap.GetType("TestDllForCoreClr.TestClass", "TestDll");
Теперь, имея ссылку на тип, можно создать объект, вызвав мeтод _new или вызвать метод врапера New.
var TO = @TestClass._new("Property from Constructor");
или
wrap.New(@TestClass,"Property from Constructor");
Можно конструировать Generic типы:
var Dictionary2 = wrap.GetType("System.Collections.Generic.Dictionary`2", "System.Collections");
var DictionaryIntString = wrap.GetGenericType(Dictionary2, "System.Int32", "System.String");
var dict = wrap.New(DictionaryIS);
Во wrap.New и wrap.GetGenericType можно передавать ссылки на типы или их строковое представление. Для строковых главное, что бы сборки были загружены.
Следующий вариант — это скопировать объект на сервер. Это важно потому, что скорость обмена по Tcp/IP составляет порядка 15 000 вызовов в секунду, при постоянном соединениии и всего 2000 при соединении на каждый запрос TCP/IP скорость обмена.
var ClientDict = new Dictionary<int, string>()
{
[1] = "Один",
[2] = "Два",
[3] = "Три"
};
// Скопируем объект с помощью Json сериализации .
//Более подробно чуть ниже.
var dict = connector.CoryTo(ClientDict);
теперь dict это ссылка на словарь на стороне сервера, и можем передавать в параметрах.
// Вызовем дженерик метод с автовыводом типа
//public V GenericMethod<K, V>(Dictionary<K, V> param1, K param2, V param3)
resGM = TO.GenericMethod(dict, 99, "Hello");
Console.WriteLine("Вызов дженерик метода с выводом типа " + resGM);
Мы можем использовать индексы для доступа и установки значения
Console.WriteLine("dict[2] " + dict[2]);
dict[2] = "Два";
Console.WriteLine("dict[2] " + dict[2]);
Можем использовать итератор
foreach (string value in dict.Values)
Console.WriteLine("Dict Values " + value);
Теперь я обращу ваше внимание на отличие синтаксиса. Прежде всего, это вызов Generic методов с заданием Generic аргуметов, ref и out параметров, асинхронный вызов.
// Будем вызывать следующий метод
// public V GenericMethodWithRefParam<К,V >(К param, V param2, ref string param3)
// Не получилось у меня использовать ref параметр. Ошибка, платформа не поддерживает.
// Создадим объект класса RefParam, у которого есть поле Value куда и будет записываться результат
var OutParam = new ClientRPC.RefParam("TroLoLo");
resGM = TO.GenericMethodWithRefParam(5, "GenericMethodWithRefParam", OutParam);
Console.WriteLine($@"Вызов дженерик метода с автовыводом типов Ref {resGM} {OutParam.Value}");
// Массив параметров для получения нужного метода
var GenericArgs = new object[] { "System.String", "System.String" };
// Массив может быть из строк и ссылок на типы например:
// var @Int32 = wrap.GetType("System.Int32");
//var GenericArgs = new object[] {@Int32, "System.String" };
// Первым параметром для вызова дженерик метода без вывода типа по параметрам
// должен быть массив представления типов
// Это аналог вызова
// resGM = TO.GenericMethodWithRefParam<String,String>(null, "GenericMethodWithRefParam", ref OutParam)
resGM = TO.GenericMethodWithRefParam(GenericArgs, null, "GenericMethodWithRefParam", OutParam);
Console.WriteLine($@"Вызов дженерик метода с дженерик аргументами Ref {resGM} {OutParam.Value}");
// Test return null
resGM = TO.GenericMethodWithRefParam(GenericArgs, null, null, OutParam);
Console.WriteLine($@"Вызов дженерик метода с дженерик аргументами Ref {resGM} {OutParam}");
Класс RefParam нужен для записи изменённого параметра в поле Value.
public class RefParam
{
public dynamic Value;
public RefParam(object Value)
{
this.Value = Value;
}
public RefParam()
{
this.Value = null;
}
public override string ToString()
{
return Value?.ToString();
}
}
Для вызова асинхронного метода:
// public async Task<V> GenericMethodAsync<K, V>(K param, string param4 = "Test")
var GenericArgs = new object[] { "System.Int32", "System.String" };
object resTask = await TO.async.GenericMethodAsync(GenericArgs , 44);
Нужно перед именем асинхронного метода добавить слово async
Если у вас есть Task, то можно дождаться выполнения, вызвав:
int res =await wrap.async.ReturnParam(task);
Еще одно отличие от реального кода заключается в том, что мы не можем напрямую использовать перегрузку ==
if (myObject1 == myObject2)
Console.WriteLine("Объекты равны перегрузка оператора ==");
Вместо него мы должны явно вызвать
if (myObject1.Equals(myObject2))
Console.WriteLine("Объекты равны Equals");
или, если есть перегрузка, оператора ==
if (MyArr.op_Equality(myObject1,myObject2))
Console.WriteLine("Объекты равны op_Equality");
Есть поддержка объектов, поддерживающих System.Dynamic.IDynamicMetaObjectProvider. Это ExpandoObject, DinamicObject, JObject итд.
Возьмем для тестов следующий объект:
public object GetExpandoObject()
{
dynamic res = new ExpandoObject();
res.Name = "Test ExpandoObject";
res.Number = 456;
res.toString = (Func<string>)(() => res.Name);
res.Sum = (Func<int, int, int>)((x, y) => x + y);
return res;
}
Теперь можно его использовать:
var EO = TO.GetExpandoObject();
Console.WriteLine("Свойство ExpandoObject Имя " + EO.Name);
Console.WriteLine("Свойство ExpandoObject Число " + EO.Number);
// Получим делегат
var Delegate = EO.toString;
Console.WriteLine("Вызов делегата toString " + Delegate()); // Вызовем как делегат
// Для ExpandoObject можно вызвать как метод
Console.WriteLine("Вызов Метода toString " + EO.toString());
var DelegateSum = EO.Sum;
Console.WriteLine("Вызов делегата Sum " + DelegateSum(3,4)); // Вызовем как делегат
// Для ExpandoObject можно вызвать как метод
Console.WriteLine("Вызов Метода Sum " + EO.Sum(3,4)); // Для ExpandoObject
}
Как видно из примера, поддерживаются не только методы и свойства, но и делегаты. Часто нужно приводить объекты к интерфейсам. Для этого есть ключевое слово _as.
string[] sa = new string[] { "Нулевой", "Первый", "Второй", "Третий", "Четвертый" };
// Скопируем массив на сервер
var ServerSa = Connector.CoryTo(sa);
// Получим интерфейс IEnumerable по имени
var en = ServerSa._as("IEnumerable");
var Enumerator = en.GetEnumerator();
while(Enumerator.MoveNext())
Console.WriteLine(Enumerator.Current);
// Получим ссылки на типы
var @IEnumerable = wrap.GetType("System.Collections.IEnumerable");
var @IEnumerator = wrap.GetType("System.Collections.IEnumerator");
// Для приведения к типу, используем ссылки на типы
en = ServerSa._as(@IEnumerable);
Enumerator = en.GetEnumerator();
// На всякий случай приведем к Интерфейсу IEnumerator
Enumerator = Enumerator._as(@IEnumerator);
while (Enumerator.MoveNext())
Console.WriteLine(Enumerator.Current);
Теперь перейдем к полуавтоматической сериализации.
var dict = connector.CoryTo(ClientDict);
Внутри connector.CoryTo происходит Json сериализация.
public dynamic CoryTo(object obj)
{
// Получим строковое представление типа
// Нужен для десериализации на сервере
string type = obj.GetType().AssemblyQualifiedName;
var str = JsonConvert.SerializeObject(obj);
return CoryTo(type, str);
}
Необходимо, что бы сборка сериализуемого типа была загружена на сервере. Пояснение чуть ниже.
Также на клиенте может не быть сборки с сериализуемым типом. Поэтому для сериализации мы можем использовать JObject
Анонимных типов.
JsonObject
Мы можем указать тип, ввиде строки или ссылки на тип и объект, который нужно сериализовать.
public dynamic CoryTo(object type, object obj)
{
var str = JsonConvert.SerializeObject(obj);
return CoryTo(type, str);
}
И в итоге, отослать на сервер:
// type может быть ссылкой на Type AutoWrapClient на стороне сервера
// Или строковым представлением типа
public dynamic CoryTo(object type, string objToStr)
{
object result;
var res = AutoWrapClient.TryInvokeMember(0, "JsonToObject", new object[] { type, objToStr }, out result, this);
if (!res)
throw new Exception(LastError);
return result;
}
Следует отметить, что для десериализации на строке сервера, сборка с типом должна быть загружена на стороне сервера.
static void TestSerializeObject(ClientRPC.TCPClientConnector connector)
{
// Создадим объект на стороне клиента
var obj = new TestDllForCoreClr.TestClass("Объект на стороне Клиента");
dynamic test = null;
try
{ // Скопируем объект на сервер
test = connector.CoryTo(obj);
}
// Сборка не загружена
//Поэтому явно загрузим сборку на сервере и повторим операцию CoryTo
catch (Exception)
{
Console.WriteLine("Ошибка " + connector.LastError);
var assembly = wrap.GetAssembly("TestDll");
test = connector.CoryTo(obj);
}
Console.WriteLine(test.ObjectProperty);
}
Также сборки, не находящиеся в каталоге Core CLR или не являющиеся NuGet пакетами, нужно вручную загружать:
static Assembly LoadAssembly(string fileName)
{
var Dir = AppContext.BaseDirectory;
string path = Path.Combine(Dir, fileName);
Assembly assembly = null;
if (File.Exists(path))
{
try
{
var asm = System.Runtime.Loader.AssemblyLoadContext.GetAssemblyName(path);
assembly = Assembly.Load(asm);
}
catch (Exception)
{
assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
}
}
else
throw new Exception("Не найдена сборка " + path);
return assembly;
}
Для того, что бы скопировать серверный объект на клиента, нужно использовать следующий метод:
var objFromServ = connector.CoryFrom<Dictionary<int, string>>(dict);
Console.WriteLine("dict[2] " + objFromServ[2]);
Можно использовать JObject, если такого типа нет на клиенте, используя:
connector.CoryFrom<dynamic>(
Ну и под конец. перейдем к подключению к серверу.
if (LoadLocalServer)
{
// Запустим процесс dotnet.exe c Server.dll,передав известный путь.
connector = ClientRPC.TCPClientConnector.LoadAndConnectToLocalServer(GetParentDir(dir, 4) + $@"\Server\Server\bin\Release\netcoreapp1.1\Server.dll");
}
else
{
// Подключимся к запущенному серверу по известному порту и адресу
//третий параметр отвечает за признак постоянного соединения с сервером
//Используется пул из 5 соединений
connector = new ClientRPC.TCPClientConnector("127.0.0.1", port, false);
// Запустим Tcp/IP сервер на стороне клиента для асинхронных методов и получения событий.
port = ClientRPC.TCPClientConnector.GetAvailablePort(6892);
connector.Open(port, 2);
}
Внутри LoadAndConnectToLocalServer мы запускаем процесс dotnet.exe с адресом файла Server.dll:
public static TCPClientConnector LoadAndConnectToLocalServer(string FileName)
{
int port = 1025;
port = GetAvailablePort(port);
ProcessStartInfo startInfo = new ProcessStartInfo("dotnet.exe");
startInfo.Arguments = @""""+ FileName+ $@""" { port}";
Console.WriteLine(startInfo.Arguments);
var server = Process.Start(startInfo);
Console.WriteLine(server.Id);
var connector = new TCPClientConnector("127.0.0.1", port);
port++;
port = GetAvailablePort(port);
connector.Open(port, 2);
return connector;
}
Теперь мы можем получить proxy.
wrap = ClientRPC.AutoWrapClient.GetProxy(connector);
И с помощью него получать типы, вызывать статические методы, создавать объекты, вызывать методы объектов и тд.
По окончании работы с сервером, нужно отключиться от него и, если мы запустили процесс, то выгрузить его.
// Вызовем финализаторы всех AutoWrapClient ссылок на серверные объекты
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Press any key");
Console.ReadKey();
// Удаления из хранилища на стороне сервера происходит пачками, по 50 элементов
// Отправим оставшиеся
connector.ClearDeletedObject();
// Отключимся от сервера, закроем все соединения, Tcp/Ip сервер на клиенте
connector.Close();
// Если мы запустили процесс сервера,
// То выгрузим его
if (LoadLocalServer) connector.CloseServer();
Console.WriteLine("Press any key");
Console.ReadKey();
Что касается событий, то можно посмотреть статью CEF, Angular 2 использование событий классов .Net Core.
Там описан процесс работы с событиями .Net объектов. Единственное, что код модуля для клиента можно получить:
var @DescribeEventMethods = wrap.GetType("NetObjectToNative.DescribeEventMethods", "Server");
string CodeModule = @DescribeEventMethods.GetCodeModuleForEvents(@EventTest);
Обращу внимание, что при подписке на событие с двумя и больше параметрами. создается
анонимный класс с полями соответствующими именами и типам параметров. Так для события:
public event Action<string, int> EventWithTwoParameter;
Будет создана обертка:
Target.EventWithTwoParameter += (arg1,arg2) =>
{
if (EventWithTwoParameter!=null)
{
var EventWithTwoParameterObject = new {arg1=arg1,arg2=arg2};
EventWithTwoParameter(EventWithTwoParameterObject);
}
};
CodeModule будет содержать следующий код:
// параметр value:Анонимный Тип
// Свойства параметра
// arg1:System.String
// arg2:System.Int32
static public void EventWithTwoParameter(dynamic value)
{
Console.WriteLine("EventWithTwoParameter " + wrap.toString(value));
// Можно обратиться к параметрам.
Console.WriteLine($"EventWithTwoParameter arg1:{value.arg1} arg2:{value.arg2}");
value(ClientRPC.AutoWrapClient.FlagDeleteObject);
}
Про использование динамической компиляции можно почитать здесь .Net Core, 1C, динамическая компиляция, Scripting API.
Что касается безопасности Analog System.Security.Permissions in .NET Core, то советуют запускать процесс под определенным аккаунтом пользователя с определенными правами.
Следует выразить сожаление, что в C# для динамиков нет псевдоинтерфейсов, аналога аннотации типа в TypeScript d.ts, для статической проверки кода и IntelliSense.
Но можно писать обычный код, переделывая его на удаленный. с минимальными телодвижениями.
Исходники лежат здесь RPCProjects.
Перед запуском примеров скомпилируйте проекты и скопируйте из папки TestDll\bin\Release\netcoreapp1.1\ библиотеку TestDll.dll в каталоги Server\bin\Release\netcoreapp1.1\ и Client\bin\Release\netcoreapp1.1\.
Если статья вызовет интерес, то в следующей статье распишу механизмы обмена и вызова методов на сервере.
P.S. Активно избавляюсь от руслиша в коде но, его еще достаточно много. Если проект будет интересен, то окончательно вычищу от русского кода.
Комментарии (10)
Splo1ter
07.03.2017 10:57Как то все сложновато)
Подумайте над API со стороны разработчика, только начинающего изучать ваш код — что бы можно было собрать рабочий пример буквально в 2 строчки на клиенте и сервере.Serginio1
07.03.2017 11:06Помоему все просто. Получаете тип создаете объекты. Единственно, что нет автоматической сериализации для не строк чисел, GUID и byte[]. По сравнению с WCF и Remoting все значительно проще.
Что в начальном примере непонятно?
Serginio1
07.03.2017 11:48Здесь еще один момент, по уму это все должна сделать MS. Ибо я никто и звать меня никак. А вещь нужная, при этом нет встроенной технологии типа Remoting. По сути введение псевдоинтерфейсов (аналог анотации типов в TypeScript) решают проблему типизации. Внутри то так или иначе тоже самое.
Плюс работа с Reflection. Если в большом .Net есть метод типа InvokeMember, то для .Net Core я сам реализовать поиск нужного метода, плюс поиск дженерик метода с выводом типа, поиск методов расширений итд. Это тоже было бы неплохо иметь в стандартных библиотеках как методы расширений по аналогии с GetTypeInfo.
У меня это все реализовано.DarkFIxED
07.03.2017 19:29Использовать рефлексию на стороне сервера? Это же удар по производительности, вдовесок к тем минусам, которые описаны выше. Что-то подсказывает мне, что вы не с той стороны какую-то проблему решаете.
Serginio1
07.03.2017 19:54Угу. На самом деле удар по производительности это маршалинг. Для примера Кроссплатформенное использование классов .Net из неуправляемого кода. Или аналог IDispatch на Linux
Скорость вызова из натива метода принимающего int и возвращающего int составляет порядка 500 000 вызовов в секунду. Для JavaScript- Native-Managed CEF, ES6, Angular 2, TypeScript использование классов .Net Core. Создание кроссплатформенного GUI для .Net с помощью CEF
Составляет порядка 60 000 вызовов ( но например итератор поряка 170 000).
То для Tcp/Ip это порядка 14 000. Так, что затраты на Reflection минимальны, по сравнению с маршалингом.
kekekeks
С подобными решениями на самом деле две проблемы:
Serginio1
Ну для локал сервера разрыва соединения не замечал. Кроме всего есть режим с подключением отключением.
Есть System.IO.Pipes.dll в \.nuget\packages\System.IO.Pipes\4.3.0\ref\netstandard1.3\, но напрямую подключить не удалось.
Ну во первых это нужно для AppDomain, а для безопасности можно код выполнять в терминальной сессии, под определенными правами пользователя.
Serginio1
Кстати при работе в Web Api ты так же хранишь состояние сессии.
Если мы говорим об песочнице аналоге AppDomain, то на данный момент я не нашел, но с выходом NetStandard 2 и .net Core 2 то там уже вроде будут NamedPipes. Кроме того, можно отрыть только определенные порты.
Зачем песочнице доступ к БД? Суть AppDomain это возможность выгружать сборки, работа с нестабильным оборудованием с использованием натива. Если это не чужой код, то какие могут быть атаки. А проблема плагинов она будет и в моем решении и в обычном решении. Просто в моем решении можно прикрыть кучу дыр, за счет прав пользователя.
Serginio1
Кстати, что касается WFF, то можно ограничить число экспортируемых типов, необходимых для функционирования удаленного приложения. Все передаваемые типы легко контролируются.
Serginio1
Есть еще момент. Клиент на начальном этапе получает ссылку на тип со статическими методами. В данном случае GetType, GetAssembly. Никто не мешает поместить туда другой тип с другим набором методов. При этом несложно сделать универсальную сериализацию параметров на основе CopyTo добавив в перечисление EnumVar новое значение и при мериализации указвать это значение, затем строковое представление типа и сериализованное в Json строку значение. А эвены можно передавать на клиента отдельно.
То есть из существуещей разработки можно сделать различные варианты. Было бы желание.