Сообщений 18    Оценка 1180 [+1/-0]         Оценить  
Система Orphus

Технологии построения распределенных приложений в .NET

Часть 1. Пространство имен System.Net

Автор: Мика Сухов
Источник: RSDN Magazine #2-2004
Опубликовано: 06.11.2004
Исправлено: 10.12.2016
Версия текста: 1.0
Вступление
Сокеты
Протокол TCP
Протокол UDP
Протокол HTTP
Протокол SMTP
Сетевая статистика
Безопасность на основе доступа к коду
Не реализованные технологии
Заключение

Исходные тексты (Whidbey beta1)
Исходные тексты (VS 2003)

Вступление

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

Сокеты

Сокеты появились тогда, когда начали создавать первые системы распределенных вычислений. И, скорее всего, о них еще не скоро забудут. Их любят многие за гибкость и простоту в использовании. На их основе можно очень быстро и просто сделать первый прототип любого распределенного приложения. Присутствуют они и в .NET (System.Net.Sockets). В листинге 1 представлен простейший пример взаимодействия “клиент-сервер”.

Листинг 1. Пример простого “клиент-сервер” взаимодействия, которое пересылает данные по сокету из одного участка программы в другой ;o).

Протокол TCP

В .NET есть и более высокоуровневые надстройки над сокетами. Классы TcpClient и TcpListener предоставляют функциональность, осуществляющую коммуникации не на уровне сокетов, а на уровне потока ввода/вывода. Для этого используется класс NetworkStream. Данный класс является наследником System.IO.Stream, но переопределенные им методы чтения и записи не записывают данные ни в какие внутренние хранилища (файлы, массивы байт), а передают прямо в сокет на сервер.

Для демонстрации работы TCP-классов я решил написать самопальный OORPC (Object-Oriented Remote Procedure Call). В двух словах скажу, что это такое, и зачем нужно. Если еще раз посмотреть на предыдущий пример и оценить, какая функциональность при коммуникации должна быть заложена в распределенном приложении, можно отчетливо увидеть, что сокеты годятся лишь для малых проектов. Писать сложные коммуникативные блоки на них очень сложно и неудобно. Именно для этого я и написал механизм под названием “Дальнозов”®.

ПРИМЕЧАНИЕ

