Violet Tape некоторые мысли о разработке на платформе .Net

23Июн/110

Обновление WCF конфигурации online

Текущий проект на работе очень хорошо сконфигурирован и разнесен по слоям и уровням, проект имеет модульную структуру, построен на мелких и относительно автономных сервисах и все в нем хорошо! Кроме настройки портов для сервисов.

Сервисов сейчас больше 50 и каждый имеет свой отдельный адрес и порт. Количество сервисов постепенно растет, и я не удивлюсь, если через полгода их уже будет 100+. Естественно в таком подходе вопрос настройки сервиса и клиента встает в полный рост. Руками такое править сложно, да и не хочется. При таком количестве сервисов риск того, что порт будет уже занят, сильно отличен от нуля.

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

Дополнительного веселья добавляет тот факт, что сервисы могут запускаться от имени пользователя не входящего в группу Администраторов. Для этого надо будет резервировать для него пространство имен, в рамках которого пользователь сможет создавать WCF сервисы.

Наступает момент выпуска беты и время рвать волосы на голове, так как настройка приложения получается совсем не тривиальной, как для разработчиков, так и для пользователей. Чрезмерно сложные программы обновления файлов конфигураций, проблема доставки конфигураций до клиентов – куча никому не нужной мороки.

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

 

Подготовка

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

Требуется: без переписывания существующего кода сделать автоматическое обновление конфигураций на клиенте – Задача минимум. Задачей максимум будет автоматическая регистрация серверной части и уведомление подключенных клиентов, если что-то меняется.

Пока что будет рассмотрена только задача минимум.

Для полной аутентичности в примере хост WCF сервисов находится в консольном приложении, сервисов 2. Есть клиентское приложение, которое их использует.

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

public class ServiceMax : IServiceMax {
	public int Execute(List<int> array) {
		return array.Max();
	}
}

public class ServiceSum : IServiceSum {
	public int Execute(List<int> array) {
		return array.Sum();
	}
}

public class ServiceMin : IServiceMin {
	public int Execute(List<int> array) {
		return array.Min();
	}
}

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

ServiceConnection это библиотека, которая делает все что необходимо нам. Она будет включаться в серверную и в клиентскую части.

Поднятие сервисов выглядит тоже достаточно просто, без заморочек. Пример для сервиса А:

internal static class Program {
	private static ServiceHost hostMax;
	private static ServiceHost hostSum;

	private static void Main() {
		StartAsConsole();
	}

	private static void StartAsConsole() {
		hostMax = new ServiceHost(typeof (ServiceMax));
		hostSum = new ServiceHost(typeof (ServiceSum));

		hostMax.Open();
		hostSum.Open();

		Console.WriteLine("press any key to stop...");
		Console.ReadLine();

		hostMax.Close();
		hostSum.Close();
	}
}

В примере видно, что хосты сервисов создаются и открываются в лоб, без всяких примочек. Теперь представьте что строчек типа

private static ServiceHost hostMax;

private static ServiceHost hostSum;

50 с небольшим штук. И есть тенденция к увеличению. Так как нужно минимальное вмешательство в код, а массовое переименование и наследования вызовет реакцию близкую к shit hits the fan, то мы пойдем другим путем.

Главные слова при внедрении чего-то нового звучат так: «Надо дописать всего две-три строки в существующий код и все!». Абсолютно неважно, что при этом надо будет подключить некоторое количество библиотек. Хотя если у вас все поместилось в одну, то это тоже несомненный плюс о котором стоит сообщить.

На клиенте соединение с серверной частью тоже выглядит немудрено, так как все в конфигурации:

private ServiceSumClient serviceSumClient;
private ServiceMinClient serviceMinClient;
private ServiceMaxClient serviceMaxClient;

private void InitServices() {
	serviceSumClient = new ServiceSumClient("WSHttpBinding_IServiceSum");
	serviceMinClient = new ServiceMinClient("WSHttpBinding_IServiceMin");
	serviceMaxClient = new ServiceMaxClient("WSHttpBinding_IServiceMax");
}

Ради простоты примера весь код клиента написан в классе самой формы. Как вы можете догадаться тут тоже, желательно ничего не менять  и не переписывать. Только если немного дописать.

Как уже упоминалось выше, основная идея будет в том, чтобы поднять внутрисетевой канал, по которому передавать сведения об изменениях серверной части.

Использование

О реализации будет позже, пока что покажу, как выглядит использование API библиотеки.

