Всемогущий маршалинг. Обзор

e5bf3524a474bcd0173914fdd8563369.png

Забегая вперёд стоит отметить, что сериализация является частью маршалинга.

Сериализация

Сериализация объекта — запись состояния объекта в байтовый поток таким образом, чтобы его можно было преобразовать обратно в копию исходного объекта.

Сериализация может применяется для любого объекта только в рамках одного процесса (одной программы).

Маршалинг

Абстрактно говоря, маршалинг — процесс передачи информации между управляемым и неуправляемым кодом.

Маршалинг объекта — запись состояние объекта и его кодовой базы таким образом, чтобы была была возможность получить копию объекта путём загрузки определений класса исходного объекта. Можно утверждать, что сериализация — часть маршалинга.

Кодовая база — информация о том, где можно найти реализация какого-либо объекта.

Процесс маршалинга возможно применить либо к сериализуемым объектам, либо к объектам, которые наследуются от класса MarshalByRefObject.

Маршалинг может применяется как в рамках одного процесса, так и в рамках нескольких процессов/потоков/машин, а также при использовании различных RPC механизмов и P/Invoke.

Резюмирую — маршалинг используется в следующих случаях:

  1. Передача данных между приложениями или процессами

  2. Передача данных между различными языками программирования

  3. Использование библиотек, написанных на других языках программирования (например, использование C++ библиотек в C# проекте)

В данном случае рассмотрим примеры использования маршалинга в следующих случаях:

  1. P/Invoke между C# и C++

  2. Взаимодействие с удалёнными объектами

P/Invoke (Platform Invoke) между C# и C++

Работать с P/Invoke возможно как в .Net Framework, так и в .Net Core.

P/Invoke — механизм, позволяющий использовать функции из неуправляемого кода (например, из DLL библиотек), используя управляемый код.

Кроме того, P/Invoke нужен для использования функционала существующих фреймворков и инструментов, написанных на C++ вместо того, чтобы переписывать код с C++ на C#.

На эту тему уже написано множество вариативных статей, в которых рассматриваются примеры использования P/Invoke для маршалинга структур (одна из них).

Поэтому, в рамках данной статьи приведу пример P/Invoke для одного относительно известного open-source проекта — opencv.net. Этот проект предназначен для использование кода библиотеки OpenCV (которая написана на C++) в C# проектах, что точь в точь отражает рассматриваемые способы маршалинга.

Взаимодействие с удалёнными объектами

На данный момент взаимодействие с удалёнными объектами доступно только в .Net Framework.

Для создания удалённого объекта мы можем использовать технологию .Net Remoting, которая позволяет создавать объекты, работающие в разных процессах или на разных компьютерах.

Маршалинг в .Net Remoting происходит автоматически при вызове методов удалённого объекта через прокси-объект. Прокси-объект выполняет сериализацию параметров метода, отправляет их на сервер, получает результат выполнения метода и десериализует его — для этого используется форматтер по умолчанию — BinaryFormatter.

Для работы с удалёнными объектами используются следующие пространства имён:

  1. System.Runtime.Remoting.*

  2. System.StubHelpers

Для взаимодействия между приложениями по умолчанию существуют следующие варианты:

  1. HttpChannel

  2. TcpChannel

  3. IpcChannel

Для взаимодействия с каналами нужно добавить:

  1. Nuget-пакет Microsoft.NETFramework.ReferenceAssemblies

  2. Ссылку на System.Runtime.Remoting.dll

MarshalByRefObject — базовый класс для объектов, которые нужно адресовать по ссылке. Использование этого класса означает то, что экземпляры классов, которые наследуются от него могут быть маршалированы через границы домена приложений.

В данном случае приведу простой пример работы с удалёнными объектами через TCP протокол.

Для Начала создадим удалённый объект:

public interface IRandomNumberGenerator
{
	int GenerateRandomNumber();
}

public class RandomNumberGenerator : MarshalByRefObject, IRandomNumberGenerator
{
	private readonly Random _random = new Random();

	public int GenerateRandomNumber()
	{
		return _random.Next();
	}
}

Создадим пример сервера:

  1. Создаём и регистрируем TCP канал (или любой из вышеописанных каналов).

  2. Устанавливаем настройки порта и форматтера.

  3. Создаём экземпляр объекта, который наследуется от MarshalByRefObject.

  4. Регистрируем удалённый объект на сервере.

public static void Main(string[] args)
{
	var serverSinkProvider = new BinaryServerFormatterSinkProvider
	{
		TypeFilterLevel = TypeFilterLevel.Full
	};

	var clientSinkProvider = new BinaryClientFormatterSinkProvider();

	var properties = new Hashtable
	{
		["port"] = 12345
	};

    // Создание TCP канала
	var tcpChannel = new TcpChannel(properties, clientSinkProvider, serverSinkProvider);

	// Регистрация канала на сервере
	ChannelServices.RegisterChannel(tcpChannel, ensureSecurity: false);

	var randomNumberGenerator = new RandomNumberGenerator();

	// Регистрация удалённого объекта на сервере
	RemotingServices.Marshal(randomNumberGenerator, URI: "RandomNumberGenerator.rem");

	Console.WriteLine($"Channel Name: {tcpChannel.ChannelName}");
	Console.WriteLine($"Channel Priority: {tcpChannel.ChannelPriority}");

	var channelDataStore = (ChannelDataStore)tcpChannel.ChannelData;
	foreach (var uri in channelDataStore.ChannelUris)
	{
		Console.WriteLine(uri);
	}

	Console.WriteLine("Server started. Press any key to stop.");
	Console.ReadKey();

    // Отключение удалённого объекта
	RemotingServices.Disconnect(randomNumberGenerator);

    // Остановка канала
	ChannelServices.UnregisterChannel(tcpChannel);
}

Помимо метода RemotingServices.Marshal() удалённый объект может быть зарегистрирован с помощью метода RemotingConfiguration.RegisterWellKnownServiceType():

RemotingConfiguration.RegisterWellKnownServiceType(
		typeof(RandomNumberGenerator),
		objectUri: "RandomNumberGenerator.rem",
		WellKnownObjectMode.SingleCall);

Кроме того, зарегистрировать удалённый объект и определить необходимое поведение можно в конфигурационном файле приложения:

Файл конфигурации сервера



	
		
	
	
		
	
	
		
			
				
			
			
				
			
		
	

Файл конфигурации клиента



	
		
	
	
		
	
	
		
			
				
			
		
	

Для загрузку конфигурации используется метод RemotingConfiguration.Configure().

Создадим пример клиента:

Для взаимодействия с удалённым объектом нужно создать прокси-объект, используя метод RemotingServices.Unmarshal(). Данный метод можно вызывать 2 способами:

  1. Через RemotingServices — RemotingServices.Connect()

  2. Через Активатор — Activator.GetObject()

public static void Main(string[] args)
{
	// Создание TCP канал
	var tcpChannel = new TcpChannel();

	// Регистрируем канал на сервере
	ChannelServices.RegisterChannel(tcpChannel, ensureSecurity: false);

	RemotingConfiguration.CustomErrorsMode = CustomErrorsModes.On;

	var randomNumberGenerator = (IRandomNumberGenerator)RemotingServices.Connect(
		classToProxy: typeof(IRandomNumberGenerator),
		url: "tcp://localhost:12345/RandomNumberGenerator.rem");

	var randomNumber = randomNumberGenerator.GenerateRandomNumber();

	Console.WriteLine($"Random number = {randomNumber}");
	Console.ReadKey();
}

Порядок выполнения приведённого примера кода:

  1. Запустить приложение-сервер, чтобы создать и зарегистрировать удалённый объект.

  2. Запустить приложение-клиент, чтобы создать прокси-объект и вызывать методы удалённого объекта через него.

После выполнения метода удалённого объекта, проси-объект будет автоматически удалён, а соединение между клиентом и сервером разорвано. Если попытаться вызвать метод удалённого объекта через этот же прокси-объект ещё раз — получим исключение.

Маршалинг в .Net Remoting происходит автоматически при вызове методов удалённого объекта через прокси-объект. Прокси-объект выполняет сериализацию параметров метода, отправляет их на сервер, получает результат выполнения метода и десериализует его. Для этого используется «форматтер» по умолчанию — BinaryFormatter.

Ссылка на Github проект.

© Habrahabr.ru