Довольно часто на форумах сайта RSDN (http://rsdn.ru) можно видеть такие идеи, как написание русскоязычной версии языка C#. Признаюсь, вначале я довольно скептически относился к данному проекту. Но в последнее время начал задумываться, почему же такой мощный по своим лингвистическим возможностям язык, как русский, не используется при написании .NET-программ. Я решил внести свою скромную лепту в эту идею. Именно поэтому, данная технология носит название “Дальнозов”, и никак не “RemoteCall” или “FarExecute”. Кстати, название было придумано Тимофеем Казаковым (TK), который предложил ряд русских интерпретаций названий .NET-технологий. Я думаю, они не заставят себя долго ждать ;o).

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

В листинге 2 представлен код тестового серверного приложения. Класс Launcher помечается атрибутом ServiceAttribute, сигнализирующим о том, что его объект может быть доступен из другого домена. В конструктор ServiceAttribute передается URI класса.

Листинг 2. Код сервера, использующий технологию “Дальнозов”.

А вот код клиента.

Листинг 3. Код клиента, использующий технологию “Дальнозов”.

Как видите, все прозрачно и легко. Все самое интересное, конечно же, скрыто внутри классов, в частности, в классе RemotingServices, представленном в листинге 4.

Листинг 4. Вспомогательный класс, используемый серверной и клиентской сторонами для инициализации инфраструктуры технологии “Дальнозов”.

Для удобности понимания процесса передачи данных с клиента на сервер приведу UML-диаграмму последовательности вызовов моих классов, представленную на рисунке 1.


Рисунок 1. Процесс доставки клиентского вызова на сервер.

На самом деле все просто. Класс TransparentProxy, дублирующий функциональность зарегистрированного серверного типа (на рисунке помечен как ServerObject), в теле своих методов создает объект Message, передавая в него uri объекта, название вызываемого метода и значения входящих параметров. После этого TransparentProxy сериализует полученный объект Message с помощью BinaryFormatter и отправляет данные на сервер. На сервере экземпляры TcpListener (которые создаются при вызове метода RemotingServices.RegisterChannel) принимают запрос и десериализуют запрос. Полученный объект Message передается в метод соответствующего экземпляра RealProxy, где происходит вызов метода серверного объекта через Reflection. После исполнения серверного кода, возвращаемые значения (return, out, ref) упаковываются в Message, сериализуются обратно в массив байтов и отправляются клиенту. Последний десериализует ответ и возвращает измененные значения параметров клиентскому коду.

Но есть одна проблема. Дело в том, что на клиенте должен всегда присутствовать тип, которые декларирует в точности все методы серверного объекта. В теле этих методов будет происходить сериализация параметров вызываемого метода с помощью binary-форматтера и передача информации через сокет. Но тогда получается, что как только серверный тип изменится (или мы захотим работать с другим серверным объектом), придется заново писать тип-заглушку. Выход есть. Можно создавать этот тип “на лету” c помощью кодогенерации из пространства имен System.Reflection.Emit. При этом никакого кода каждый раз писать не нужно. Дешево и эффективно. Именно так я и сделал. С помощью Reflection я вытаскиваю все методы типа, для которого мне нужно будет создать заглушку. Затем создаю тип заглушки, наследуя его от реального. В заглушке реализую все те методы, которые есть у базового типа. В каждом методе происходит процесс сериализации, отсылки по сокету, принятия ответа и десериализации.

ПРИМЕЧАНИЕ

Код создания заглушек (да и вообще всех листингов) вы найдете в классе TransparentProxyGenerator (в исходных текстах, сопровождающих статью).

Безопасность

Безопасность - один из наиболее важных моментов в построении распределенного приложения. Рассказать об использовании технологий коммуникации в .NET, но не упомянуть о стандартном решении безопасности в этих технологиях было бы неправильно. Именно поэтому я решил написать о стандартной реализации безопасности, предоставляемой .NET для технологий удаленного взаимодействия.

Что ж, вступление пройдено, поэтому перейдем к теории и реализации.

Для реализации безопасного соединения через протокол TCP (да и вообще через любой транспортный протокол) в .NET существует класс AuthenticatedStream. Это абстрактный базовый класс для всех потоков данных (Stream), для которых может быть применено то или иное решение безопасности. Так, например, в .Net стандартно существует два его наследника, NegotiateStream и SslStream.

Класс NegotiateStream позволяет аутентифицировать клиента через такие протоколы как NTLM и Kerberos. Тип аутентификации выбирается автоматически. Если текущая версия ОС клиента Win9х – используется NTLM, иначе – в зависимости от типа протокола аутентификации, который поддерживает сервер. Чаще всего используется Kerberos (в Windows 2003 этот протокол является основным). Если же сеть не объединена в домен, всегда используется NTLM. NTLM также используется, если на сервере используется NT4 или более ранняя версия.

В начале немножко теории о том, как работают протоколы NTLM и Kerberos.


Рисунок 2. Процесс аутентификации по протоколу NTLM.

На рисунке 2 показано, как работает процесс аутентификации по протоколу NTLM (Microsoft Windows NT LAN Manager). В начале клиент посылает свой логин и имя домена на сервер. Тот перенаправляет его контроллеру домена. Получив запрос, контроллер формирует случайным образом пакет, challenge, и шифрует его клиентским паролем. Challenge посылается клиенту. Клиент дешифрует challenge, создает ответ и шифрует его с помощью challenge. Зашифрованный ответ отсылается на сервер, который перенаправляет его на контроллер домена. Контроллер дешифрует ответ и проверяет его. Если все нормально, контроллер уведомляет сервер, что клиент аутентифицирован.

ПРИМЕЧАНИЕ

Протокол NTLM может работать и в сети компьютеров, которые не объединены в домен. В этом случае всю работу по аутентификации проделывает компьютер, к которому обратился клиент.


Рисунок 3. Процесс аутентификации по протоколу Kerberos.

Существует и более надежный протокол аутентификации, Kerberos. На рисунке 3 представлен его алгоритм. Клиент посылает запрос о том, что хочет получить ticket-granting ticket (TGT), билет, использующий во время сеанса работы с Центром Распределения Ключей (ЦРК). ЦРК создает TGT, а также сессионный ключ (СК), зашифрованный клиентским паролем. СК будут шифроваться все последующие пакеты, отправляющиеся на ЦРК. Клиент дешифрует СК, создает authenticator (имя клиента + отметка текущего времени) и шифрует его с помощью СК. После этого клиент отправляет на ЦРК запрос, содержащий TGT, authenticator и имя сервера, с которым будет работать клиент. ЦРК дешифрует TGT и authenticator, и сравнивает отметки времени (это нужно для того, чтобы ключ не подменили в процессе аутентификации). Максимальная разница в отметке времени не должна превышать пяти минут. После этого ЦРК создает билет, зашифрованным секретным паролем сервера, с которым будет работать клиент, и создается серверный сессионный ключ (ССК), с помощью которого будут шифроваться все пакеты, отправляемые на сервер. ССК шифруется с помощью СК. После этого клиент дешифрует ССК, создает authenticator, шифрует его с помощью ССК и отправляет его вместе с билетом на сервер. Сервер дешифрует билет, получает ССК, дешифрует authenticator и получает Identity клиента. Если требуется взаимная аутентификация (mutual authentication, аутентификация, при которой клиент удостоверяется, что его аутентифицирован конкретный сервер), сервер отсылает authenticator клиенту.

Преимущества Kerberos перед NTLM состоят в следующем:

Существует еще и протокол Negotiate. Он служит для того, чтобы выбрать наиболее подходящий для общения протокол сетевой безопасности (NTLM или Kerberos).

Вернемся к классу NegotiateStream. На сервере вызывается метод NegotiateStream.ServerAuthenticate. На клиенте нужно вызвать метод NegotiateStream.ClientAuthenticate, через который можно задать уровень имперсонации. Для используется перечислитель ImpersonationLevel (System.Net). Ниже в таблице 1 представлено описание его полей.

Impersonation Идентифицировать клиента и дать возможность его Identity работать с локальными ресурсами.
Delegation То же самое, что и Impersonation, но также разрешить работать с удаленными ресурсами (поддерживается только протоколом Kerberos).
Identify Эта опция нужна лишь для того, чтобы получить имя учетной записи клиентского приложения.

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

Листинг 5. Код клиента, устанавливающий уровень олицетворения в Delegation.
Листинг 6. Код сервера, выводящий в консоль название учетной записи клиентского приложения.

Второй наследник класса AuthenticatedStream SslStream предназначен для работы с шифрованными данными по сети. Аналогично классу CryptoStream, работающему с криптопровайдерами из пространства имен System.Security.Cryptography, SslStream работает с классами сертификатов стандарта X.509v3 (System.Security.Cryptography.X509Certificates). SslStream поддерживает такие протоколы безопасности, как SSL версии 2, SSL версии 3, TLS, PCT. Но прежде чем начать работу с этим классом, нужно получить сертификат. Конечно же, для теста я не буду покупать сертификат в центре сертификации. Я его сгенерирую с помощью утилиты makecert, которая поставляется вместе с .NET. Вот код bat-файла, генерирующий сертификат сервера в этом тесте.

Makecert –sr localmachine –ss My –n “CN=mika” –r mika.cer -sk mika.pvk

Сертификат сервера должен содержаться в хранилище My, ассоциированном с локальной машиной, и содержать имя “CN={targetHost}”, где значение targetHost передается в конструктор класса SslStream.ClientAuthenticate.

Давайте добавим и это нововведение в нашу технологию "Дальнозов". Вот как теперь выглядит код установления безопасного соединения.

Листинг 7. Код клиента, устанавливающий безопасное соединение на основе сертификатов.
Листинг 8. Код сервера, устанавливающий безопасное соединение на основе сертификатов.

Протокол UDP

В .NET есть надстройка над сокетом, общающимся через UDP. Это класс UdpClient. Но пусть слово “Client” не вводит вас в заблуждение. UdpListener в .NET нет. UdpClient по сути является одновременно и клиентом, и сервером. Соответственно, его методы Receive и Send нужны именно для двунаправленного общения.

Вы хоть раз хотели создать свой чат? Я – да. Тогда я даже не приступил к стадии проектирования, оставив задумку. Но теперь настало время исправить ошибки прошлого. Для демонстрации UDP я написал программу, которая может функционировать в двух режимах: в локальной сети и в Интернет. В первом случае используется широковещательная рассылка (broadcast), во втором – групповая (multicast).

Если вы не знакомы с такими понятиями как broadcast и multicast, советую прочитать нижеследующее. Broadcast- и multicast-рассылки созданы для того, чтобы за один вызов метода Send отослать данные сразу нескольким пользователям. Отличие их состоит в следующем.

Для multicast в Интернете существуют специальные UDP-сервисы (их IP-адреса: 224.0.0.0 - 239.255.255.255, причем диапазон 224.0.0.0 – 224.0.0.255 зарезервирован для использования их маршрутизаторами, так что их использовать не рекомендуется), к которым пользовательские приложения посылают уведомления о включении и исключении их из группы рассылок сообщений (Socket.JoinMulticastGroup и Socket.DropMulticastGroup). Как только будет получено такое сообщение, сервер записывает или вычеркивает IP-адрес клиентской машины в своей виртуальной таблице рассылки сообщений. Ключом в данной таблице, как несложно догадаться, является порт, на который было послано сообщение о подписке. Теперь любое UDP-сообщение, посланное на конкретный порт, будет автоматически переслано всем этим подписчикам.

ПРЕДУПРЕЖДЕНИЕ

В MSDN написано, что UDP-сервисы доступны в диапазоне IP-адресов от 224.0.0.2 до 244.255.255.255. Когда я первый раз увидел последний адрес, то был несколько обескуражен. Еще раз посмотрите на тот диапазон, который я привел выше. Диапазон оканчивается IP-адресом 239.255.255.255, что означает, что он значительно уже, чем пишет об этом Майкрософт. Может быть, компания рассчитывала, что к выходу следующей версии .NET (Whidbey) будут добавлены еще несколько UDP-серверов. Но, так или иначе, IP-адрес 240.0.0.0 считается неверным при использовании контекста групповой рассылки.

У одного из перегруженных методов Socket.JoinMulticastGroup есть такой параметр, как TTL (time-to-live). Этот параметр означает максимальное количество маршрутизаторов, через которое может пройти UDP-пакет. Например, если между UDP-сервисом и клиентским приложением стоят три маршрутизатора, а значение TTL равно двум, то приложение никогда не получит извещение от этого UDP-сервиса.

Стоит упомянуть, что для успешной доставки сообщения от UDP-сервиса к конкретному приложению все маршрутизаторы должны поддерживать протокол IGMP. Вам, скорее всего, не стоит об этом заботиться, так как вряд ли ваш провайдер использует маршрутизатор, не поддерживающий этого протокола. Но вот если у вас реализована собственная прокси-программа, то об этом стоит позаботиться.

Принцип broadcast-рассылки несколько отличается. Для такой рассылки не нужно никаких специальных серверов, так как источниками служат все маршрутизаторы, встречающиеся на пути сообщения, посланного от одной машины к другой. Дело в том, что при broadcast-рассылке задается не конкретный IP-адрес конечной машины, а диапазон, в который он входит. Например, вы находитесь в локальной сети, и ваш IP-адрес 192.168.100.N. Вы хотите послать сообщение на машины с IP-адресами 192.168.100.M и 192.168.100.L. Если вы будете посылать это сообщение через broadcast, то вам нужно указать адрес 192.168.100.255. Теперь ваше сообщение будет отослано всем машинам в данной подсети. При этом машинам, не участвующим в рассылке, также придется обрабатывать пакет.

Проанализировав все выше написанное, можно прийти к следующему выводу. Посылать через Интернет broadcast-сообщения бессмысленно, а вот через multicast – очень удобно. И, наоборот, в локальной сети, когда почти все машины участвуют в коммуникации, multicast был бы не очень удобен (если, конечно, вы не установите себе на какой-нибудь компьютер UDP-сервис multicast-рассылки), чего нельзя сказать про broadcast, который подошел бы в данной ситуации как нельзя лучше.

Однако вернемся к нашей чат-программе. В листинге 9 представлены классы для высокоуровневого взаимодействия по протоколу UDP. Пример их использования представлен в листинге 10.

Листинг 9. Классы для осуществления взаимодействия по протоколу UDP.
Листинг 10. Кусок кода чат-программы, отвечающий за обработку входящей и исходящей информации.
ПРИМЕЧАНИЕ

Приглядитесь к реализации класса ChatUdpListener. В отличие от ChatUdpClient, он работает не с экземпляром UdpClient, а с сокетами. Дело в том, что метод UdpClient.Receive – блокировочного характера. И, соответственно, когда из другого потока вызывается метод UdpClient.Close, происходит ошибка закрытия сокета. Решение: реализовать асинхронное получение данных через UdpClient.BeginReceive. Но тут есть одна проблема, этого метода нет. Я не могу понять, почему разработчики .NET не реализовали этот метод, но деваться некуда. Поэтому сначала нужно вытащить сам сокет из UdpClient через свойство UdpClient.Client (кстати, в .NET 2.0 это поле объявлено как public, в отличие от ранних версий, где оно имело атрибут protected), а затем через метод Socket.BeginReceiveFrom создавать потоки, обрабатывающие входную информацию. Именно так я и сделал.

Безопасность

В .NET нет стандартных решений для построения безопасного соединения на основе протокола UDP. Да это и не удивительно, так как этот низкоуровневый протокол был разработан для быстрой доставки данных без подтверждения.

Протокол HTTP

Связка классов HttpWebRequest и HttpWebResponce, наследников классов WebRequest и WebResponce, умеет посылать запросы Web-серверам по протоколу HTTP и получать ответы. Их серверный собрат, который должен появиться в .NET версии 2.0, HttpListener, полностью реализует функциональность Web-сервера (кстати, когда я начал писать данную статью, то этот класса назывался HttpWebListener ;o) ).

