Удаленный прокси с использованием .NET Remoting

Продолжим изучение структурного шаблона Прокси. Обеспечим возможность удаленного доступа к экземпляру Виртуального прокси, созданного в прошлый раз.

2. Удаленный прокси

Процесс разработки Удаленного прокси в .NET достаточно прост. Это обеспечивается классами из пространства имен System.Runtime.Remoting. По сути, .NET предлагает свою реализацию данного шаблона. Но при этом на объекты приложения накладывается ряд требований, в частности:

  • Классы, передаваемые удаленно как параметры или возвращаемые значения, должны поддерживать сериализацию. Таким классом является EmployeeInfo и поэтому ему необходимо добавить атрибут [Serializable].
  • Класс, с которым будет осуществляться удаленное взаимодействие, должен быть унаследован от MarshalByRefObject. Поскольку доступ будет осуществляться к Виртуальному прокси, то добавим указанный класс в качестве родительского EmployeeDataSourceProxy.

Стоит отметить еще две особенности. При обращении к серверу .NET Remoting создает необходимые экземпляры класса и управляет временем их существования. При этом:

  1. Для создания объекта используется конструктор по умолчанию, а созданный объект не доступен локально. Для контроля над созданием или при необходимости вызывать конструктор с параметрами можно самостоятельно создать объект. Это и будет сделано в разрабатываемом коде.
  2. По умолчанию, при отсутствии обращений созданный объект будет уничтожен через определенное время и создан заново при очередном запросе. MarshalByRefObject предоставляет возможность установить свою политику путем переопределения метода InitializeLifetimeService(). В примере будем просто возвращать значение null, что соответствует неограниченному сроку жизни объекта.

Перейдем к модификации предыдущего примера. Классы EmployeeInfo, EmployeeDataSource и EmployeeDataSourceProxy будут задействованы как в проекте сервера, так и клиента. Поэтому вынесем их код в библиотеку классов и внесем указанные выше модификации.

Обратите внимание, что по прежнему класс EmployeeDataSource остается не измененным. Поэтому в приведенном ниже исходном коде он сокращен.

[Serializable]
public class EmployeeInfo
{
    public int Id { get; set; }

    public string FullName { get; set; }

    /* Skipped */
}

public interface IEmployeeDataSource
{
    EmployeeInfo GetEmployeeInfo(int id);

    void SetEmployeeInfo(EmployeeInfo employeeInfo);
}

public class EmployeeDataSource : IEmployeeDataSource { /* Skipped */ }

public class EmployeeDataSourceProxy : MarshalByRefObject, IEmployeeDataSource
{
    #region Singleton implementation

    private static readonly Lazy<EmployeeDataSourceProxy> _instance =
        new Lazy<EmployeeDataSourceProxy>(() => new EmployeeDataSourceProxy());

    public static EmployeeDataSourceProxy Instance
    {
        get { return EmployeeDataSourceProxy._instance.Value; }
    }

    private EmployeeDataSourceProxy()
    {
        Console.WriteLine("EmployeeDataSourceProxy ctor...");
    }

    #endregion

    private readonly IEmployeeDataSource _dataSource = new EmployeeDataSource();

    private static ConcurrentDictionary<int, EmployeeInfo> _cache
        = new ConcurrentDictionary<int, EmployeeInfo>();

    public EmployeeInfo GetEmployeeInfo(int id)
    {
        return EmployeeDataSourceProxy._cache.GetOrAdd(id, this._dataSource.GetEmployeeInfo);
    }

    public void SetEmployeeInfo(EmployeeInfo employieInfo)
    {
        this._dataSource.SetEmployeeInfo(employieInfo);

        EmployeeDataSourceProxy._cache.AddOrUpdate(employieInfo.Id,
            employieInfo, (key, value) => employieInfo);
    }

    public override object InitializeLifetimeService() { return null; }
}

Отдельно остановимся на классе DataSourceFactory. Теперь это Абстрактная фабрика, порождающая объекты как для клиента, так и для сервера. Кроме того, она скрывает не только тип создаваемых объектов, но и подробности взаимодействия с .NET Remote.

public static class DataSourceFactory
{
    private static bool _isClientNotRegistered = true;
    private static bool _isServerNotRegistered = true;
    private static readonly string _dataSourceUri = "B7167E88-14FC-4023-AF96-CB1E50E7CE5A";
    private static readonly object _threadSafetyObject = new object();

