Pull to refresh

.Net Core, AppDomain, WCF, RPC маршалинг по Tcp/Ip свой велосипед

Reading time12 min
Views7.9K
Как известно, в .Net Core, на данный момент, нет AppDomain, а WCF только SOAP клиент .Net Core, WCF и ODATA клиенты.

Конечно, задачу можно решить и через 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. Активно избавляюсь от руслиша в коде но, его еще достаточно много. Если проект будет интересен, то окончательно вычищу от русского кода.
Tags:
Hubs:
Total votes 7: ↑6 and ↓1+5
Comments10

Articles