Начать работу с HttpListener довольно просто. Вначале нужно создать объект этого класса. После этого необходимо зарегистрировать http-префикс (HttpListener.Prefixes), с которого будут начинаться все запросы, адресованные нашему приложению. Затем нужно вызвать метод HttpListener.Start, чтобы активизировать работу сервера. И, для того, чтобы получить запрос необходимо вызвать метод HttpListener.GetContext. Как только придет запрос, этот метод вернет экземпляр класса HttpListenerContext, в котором будет содержаться вся нужная информация о запросе.

ПРЕДУПРЕЖДЕНИЕ

Класс HttpListener работает только с IIS версии не ниже 6.0. Это связано с тем, что HttpListener создает очередь HTTP-запросов (WinAPI-функция HttpCreateHttpHandle из httpapi.dll), которые пополняются непосредственно слушающим HTTP-сокетом, реализованным на уровне ядра. Все это появилось только в Windows 2003.

Для примера я реализовал простенький Web-сервер, который хостит ASP.NET (System.Web.Hosting) и обрабатывает GET-запросы к ASPX-ресурсам.

Листинг 11. Классы, отвечающие за реализацию функциональности Web-сервера.
Листинг 12. Инициализация хостинга для Web-сервера.

Как показано на рисунке 4, сервер, получая запрос от клиента (объект класса ListenerWebRequest), по расширению вытаскивает нужный ему хост-обработчик (в тестовой программе поддерживается лишь расширение запроса “.aspx”). После этого в хост-обработчик передаются данные запроса и выходной поток, в который будет записываться ответ. После обработки запроса заполняются соответствующие выходные HTTP-заголовки, и ответ отсылается клиенту через ListenerWebResponse.