    public static IEmployeeDataSource CreateServerDataSource()
    {
        EmployeeDataSourceProxy dataSource = EmployeeDataSourceProxy.Instance;

        lock (DataSourceFactory._threadSafetyObject) {
            if (DataSourceFactory._isServerNotRegistered) {
                int tcpPort = 80;

                TcpServerChannel channel = new TcpServerChannel(tcpPort);
                ChannelServices.RegisterChannel(channel, true);

                RemotingConfiguration.RegisterWellKnownServiceType(
                    typeof(EmployeeDataSourceProxy),
                    DataSourceFactory._dataSourceUri, WellKnownObjectMode.Singleton);

                RemotingServices.Marshal(dataSource,
                    DataSourceFactory._dataSourceUri, typeof(EmployeeDataSourceProxy));

                DataSourceFactory._isServerNotRegistered = false;
            }
        }

        return dataSource;
    }

    public static IEmployeeDataSource CreateEmployeeDataSource(bool isRemote)
    {
        if (isRemote) {
            return DataSourceFactory.CreateRemoteDataSource();
        }

        return DataSourceFactory.CreateLocalDataSource();
    }

    private static IEmployeeDataSource CreateLocalDataSource()
    {
        return EmployeeDataSourceProxy.Instance;
    }

    private static IEmployeeDataSource CreateRemoteDataSource()
    {
        lock (DataSourceFactory._threadSafetyObject) {
            if (DataSourceFactory._isClientNotRegistered) {
                TcpClientChannel channel = new TcpClientChannel();
                ChannelServices.RegisterChannel(channel, true);

                string server = "localhost";
                int tcpPort = 80;

                string serverUrl = string.Format("tcp://{0}:{1}/{2}",
                    server, tcpPort, DataSourceFactory._dataSourceUri);

                RemotingConfiguration.RegisterWellKnownClientType(
                    typeof(EmployeeDataSourceProxy), serverUrl);

                DataSourceFactory._isClientNotRegistered = false;
            }
        }

        return EmployeeDataSourceProxy.Instance;
    }
}

Метод CreateServerDataSource() используется для создания объекта на сервере. Рассмотрим подробнее:

  1. Получаем экземпляр Виртуального прокси EmployeeDataSourceProxy. Как уже упоминалось, будем использовать готовый объект вместо его автоматического создания при запросах.
  2. Регистрируем канал (TcpServerChannel) и порт (80), через которые будет подключаться клиент. Для упрощения примера, в качестве номера порта используем готовое значение.
  3. Вызов метода RegisterWellKnownServiceType() регистрирует тип, который будет использоваться для работы с клиентами.
    • Передаваемое в качестве параметра значение _dataSourceUri является идентификатором для доступа к объекту. Можно использовать любую уникальную строку. В данном случае это обычный GUID.
    • Значение WellKnownObjectMode.Singleton указывает на то, что все запросы клиента будут обслуживаться один экземпляром объекта. Таким образом Remoting позволяет использовать любой класс как Одиночку, даже если сам он не является таковым. Другой режим, WellKnownObjectMode.SingleCall, подразумевает отдельные объекты для каждого запроса.
  4. Чтобы созданный на первом шаге экземпляр Виртуального прокси использовался для обслуживания клиентских запросов его необходимо зарегистрировать. С этой целью вызывается метод Marshal() c указанием объекта, его типа и идентификатора.

Перейдем к методу CreateRemoteDataSource(), предназначенному для создания экземпляра объекта в клиентском приложении. Посмотрев на него можно заметить, что используется схожий набор команд:

  1. Регистрируем канал для обращений к серверу.
  2. Регистрируем тип, экземпляры которого будут расположены на сервере. Обратите внимание, что протокол, адрес сервера, порт и идентификатор указаны в передаваемой строке serverUrl.
  3. После вызова RegisterWellKnownClientType() использование ключевого слова new с указанным типом будет приводить к созданию Удаленного прокси для объекта сервере.
  4. В завершении метода произойдет создание EmployeeDataSourceProxy. И как уже сказано .NET подменит экземпляр создаваемого класса на Удаленный прокси.

Стоит отметить, что полученная цепочка из Удаленного и Виртуального прокси по прежнему прозрачна для клиента. Детали кэширования и взаимодействия по сети скрыты в классах прокси, а подробности порождения – в Абстрактной фабрике DataSourceFactory.

