Всемогущий маршалинг. Обзор
Забегая вперёд стоит отметить, что сериализация является частью маршалинга.
Сериализация
Сериализация объекта — запись состояния объекта в байтовый поток таким образом, чтобы его можно было преобразовать обратно в копию исходного объекта.
Сериализация может применяется для любого объекта только в рамках одного процесса (одной программы).
Маршалинг
Абстрактно говоря, маршалинг — процесс передачи информации между управляемым и неуправляемым кодом.
Маршалинг объекта — запись состояние объекта и его кодовой базы таким образом, чтобы была была возможность получить копию объекта путём загрузки определений класса исходного объекта. Можно утверждать, что сериализация — часть маршалинга.
Кодовая база — информация о том, где можно найти реализация какого-либо объекта.
Процесс маршалинга возможно применить либо к сериализуемым объектам, либо к объектам, которые наследуются от класса MarshalByRefObject.
Маршалинг может применяется как в рамках одного процесса, так и в рамках нескольких процессов/потоков/машин, а также при использовании различных RPC механизмов и P/Invoke.
Резюмирую — маршалинг используется в следующих случаях:
Передача данных между приложениями или процессами
Передача данных между различными языками программирования
Использование библиотек, написанных на других языках программирования (например, использование C++ библиотек в C# проекте)
В данном случае рассмотрим примеры использования маршалинга в следующих случаях:
P/Invoke между C# и C++
Взаимодействие с удалёнными объектами
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.
Для работы с удалёнными объектами используются следующие пространства имён:
System.Runtime.Remoting.*
System.StubHelpers
Для взаимодействия между приложениями по умолчанию существуют следующие варианты:
HttpChannel
TcpChannel
IpcChannel
Для взаимодействия с каналами нужно добавить:
Nuget-пакет Microsoft.NETFramework.ReferenceAssemblies
Ссылку на 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();
}
}
Создадим пример сервера:
Создаём и регистрируем TCP канал (или любой из вышеописанных каналов).
Устанавливаем настройки порта и форматтера.
Создаём экземпляр объекта, который наследуется от
MarshalByRefObject
.Регистрируем удалённый объект на сервере.
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 способами:
Через RemotingServices —
RemotingServices.Connect()
Через Активатор —
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();
}
Порядок выполнения приведённого примера кода:
Запустить приложение-сервер, чтобы создать и зарегистрировать удалённый объект.
Запустить приложение-клиент, чтобы создать прокси-объект и вызывать методы удалённого объекта через него.
После выполнения метода удалённого объекта, проси-объект будет автоматически удалён, а соединение между клиентом и сервером разорвано. Если попытаться вызвать метод удалённого объекта через этот же прокси-объект ещё раз — получим исключение.
Маршалинг в .Net Remoting происходит автоматически при вызове методов удалённого объекта через прокси-объект. Прокси-объект выполняет сериализацию параметров метода, отправляет их на сервер, получает результат выполнения метода и десериализует его. Для этого используется «форматтер» по умолчанию — BinaryFormatter
.
Ссылка на Github проект.