Рисунок 4. Процесс обработки запроса Web-сервером.

ПРИМЕЧАНИЕ

Вкратце расскажу, как пользоваться классами для хостинга ASP.NET-приложений. В самом начале нужно создать Domain, который будет хостить инфраструктуру ASP.NET. Это можно сделать, например, через метод ApplicationHost.CreateApplicationHost. Этот метод принимает три параметра: тип, через который происходит общение с ASP.NET, виртуальный путь (например, MyWebApp) и физический путь к Web-приложению. Главное, чтобы по физическому пути в корне каталога находился конфигурационный файл web.config домена ASP.NET, и папка bin со сборкой, в которой находится код Web-приложения. В тестовом примере физический путь соответствует тому пути, где находится само приложение, и по нему содержится всего одна страничка, Default.aspx. Затем, чтобы передать запрос, нужно создать объект класса HttpWorkerRequest. Создать его не удастся, так как он абстрактен, а вот его наследника, SimpleWorkerRequest, создать можно. Но одно условие, данный класс поддерживает обработку только запросов типа GET. Отсюда и ограничение в моей программе. Но если есть желание обрабатывать полноценные запросы, например, POST, советую посмотреть на реализацию Cassini. Чуть переделав их обработчик, можно получить желаемое.

Итак, передав в конструктор SimpleWorkerRequest имя ASP.NET-страницы (TestPage.aspx), строку запроса и объект TextWriter, в который будет записан ответ Web-приложения, мы получим экземпляр обработчика. Теперь нужно начать процесс взаимодействия со средой ASP.NET. Для этого вызовем метод HttpRuntime.ProcessRequest и передадим в него наш экземпляр SimpleWorkerRequest. Все, теперь после окончания выполнения метода мы получим данные, которые будут записаны в TextWriter.