Осталось рассмотреть создание клиента и сервера. Здесь все очень просто. Стоит только упомянуть один момент: для большей наглядности, добавим на сервере запись с id = 42 и запросим ее на клиенте. Кроме того, повторный запуск клиента покажет уже две измененные записи.

Код сервера выглядит следующим образом:

class Program
{
    static void Main(string[] args)
    {
        IEmployeeDataSource dataSource = DataSourceFactory.CreateServerDataSource();

        EmployeeInfo employeeInfo = new EmployeeInfo() {
            Id = 42,
            FullName = "John Doe"
        };
        dataSource.SetEmployeeInfo(employeeInfo);
        
        Console.WriteLine("Press [Enter] to terminate server...");
        Console.ReadLine();           
    }
}

Код клиента, по сути, не изменился. Единственное существенное отличие – появление параметра у метода CreateEmployeeDataSource(). Но это сделано исключительно в демонстрационных целях: изменяя значение переменной useRemoteDB можно запускать клиента как для работы с локальным источником данных, так и с удаленным.

class Program
{
    public static void ShowEmployeeInfo(int id, IEmployeeDataSource dataSource)
    {
        EmployeeInfo employeeInfo = dataSource.GetEmployeeInfo(id);
        Console.WriteLine("Employee id = {0}", employeeInfo.Id);
        Console.WriteLine("Employee name = {0}\n", employeeInfo.FullName);
    }

    public static void SetEmployeeName(int id, string fullName, IEmployeeDataSource dataSource)
    {
        EmployeeInfo employeeInfo = dataSource.GetEmployeeInfo(id);
        employeeInfo.FullName = fullName;
        dataSource.SetEmployeeInfo(employeeInfo);
    }

    public static void Main(string[] args)
    {
        bool useRemoteDB = true;
        IEmployeeDataSource dataSource = DataSourceFactory.CreateEmployeeDataSource(useRemoteDB);

        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);

        SetEmployeeName(11, "Employee name 1", dataSource);
        SetEmployeeName(12, "Employee name 2", dataSource);

        ShowEmployeeInfo(42, dataSource);

        ShowEmployeeInfo(11, dataSource);
        ShowEmployeeInfo(12, dataSource);

        Console.WriteLine("\nDone ...");
        Console.ReadKey(true);
    }
}

Осталось дать ссылку на демонстрационный проект для Visual Studio 2010:
RemoteProxyExample.zip (16.1 kb).

Комментарии (6) -

Хотел поинтересоваться, почему в этом примере Remoting, а не WCF? В демонстрационных целях для простоты, или вы его правда используете сейчас, в 3.5-4 .NET?

На мой взгляд Remoting несколько проще понять и использовать. Более того, добавляя WCF пришлось бы, хотя бы кратко, дать его основы. Для описания использования Remoting не пришлось вводить новых (по сравнению с исходным примером) определений.

Что касается использования, то все зависит от задачи. Для нового приложения я скорее всего выберу WCF, а для небольшой (вспомогательной) задачи или экспериментов когда надо просто перекинуть данные – чем плох Remote?

Я согласен с «для небольшой (вспомогательной) задачи или экспериментов когда надо просто перекинуть данные». Если задача только проиллюстрировать паттерн, то неважно, что выбрать. Можно и про WCF сказать теми же 2мя абзацами, что и про Remoting вы написали.

Но если говорить о том, как чаще всего (и, всё-таки, правильней — ведь там и гибкая настройка протоколов, и транзакционности, и управлением временем жизни объекта, и пр. пр.) в .NET реализовывается данный паттерн, то хотя бы упомянуть WCF не лишне, мне кажется Smile

Вот все же не соглашусь про WCF и 2 абзаца. Поскольку вы "в теме", то кажется что можно уложиться (считая что не надо давать определения базовым деталям).

Но возможно стоит сделать вариант статьи на WCF как сравнение или как заменить Remoting на WCF. Я подумаю над этим.

Очень познавательно.

Конечно, за 2 абзаца не получится описать возможности WCF, просто, на мой взгляд, в этом нет и необходимости, в рамках такой статьи вполне достаточно описать, что получится от применения WCF.

Впрочем, в любом виде очень полезная серия Smile.

Добавить комментарий