На сервере надо будет задать адрес служебного сервиса, указать в какой сборке искать хосты WCF  и запустить его.

private static void Main() {
	starter = new Starter();
	starter.StartDiscoveryService("net.tcp://localhost:10001/ClientDiscoveryHostServiceB");

	// ...

	starter.StopDiscoveryService();
}

private static void StartAsConsole() {
	hostMax = new ServiceHost(typeof (ServiceMax));
	hostSum = new ServiceHost(typeof (ServiceSum));

	// ... описание остальных хостов

	starter.AddDiscoveryInfoBy(typeof (Program));

	// ...
}

В начале создаем класс Starter, запускаем сервис обнаружения и обновления, говорим по какому адресу он будет доступен. После инициализации всех хостов говорим нашему служебному классу найти всю информацию по хостам в указанном классе.  Всего получилось 4 строчки кода.

Такую вставку кода надо будет сделать в каждом модуле программы, который поднимает WCF сервисы.

Для клиента надо будет написать примерно такое же количество строк кода.

hostDiscoveryA = new HostDiscovery(new Uri("net.tcp://localhost:10000/ClientDiscoveryHostServiceA"));
hostDiscoveryA.GetAddressesFor(Assembly.GetExecutingAssembly());
hostDiscoveryA.UpdateEndPoints(this);
hostDiscoveryA.UpdateConfig(xDocument);

– где хDocument это файл конфигурации клиента.

  1. Первым делом организуем канал обновления. Указываем необходимый адрес.
  2. Получаем все адреса с сервера для используемых сервисов.
  3. Так как клиентские классы объявлены в code behind формы, и там же находится этот код, то в метод UpdateEndPoints передается объект формы. Обновляем адреса на лету.
  4. Обновляем файл конфигурации.

Все, больше на клиенте ничего не надо.

Основная библиотека для наращивания функционала и для повторного использования –это  ServiceConnection. Можно взять и начать использовать в любом проекте, на любой стадии.

Да, как большинство уже догадалось, цена такому решению – рефлексия. Но не все так страшно как может показаться. Тестовый пример можно дополнить выполнением в фоновом режиме.

Скачать и посмотреть исходный код можно с Assembla

Для запуска проекта предварительно надо будет запустить AddServices.bat с правами администратора, чтобы зарезервировать адреса для работы.

Описание работы

Основная библиотека состоит из сервиса, класса инициализации для серверной части и класса поддержки для клиентской части.

Серверная часть:

public class Starter {
	private static ServiceHost serviceHost;

	/// <summary>
	/// Создание и открытие канала синхронизации\конфигурации
	/// </summary>
	/// <param name="discoveryChannel">адрес сервиса. Должен задаваться на серверной и клиентской стороне</param>

	public void StartDiscoveryService(string discoveryChannel) { … }

	/// <summary>
	/// Остановка канала для синхронизации\конфигурации
	/// </summary>
	public void StopDiscoveryService() {
		serviceHost.Close();
	}

	/// <summary>
	/// Обнаружение и сохранение адресов "рабочих" сервисов/служб
	/// </summary>
	/// <typeparam name="T">Интерфейс службы/сервиса</typeparam>
	/// <param name="host">Служба/сервис за настройками которой надо следить</param>
	public void AddDiscoveryInfoFor<T>(ServiceHost host) { … }

	private static void AddDiscoveryInfoFor(object host) { … }

	/// <summary>
	/// Обнаружение ServiceHost в указанном классе
	/// </summary>
	/// <param name="obj">Класс в котором надо найти ServiceHost'ы</param>
	public void AddDiscoveryInfoBy(Type obj) { … }
}

Из интерфейса класса очевидна его работа. Наиболее интересные методы, это:

  • AddDiscoveryInfoFor – позволяет точечно добавлять хосты и интерфейсы соответствия для отслеживания изменения конфигурации.
  • AddDiscoveryInfoBy – самостоятельно находит и добавляет хосты сервисов.

Так или иначе оба метода в итоге обращаются к

void AddDiscoveryInfoFor(object host)

который уже и занимается разбором оконечной точки сервиса. Никакого rocket science, вся информация содержится в оконечной точке в доступном виде. После разбора информация об интерфейсе и адресе заносится в ClientDiscoveryHostService, чтобы клиент мог получить эту информацию.