А вот код клиента. Клиент посылает запросы через HttpWebRequest и получает ответ в виде HTML-текста, который выводит ActiveX-control DHTML Editor.

Листинг 13. Код клиентского приложения, который получает ответ от Web-сервера и отображает его.
ПРИМЕЧАНИЕ

В новой версии Visual Studio Whidbey будет контрол WebBrowser. Но о нем я узнал тогда, когда уже написал исходные коды. Так что уж особо не обижайтесь за мой велосипед ;o).

В .NET есть и другие реализации наследников классов WebRequest и WebResponce. Например, классы, отвечающие за взаимодействие по протоколу FTP (FtpWebRequest и FtpWebResponse), или классы взаимодействия с протоколом FILE (FileWebRequest и FileWebResponse). Именно такая функциональность называется “Расширяемыми Протоколами” (“Pluggable Protocols”). Она позволяет придумать свой собственный протокол и реализовать для него MyProtocolWebRequest и MyProtocolWebResponse. Главное – не забыть их зарегистрировать, и внутренняя инфраструктура из пространства имен System.Net тоже сможет их использовать. Это можно сделать через статический метод WebRequest.RegisterPrefix или через файл конфигурации (секция <webRequestModules>). Для этого нужно создать класс, реализующий интерфейс IWebRequestCreate, и зарегистрировать его с нужным префиксом (“HTTP”, “FTP”, “MyProtocol” и т.д.). Теперь можно использовать, например, класс WebClient, для работы по этому протоколу. Умеет он, конечно, немного, но сможет, например, загрузить статическую картинку с Web-сервера, или облегчить проведение операций загрузки и скачивания файлов. WebClient инкапсулирует манипуляции с WebRequest и WebResponce. Передав в объект класса WebClient адрес, содержащий название нашего протокола “MyProtocol”, он автоматически создаст для него объекты наших классов MyProtocolWebRequest и MyProtocolWebResponse. Единственное, что несколько огорчает в “ Расширяемых Протоколах” – это отсутствие аналогов HttpListener для протоколов FTP и FILE.

При работе с HTTP-протоколом вы наверняка столкнетесь с такими понятиями, как ServicePointManager и его управляемая сущность ServicePoint. Первое мы пока опустим, так как с ним связана функциональность более широкого применения, а вот второе сейчас разберем.

ServicePoint – это физическое олицетворение той точки в сети, с которой предполагается работать через классы HttpWebRequest/HttpWebResponse. Оно содержит информацию о конкретной "Web-точке", например, о том, какие клиентские сертификаты будут использоваться при передаче шифрованных сообщений, максимальном количестве подключений (например, через сокеты) или адресе конечной точки, с которым будет ассоциирован запрос. Именно по этому адресу (System.Uri) будет идентифицироваться объект ServicePoint. Вернее, не по самому адресу, а по его хосту (System.Uri.Host). То есть, для путей вида http://rsdn.ru и http://rsdn.ru/Forum/MainList.aspx объекты ServicePoint будут одинаковыми.