private static void AddDiscoveryInfoFor(object host) {
	ServiceHost xhost;
	if (host is ServiceHost) {
		xhost = (ServiceHost) host;
	}
	else {
		return;
	}

	var baseAddresses = xhost.BaseAddresses;

	if (xhost.Description.Endpoints.Count == 0) return;

	var serviceEndpoint = xhost.Description.Endpoints[0];
	var contractDescription = serviceEndpoint.Contract;

	ClientDiscoveryHostService.AddEndPoints(contractDescription.Name, baseAddresses.ToList().ConvertAll(i => i.ToString()));
}

 

Клиентская часть

Клиентская часть чуть больше по реализации, но не сложнее.

public class HostDiscovery {

	private readonly IClientDiscoveryHostService discoveryHost;
	private static readonly Dictionary<string, string> addresses = new Dictionary<string, string>();

	/// <summary>
	/// Создние клиентской части
	/// </summary>
	/// <param name="discoveryServiceUri">Адрес до серверной части служебного сервиса</param>
	public HostDiscovery(Uri discoveryServiceUri) { … }

	/// <summary>
	/// Получить адрес для указанного типа "рабочего" сервиса
	/// </summary>
	/// <param name="type">"Рабочий" сервис</param>
	/// <returns>Полный адрес сервиса</returns>
	public IEnumerable<string> GetAddressFor(Type type) { … }

	/// <summary>
	/// Получает адреса для всех "рабочих" сервисов из указанной сборки
	/// </summary>
	/// <param name="assembly">Сборка для сканирования</param>
	/// <returns>Список адресов для сервисов из переданной сборки</returns>
	public IEnumerable<string> GetAddressesFor(Assembly assembly) { … }

	/// <summary>
	/// Обновление адресов полученных от сервера на лету.
	/// Выполнять после получения адресов.
	/// </summary>
	/// <param name="obj">Класс, где объявлены клиентские сервисы</param>
	public void UpdateEndPoints<T>(T obj) where T : class { … }

	private static void UpdateEndpointAddress(ServiceEndpoint serviceEndpoint) { … }

	/// <summary>
	/// Обновляет файл конфигурации приложения по итогам получения адресов. Перед использованием надо вызвать
	/// либо GetAddressFor, либо GetAddressesFor
	/// </summary>
	/// <param name="document">Конфигурация сервисов</param>
	/// <returns>Документ с новыми адресами</returns>
	public XDocument UpdateConfig(XDocument document) { … }
}

Поднятие сервиса обнаружения и обновления делается полностью в коде, не требуется никаких файлов конфигураций.

Реализация методов не составляет большого труда, если четко себе проговорить, как собираешься искать нужные элементы. Например метод GetAddressesFor:

  • Получить все типы в сборке
  • Найти все интерфейсы для которых указан атрибут ServiceContract. Без этого не поднять WCF сервис.
  • Получить оконечные точки для найденых интерфейсов.
  • Опросить оконечные точки на предмет их адреса.
public IEnumerable<string> GetAddressesFor(Assembly assembly) {
	var list = new List<string>();

	var types = assembly.GetTypes();
	foreach (var type in types) {
		var interfaces = type.GetInterfaces()
								.Where(i => i.GetCustomAttributes(false)
								.OfType<ServiceContractAttribute>()
								.Count() > 0)
						.SelectMany(GetAddressFor);
		list.AddRange(interfaces);
	}

	return list.Distinct().AsEnumerable();
}

Код может не самый оптимальный, но наглядный )))

Обновление оконечных точек на лету в методе UpdateEndPoints тоже не сложный процесс, если помнить о том, что надо попробовать найти всевозможные поля и свойства класса в зависимости от модификаторов доступности и видимости.

И еще одни маленький нюанс. Так как идет поиск по клиентским автосгенерированным классам, которые объявляются как

 

public class ServiceSumClient : ClientBase<IServiceSum>, IServiceSum { … }
public class ServiceMinClient : ClientBase<IServiceMin>, IServiceMin { … }

 

то легче всего обратиться к базовому классу через приведение к dynamic

var genericTypeDefinition = baseType.GetGenericTypeDefinition();
if (genericTypeDefinition == typeof (ClientBase<>)) {
	var serviceEndpoint = ((ServiceEndpoint) ((dynamic) info.GetValue(obj)).Endpoint);
	UpdateEndpointAddress(serviceEndpoint);
}

 

Кратенько наверно это все, что я хотел рассказать.

Скачать и посмотреть исходный код можно с Assembla

 

Hard’n’heavy!

 

 

Комментарии (0) Пинги (0)

Пока нет комментариев.


Оставить комментарий


Нет обратных ссылок на эту запись.