ПРИМЕЧАНИЕ

Максимальное количество подключений можно изменить не только через класс ServicePoint, но и, например, через конфигурационный файл. Для этого существует секция <connectionManagement>, в которую добавляются IP-адреса или DNS-имена, которым соответствует числовое значение максимального количества подключений.

Создать объект ServicePoint можно двумя путями. Первый – через метод ServicePointManager.FindServicePoint. Данный метод ищет в своей внутренней таблице объект ServicePoint, и, если не находит последнего, создает новый экземпляр. Второй – получить его через свойство HttpWebRequest.ServicePoint.

Безопасность

С аутентификацией в Web-протоколах, в общем-то, все тривиально. Идентифицирующие данные записываются через свойство WebRequest.Credentials. Но прежде, чем заняться этим, давайте рассмотрим, как происходит сам процесс аутентификации в сети. В примере я использовал протокол HTTP.

Клиент посылает запрос на удаленный сервер. Если сервер требует аутентификации, то клиенту возвращается ответ с кодом ошибки 401 (Authentication Required) и HTTP-заголовок “WWW-Authenticate”, в котором записан тип аутентификации, поддерживаемый сервером (challenge). После этого, клиент должен снова послать запрос, в котором будет присутствовать HTTP-заголовок “Authorization”. В теле этого заголовка будет записана информация, по которой сможет аутентифицировать клиента прокси-сервер. Например, для Basic-аутентификации там будет записано “Basic {DomainName}\\{UserName}:{Password}”, где связка “{DomainName}\\{UserName}:{Password}” содержится в кодировке Base64. Если сервер снова вернул ошибку 401, значит аутентификационные данные недействительны.

Также в .NET существует такое понятие как предварительная аутентификация. В этом случае соответствующие аутентификационные заголовки посылаются сразу, при первом запросе, минуя вышеописанный сценарий. Для этого нужно выставить значение свойства WebRequest.PreAuthenticate в true и зарегистрировать хоть один модуль аутентификации, который умеет выполнять предаутентификационную обработку запроса. Но тут есть одна тонкость. Дело в том, что аутентификация по Web-протоколам устроена таким образом, что сервер сам выбирает ее тип (Basic, Digest и т.д.). Соответственно, клиент не может отослать аутентификационные данные, если не знает типа аутентификации. Поэтому предварительная аутентификация осуществляется только при работе с адресами, уже успешно аутентифицировавшими клиента ранее.

В .NET появилась функциональность, скрывающая детали аутентификации и отделяющая их от кода обработки запросов. Для этого создается класс, реализующий интерфейс IAuthenticationModule, который будет отвечать за аутентификацию клиентского приложения. Этот класс нужно зарегистрировать, что можно сделать двумя путями: прописать название типа в секцию <authenticationModules> или вызвать метод AuthenticationManager.Register.

ПРЕДУПРЕЖДЕНИЕ

Если вы регистрируете свой модуль аутентификации, не забудьте вначале отрегистрировать уже существующие. Это нужно потому, что если какой-то из этих модулей сможет обработать запрос аутентификации, то до вашего собственного модуля обработка не дойдет.

В листинге 14 приведен модуль аутентификации, посылающий запрос с аутентификационными данными. Поле Password в них зашифровано по алгоритму реверсирования символов ;o).

Листинг 14. Демонстрация возможностей IAuthenticationModule.

Осталось немного – реализовать серверную часть, которая будет понимать, что же мы посылаем в аутентификационном токене ;o).

Стандартно .NET поддерживает такие типы аутентификации, как: Digest (System.Net.DigestClient), NTLM (System.Net.NtlmClient), Kerberos (System.Net.KerberosClient), Negotiate (System.Net.NegotiateClient) и, конечно же, Basic (System.Net.BasicClient). Изобретать тут нечего – все уже давно реализовано.

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

Листинг 15. Отправка HTTP-запроса с аутентификационными данными.

Теперь, чтобы наш Web-сервер использовал basic-аутентификацию, нужно установить значение свойства HttpListener.AuthenticationScheme в AuthenticationSchemes.Basic. Если же нужна интеллектуальная аутентификация клиента (например, изменение типа аутентификации для определенных IP-адресов), когда информация о пользователе не посылается в http-заголовке “Authorization”, нужно создать обработчик делегата AuthenticationSchemeSelector и передать его в свойство HttpListener.AuthenticationSchemeSelectorDelegate.

ПРЕДУПРЕЖДЕНИЕ

Как видно из примера, мы не создавали дополнительного элемента “Authorization” в коллекции WebRequest.Headers. Дело в том, что это специальный заголовок, который присутствует изначально в этой коллекции. Если попытаться создать его, будет выдано исключение ArgumentException. Узнать, какие именно HTTP-заголовки нельзя создавать, можно через метод WebHeaderCollection.IsRestricted. В описании этого метода в MSDN как раз и перечислены такие HTTP-заголовки.

Так что же насчет ServicePointManager, уже упоминавшегося выше? Этот класс нужен для управления объектами ServicePoint. Через него можно задать максимальное количество данных сущностей, время существования во внутреннем хранилище, а также протокол безопасности и политику управлениями сертификатами. Именно о сертификатах в HTTP и пойдет сейчас речь.

Как и при использовании TCP, при работе по протоколу HTTP можно использовать сертификаты X.509v3. Чтобы послать зашифрованный запрос на какой-нибудь Web-сервер, нужно создать объект класса HttpWebRequest, передав в него адрес с префиксом “https”. Далее нужно добавить все сертификаты, которые будут использоваться в запросе через свойство HttpWebRequest.ClientCertificates. После этого нужно выставить тот протокол безопасности, через который будет происходить взаимодействие. Это можно сделать через ServicePointManager.SecurityProtocol, по умолчанию установленный в значение SecurityProtocolType.Ssl3. Далее, если нужна собственная аутентификация сервера клиентом и проверка его сертификата, следует реализовать интерфейс ICertificatePolicy и вызвать свойство ServicePointManager.CertificatePolicy.

ПРЕДУПРЕЖДЕНИЕ

По умолчанию в качестве политики управлениями сертификатами задействован класс, который отклоняет установление безопасного соединения при любом коде ошибки. Это неправильно. Дело в том, что есть такой код ошибки, 0x800c010f, которой позволяет продолжить безопасное общение между клиентом и сервером. Чтобы предупредить эту ошибку, следует реализовать свою политику.

Код, представленный в листинге 16, показывает инициализацию клиента для работы с нашим Web–сервером в безопасном режиме, используя сертификаты X.509v3.

Листинг 16. Использование протокола https для получения информации.

Теперь осталось немного – инициализировать сервер и поместить в него серверный сертификат (тот самый, который должен получить клиент для проверки подлинности сервера). Но не все так гладко. Дело в том, что для этого нужно вызвать API-функцию HttpSetServiceConfiguration, чтобы установить сертификат. Но .NET вообще не импортирует эту функцию, а это означает, что HttpListener не работает через безопасное соединение, и об использовании HTTPS можно забыть (это можно проверить, открыв класс HttpApi из пространства имен System.Net каким-нибудь IL-дизассемблером). Надеюсь, к выходу релиз версии .NET 2.0 эта ошибка будет исправлена.

Прокси-серверы

Часто бывают случаи, когда запрос идет не напрямую к Web-серверу, а через прокси-сервер. Чтобы запрос успешно выполнился, его нужно перенаправлять этим прокси-серверам. Для того чтобы задать информацию о них, нужно присвоить свойству WebRequest.Proxy объект класса, реализующего интерфейс IWebProxy. Например, объект класса WebProxy, который предоставляет базовую функциональность по обработке запросов и переадресации их к прокси-серверам. Если у вас в настройках IE уже прописан адрес прокси-сервера, то он будет создаваться автоматически. В этом случае ничего присваивать свойству WebRequest.Proxy не нужно - это осуществляется за счет использования метода WebProxy.GetProxy. Если же информация о прокси-сервере не прописана в настройках IE, ее можно записать через конфигурационный файл. Для этого нужно вписать данные в секцию <proxy>.

Если вы хотите, чтобы по умолчанию создавался не объект класса WebProxy, а ваш собственный наследник IWebProxy, можно опять же воспользоваться конфигурационным файлом. Для этого впишите в секцию <module> свой тип, реализующий IWebProxy. Для автоматической реализации логики обработки bypass-адресов и перенаправления запросов через прокси-сервер, рекомендую наследовать свои классы от класса WebProxy. К счастью, он не объявлен как sealed, что иногда можно увидеть в декларациях очень полезных по своей функциональности классов.

Прокси-серверы также поддерживают аутентификацию. Чтобы пройти этот этап успешно, нужно выполнить точно такие же действия, как и при аутентификации на конечных серверах. Единственное отличие - вписывать идентифицирующие данные нужно в WebRequest.Proxy.Credentials.

ПРИМЕЧАНИЕ

Кстати, на прокси-серверы также распространяется такое понятие, как предварительная аутентификация. Правда, это задается неявно. Если сам запрос WebRequest поддерживает предварительную аутентификацию, то она будет работать и для прокси-сервера.

Протокол SMTP

Видимо, разработчикам уже так надоел основанный на CDO SmtpMail своими ошибками, а Microsoft – документирование этих багов и способов их обходов, что в .NET 2.0 появился новое пространство имен System.Net.Mail (и производное для него System.Net.Mime). Новый класс SmtpClient реализован, как это модно говорить, на pure .NET (за исключением сокетной части), что позволит находить и устранять ошибки в программе значительно проще.

В листинге 18 представлена простейшая программа, демонстрирующая работу с новыми типами. Сперва создается объект класса SmtpClient. Ему передается имя сервера, которому будет послано сообщение. Затем создается и инициализируется объект класса MailMessage, в котором задается адрес отправителя и получателя, а также присоединяется вложение, содержащее строку. И наконец, вызывается метод SmtpClient.Send, который отправляет письмо.

Листинг 18. Программа, посылающая тестовое письмо по протоколу Smtp.

Сетевая статистика

В .NET 2.0 появится класс NetworkInformation (System.Net.NetworkInformation), предоставляющий информацию о стеке протоколов TCP/IP. Так, например, можно узнать число отосланных и принятых UDP-дейтаграмм, или число исходящих IP-пакетов. Это может оказаться полезным, например, при написании административной программы, просмотривающей статистику сервера. Ниже, в листинге 18, представлен JScript.NET-файл, с помощью которого можно получить общую информацию о компьютере и домене, в который он входит.

Листинг 18. JScript-файл, выводящий общую информацию о компьютере
ПРЕДУПРЕЖДЕНИЕ

Данные предоставляются с помощью Win32 API-функций из библиотеки iphlpapi.dll, которая появилась в Windows XP. Соответственно, этот класс не поддерживается в версиях ОС Windows до Windows XP.

Безопасность на основе доступа к коду

Code Access Security (или сокращенно CAS) – родная технология безопасности .NET. Определяет, к какому участку кода имеет доступ приложение. Но как это может применяться при построении коммуникативной части распределенного приложения, спросите вы. Дело в том, что некоторые проверки можно делать на уровне приложения (например, на клиенте можно пресечь доступ к тому или иному ресурсу, пока вызов не вышел за пределы управляемой песочницы). Чтобы понять это, рассмотрим классы SocketPermission и WebPermission. Первый используется для ограничения доступа на уровне сокета, второй – на уровне протокола HTTP. Следующий код, представленный в листинге 19, демонстрирует возможности ограничения доступа на базе CAS.

Листинг 19. Пример демонстрирует, как запретить приложению соединяться с серверами в подмаске “192.168.100.XXX” через порт 8320 по протоколу UDP, и запретить работать с HTTP-клиентами.

Как видно из примера, оба разрешения (permission) используют перечислитель NetworkAccess. Он нужен для определения тех действия, за которые будет отвечать данное разрешение. Так, например, Accept означает, что разрешение будет отвечать за операцию обработки клиентских сокетов, а Connect – за операцию соединения с удаленным сервером.

SocketPermission с помощью TransportType определяет, какой тип транспортного протокола будет контролироваться. В нашем примере используется член UDP, но можно было выставить и Connectionless. Правда, это могло бы пересекаться с каким-нибудь другим протоколом, не ориентированным на постоянное подключение (например, icmp, igmp, ipx или вашим собственным протоколом, реализованным на базе транспортного уровня модели OSI).

Существуют также разрешения, с помощью которых можно позволить или запретить использование DNS-сервисов. Это можно сделать через класс DnsPermission. Например, в листинге 20 представлен код, демонстрирующий данный подход.

Листинг 20. Код программы, который генерирует исключение при вызове метода GetLocalHostAddress из-за того, что у него отняли права на использование DNS-сервиса.

Если мы снимем атрибут DnsPermissionAttribute метода GetLocalHostAddress, то мы все равно получим исключение SecurityException при выполении метода Dns.Resolve, так как разрешение было уже “снято” до этого в методе Main.

Не реализованные технологии

Существует еще несколько технологий удаленного взаимодействия, например, каналы (pipes). Такая технология очень удобна для межпроцессного общения разных программ. Классический пример: программа анализирует довольно-таки большое количество информации. При этом у нее нет пользовательского интерфейса (например, сервис NT). Чтобы узнать, исправно ли она работает, или изменить уже существующие настройки, к ней подключается административная консоль. Делать взаимодействие на сокетах ради локальной программы несколько расточительно. Именно для этого лучше всего использовать каналы.

Но как бы хороша или плоха не была эта технология, для них не нашлось ниши в .NET. Во Framework нет типов, через которые можно работать с каналами. Поэтому единственный способ использования их – это взаимодействие с неуправляемым кодом (System.Runtime.InteropServices), что не может не огорчать.

Заключение

Вот мы и рассмотрели технологии удаленного взаимодействия пространства имен System.Net. Но на этом мы не остановимся. В .NET есть технологии, которые предоставляют более удобный и простой интерфейс использования для построения распределенных приложений. Это Remoting и Web Services. Но об этом и многом другом речь пойдет в следующей статье, “Технологии построения распределенных приложений в .NET. Часть 2”.


Эта статья опубликована в журнале RSDN Magazine #2-2004. Информацию о журнале можно найти здесь
    Сообщений 18    Оценка 1180 [+1/-0]         Оценить